diff --git a/designsafe/apps/api/agave/filemanager/agave.py b/designsafe/apps/api/agave/filemanager/agave.py
index dcb20ed147..4938122b4b 100644
--- a/designsafe/apps/api/agave/filemanager/agave.py
+++ b/designsafe/apps/api/agave/filemanager/agave.py
@@ -28,6 +28,8 @@ class AgaveFileManager(BaseFileManager):
'path': '/corral-repl/tacc/NHERI/shared'},
{'regex': r'^designsafe.storage.community$',
'path': '/corral-repl/tacc/NHERI/community'},
+ {'regex': r'^designsafe.storage.published$',
+ 'path': '/corral-repl/tacc/NHERI/published'},
{'regex': r'^project\-',
'path': '/corral-repl/tacc/NHERI/projects'}
]
@@ -53,7 +55,8 @@ def import_data(self, system, file_path, from_system, from_file_path):
res = f.import_data(from_system, from_file_path)
file_name = from_file_path.split('/')[-1]
reindex_agave.apply_async(kwargs={'username': 'ds_admin',
- 'file_id': '{}/{}'.format(system, os.path.join(file_path, file_name))})
+ 'file_id': '{}/{}'.format(system, os.path.join(file_path, file_name))},
+ queue='indexing')
return res
def copy(self, system, file_path, dest_path=None, dest_name=None):
@@ -75,7 +78,8 @@ def copy(self, system, file_path, dest_path=None, dest_name=None):
# schedule celery task to index new copy
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
- 'file_id': '{}/{}/{}'.format(system, dest_path.strip('/'), dest_name)})
+ 'file_id': '{}/{}/{}'.format(system, dest_path.strip('/'), dest_name)},
+ queue='indexing')
return copied_file
@@ -84,7 +88,8 @@ def delete(self, system, path):
parent_path = '/'.join(path.strip('/').split('/')[:-1])
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, parent_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
return resp
def download(self, system, path):
@@ -109,7 +114,8 @@ def mkdir(self, system, file_path, dir_name):
f = BaseFileResource(self._ag, system, file_path)
resp = f.mkdir(dir_name)
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
- 'file_id': '{}/{}'.format(system, file_path)})
+ 'file_id': '{}/{}'.format(system, file_path)},
+ queue='indexing')
return resp
def move(self, system, file_path, dest_path, dest_name=None):
@@ -119,10 +125,12 @@ def move(self, system, file_path, dest_path, dest_name=None):
parent_path = parent_path.strip('/') or '/'
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, parent_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, os.path.join(dest_path, resp.name)),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
return resp
def rename(self, system, file_path, rename_to):
@@ -131,7 +139,8 @@ def rename(self, system, file_path, rename_to):
parent_path = '/'.join(file_path.strip('/').split('/')[:-1])
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, parent_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
return resp
def share(self, system, file_path, username, permission):
@@ -142,7 +151,8 @@ def share(self, system, file_path, username, permission):
pem.permission_bit = permission
resp = pem.save()
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
- 'file_id': '{}/{}'.format(system, file_path)})
+ 'file_id': '{}/{}'.format(system, file_path)},
+ queue='indexing')
return resp
def trash(self, system, file_path, trash_path):
@@ -171,10 +181,12 @@ def trash(self, system, file_path, trash_path):
parent_path = parent_path.strip('/') or '/'
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, trash_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, parent_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
return resp
def upload(self, system, file_path, upload_file):
@@ -182,5 +194,6 @@ def upload(self, system, file_path, upload_file):
resp = f.upload(upload_file)
reindex_agave.apply_async(kwargs = {'username': 'ds_admin',
'file_id': '{}/{}'.format(system, file_path),
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
return resp
diff --git a/designsafe/apps/api/agave/filemanager/public_search_index.py b/designsafe/apps/api/agave/filemanager/public_search_index.py
index 7feb71f9b7..5ac1f331cb 100644
--- a/designsafe/apps/api/agave/filemanager/public_search_index.py
+++ b/designsafe/apps/api/agave/filemanager/public_search_index.py
@@ -12,6 +12,7 @@
import os
import re
import datetime
+import itertools
from django.conf import settings
from elasticsearch import TransportError
from elasticsearch_dsl import Search, DocType
@@ -38,6 +39,120 @@
except KeyError as e:
logger.exception('ELASTIC_SEARCH missing %s' % e)
+class PublicationIndexed(DocType):
+ class Meta:
+ index = 'published'
+ doc_type = 'publication'
+
+class Publication(object):
+ def __init__(self, wrap=None, project_id=None, *args, **kwargs):
+ if wrap is not None:
+ if isinstance(wrap, PublicationIndexed):
+ self._wrap = wrap
+ else:
+ s = PublicationIndexed.search()
+ s.query = Q({"term":
+ {"projectId._exact": wrap['projectId']}
+ })
+ try:
+ res = s.execute()
+ except TransportError as e:
+ if e.status_code != 404:
+ res = s.execute()
+
+ if res.hits.total:
+ self._wrap = res[0]
+ raise Exception('Initializing from existent publication '
+ 'and a publication object was given. '
+ 'Are you sure you want to do this? ')
+ else:
+ self._wrap = PublicationIndexed(**wrap)
+
+ elif project_id is not None:
+ s = PublicationIndexed.search()
+ s.query = Q({"term": {"projectId._exact": project_id }})
+ logger.debug('p serach query: {}'.format(s.to_dict()))
+ try:
+ res = s.execute()
+ except TransportError as e:
+ if e.status_code == 404:
+ raise
+ res = s.execute()
+
+ if res.hits.total:
+ self._wrap = res[0]
+ else:
+ self._wrap = PublicationIndexed(projectId=project_id)
+ else:
+ self._wrap = PublicationIndexed()
+
+ @classmethod
+ def listing(cls):
+ list_search = PublicSearchManager(cls,
+ PublicationIndexed.search(),
+ page_size=100)
+ list_search._search.query = Q({"match_all":{}})
+ list_search.sort({'projectId._exact': 'asc'})
+ #s = PublicationIndexed.search()
+ #s.query = Q({"match_all":{}})
+ #try:
+ # res = s.execute()
+ #except TransportError as err:
+ # if err.satus_code == 404:
+ # raise
+ # res = s.execute()
+
+ return list_search.results(0)
+
+ @property
+ def id(self):
+ return self._wrap.meta.id
+
+ def save(self, **kwargs):
+ self._wrap.save(**kwargs)
+ return self
+
+ def to_dict(self):
+ return self._wrap.to_dict()
+
+ def to_file(self):
+ dict_obj = {'agavePath': 'agave://designsafe.storage.published/{}'.\
+ format(self.project.value.projectId),
+ 'children': [],
+ 'deleted': False,
+ 'format': 'folder',
+ 'length': 24731027,
+ 'meta': {
+ 'title': self.project['value']['title']
+ },
+ 'name': self.project.value.projectId,
+ 'path': '/{}'.format(self.project.value.projectId),
+ 'permissions': 'READ',
+ 'project': self.project.value.projectId,
+ 'system': 'designsafe.storage.published',
+ 'systemId': 'designsafe.storage.published',
+ 'type': 'dir'}
+ return dict_obj
+
+ def related_file_paths(self):
+ dict_obj = self._wrap.to_dict()
+ related_objs = dict_obj.get('modelConfigs', []) + \
+ dict_obj.get('analysisList', []) + \
+ dict_obj.get('sensorsList', []) + \
+ dict_obj.get('eventsList', [])
+ file_paths = []
+ proj_sys = 'project-{}'.format(dict_obj['project']['uuid'])
+ for obj in related_objs:
+ file_paths += obj['_filePaths']
+
+ return file_paths
+
+ def __getattr__(self, name):
+ val = getattr(self._wrap, name, None)
+ if val:
+ return val
+ else:
+ raise AttributeError('\'Publication\' has no attribute \'{}\''.format(name))
class CMSIndexed(DocType):
class Meta:
@@ -307,7 +422,7 @@ def to_dict(self):
obj_dict = self._doc.to_dict()
obj_dict['system'] = self.system
obj_dict['path'] = self.path
- obj_dict['children'] = [doc.to_dict() for doc in self.children]
+ obj_dict['children'] = [doc.to_dict() if not hasattr(doc, 'projectId') else doc.to_file() for doc in self.children]
obj_dict['metadata'] = self.metadata()
obj_dict['permissions'] = 'READ'
obj_dict['trail'] = self.trail()
@@ -338,6 +453,23 @@ def __init__(self):
def listing(self, system, file_path, offset=0, limit=100):
file_path = file_path or '/'
listing = PublicObject.listing(system, file_path, offset, limit)
+ publications = Publication.listing()
+ #children = [{'agavePath': 'agave://designsafe.storage.published/{}'.format(pub.project.value.projectId),
+ # 'children': [],
+ # 'deleted': False,
+ # 'format': 'folder',
+ # 'length': 24731027,
+ # 'meta': {
+ # 'title': pub.project['value']['title']
+ # },
+ # 'name': pub.project.value.projectId,
+ # 'path': '/{}'.format(pub.project.value.projectId),
+ # 'permissions': 'READ',
+ # 'project': pub.project.value.projectId,
+ # 'system': 'designsafe.storage.published',
+ # 'systemId': 'designsafe.storage.published',
+ # 'type': 'dir'} for pub in publications]
+ listing.children = itertools.chain(publications, listing.children)
return listing
def search(self, system, query_string,
@@ -442,3 +574,13 @@ def search(self, system, query_string,
'permissions': 'READ'
}
return result
+
+class PublicationManager(object):
+ def save_publication(self, publication):
+ publication['projectId'] = publication['project']['value']['projectId']
+ publication['created'] = datetime.datetime.now().isoformat()
+ publication['status'] = 'publishing'
+ pub = Publication(publication)
+ pub.save()
+ return pub
+
diff --git a/designsafe/apps/api/agave/filemanager/published.py b/designsafe/apps/api/agave/filemanager/published.py
new file mode 100644
index 0000000000..22d1c60913
--- /dev/null
+++ b/designsafe/apps/api/agave/filemanager/published.py
@@ -0,0 +1,52 @@
+"""File Manager for published Data
+"""
+
+import logging
+import json
+import os
+import re
+import datetime
+from django.conf import settings
+from .base import BaseFileManager
+from designsafe.apps.api.agave.filemanager.agave import AgaveFileManager
+from designsafe.apps.api.exceptions import ApiException
+from designsafe.apps.api.agave.filemanager.public_search_index import Publication
+
+logger = logging.getLogger(__name__)
+
+class PublishedFileManager(AgaveFileManager):
+ NAME = 'community'
+ DEFAULT_SYSTEM_ID = 'designsafe.storage.published'
+
+ def listing(self, system, file_path, offset=0, limit=100):
+ path_comps = file_path.strip('/').split('/')
+ if len(path_comps) < 1:
+ raise ApiException(messsage='Invalid Action', status=400)
+ elif len(path_comps) == 1:
+ project_id = path_comps[0]
+ publication = Publication(project_id=project_id)
+ return publication
+ else:
+ return super(PublishedFileManager, self).\
+ listing(system, file_path, offset, limit)
+
+ def delete(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def mkdir(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def move(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def rename(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def share(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def trash(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
+
+ def upload(self, *args, **kwargs):
+ return ApiException(messsage='Invalid Action', status=400)
diff --git a/designsafe/apps/api/agave/models/base.py b/designsafe/apps/api/agave/models/base.py
new file mode 100644
index 0000000000..ea988ffe4b
--- /dev/null
+++ b/designsafe/apps/api/agave/models/base.py
@@ -0,0 +1,406 @@
+""" Base classes to handle agave metadata objects """
+import inspect
+import six
+import json
+import re
+import logging
+import datetime
+
+logger = logging.getLogger(__name__)
+
+REGISTRY = {}
+LAZY_OPS = []
+
+class RelatedQuery(object):
+ def __init__(self, uuid=None, uuids=None, related_obj_name=None, rel_cls=None):
+ self.uuid = uuid
+ self.uuids = uuids or []
+ self.related_obj_name = related_obj_name
+ self.rel_cls = rel_cls
+ query = {'name': related_obj_name, 'associationIds': []}
+ self._query = query
+
+ def __call__(self, agave_client):
+ metas = agave_client.meta.listMetadata(q=json.dumps(self.query))
+ return [self.rel_cls(**meta) for meta in metas]
+
+ def add(self, uuid):
+ self.uuids.append(uuid)
+
+ @property
+ def query(self):
+ """JSON query to submit to agave metadata endpoint.
+
+ This class represents both a forward-lookup field and a reverse-lookup field.
+ There are two class attributes ``uuid`` and ``uuids`` (notice the 's').
+ IF ``self.uuid`` has a valid value then it means this is a reverse-lookup field
+ and we need to retrieve all the objects related to this object's specific UUID
+ AND with a specific object name: ``{"name": "some.name", "associationIds": "UUID"}``.
+ IF ``self.uuids`` has a valid value (it could be a string or an array of strings) means
+ this is a forward-lookup field and we need to retrieve every object for every UUID
+ in the ``self.uuids`` attribute: ``{"uuid": {"$in": ["UUID1", "UUID2"]}}``.
+
+ ..todo:: This class should be separated in two classes, one for reverse-lookup fields
+ and another one for forward-lookup fields. The reason why it was first implemented like this
+ is because the implementation of a reverse-lookup field was not completely clear.
+ This TODO is mainly for readability.
+ """
+ if self.uuid:
+ self._query['associationIds'] = self.uuid
+ elif self.uuids is not None:
+ if isinstance(self.uuids, basestring):
+ self.uuid = [self.uuids]
+ elif len(self.uuids) == 0:
+ return []
+
+ self._query = {'uuid': {'$in': self.uuids}}
+ else:
+ raise ValueError('Cannot create query')
+
+ return self._query
+
+def register_lazy_rel(cls, field_name, related_obj_name, multiple, rel_cls):
+ reg_key = '{}.{}'.format(cls.model_name, cls.__name__)
+ LAZY_OPS.append((reg_key,
+ field_name,
+ RelatedQuery(related_obj_name=related_obj_name, rel_cls=rel_cls))
+ )
+
+def set_lazy_rels():
+ for lazy_args in LAZY_OPS:
+ cls = REGISTRY[lazy_args[0]]
+ #lazy_args[2].rel_cls = cls
+ cls._meta._reverse_fields.append(lazy_args[1])
+ setattr(cls, lazy_args[1], lazy_args[2])
+
+ del LAZY_OPS[:]
+
+def register_class(cls, name, model_name):
+ registry_key = '{}.{}'.format(model_name, name)
+ if REGISTRY.get(registry_key) is None:
+ REGISTRY[registry_key] = cls
+
+def camelcase_to_spinal(string):
+ rec = re.compile('([A-Z])+')
+ return rec.sub(r'_\1', string).lower()
+
+def spinal_to_camelcase(string):
+ comps = string.split('_')
+ if string.startswith('_'):
+ comps = comps[1:]
+ first = ''.join(['_', comps[0]])
+ else:
+ first = comps[0]
+ camel = ''.join(x.capitalize() or '_' for x in comps[1:])
+ camel = ''.join([first, camel])
+ return camel
+
+class Manager(object):
+ def __init__(self, model_cls):
+ self.model_cls = model_cls
+ self.agave_client = None
+
+ def set_client(self, agave_client):
+ self.agave_client = agave_client
+ setattr(self.model_cls._meta, 'agave_client', agave_client)
+
+ def get(self, agave_client, uuid):
+ meta = agave_client.meta.getMetadata(uuid=uuid)
+ return self.model_cls(**meta)
+
+ def list(self, agave_client, association_id=None):
+ if association_id is None:
+ metas = agave_client.meta.listMetadata(q=json.dumps({'name': self.model_cls.model_name}))
+ else:
+ metas = agave_client.meta.listMetadata(q=json.dumps({'name': self.model_cls.model_name,
+ 'associationIds': association_id}))
+ for meta in metas:
+ yield self.model_cls(**meta)
+
+class Links(object):
+ def __init__(self, values):
+ for attrname, val in six.iteritems(values):
+ setattr(self, attrname, val)
+
+class Options(object):
+ """Options class to store model's _meta data
+ """
+ _model = None
+ _schema_fields = ['uuid', 'schema_id', 'internal_username',
+ 'association_ids', 'last_updated', 'created',
+ 'owner', 'name', '_links']
+
+ def __init__(self, model_name):
+ self._nested_fields = {}
+ self._related_fields = {}
+ self._reverse_fields = []
+ self._fields_map = {}
+ self._fields = []
+ self.name = model_name
+ self.model_name = model_name
+ self.model_manager = None
+
+ def add_field(self, field):
+ if field.nested_cls:
+ self._nested_fields[field.attname] = field
+ elif field.related:
+ self._related_fields[field.attname] = field
+ else:
+ self._fields_map[field.attname] = field
+ self._fields.append(field)
+
+ def contribute_to_class(self, cls, name):
+ cls._meta = self
+ self._model = cls
+
+
+
+class BaseModel(type):
+ """
+ Metaclass for models
+ """
+ def __new__(cls, name, bases, attrs):
+ super_new = super(BaseModel, cls).__new__
+
+ # Also ensure initialization is only performed for subclasses of Model
+ # (excluding Model class itself).
+ parents = [b for b in bases if isinstance(b, BaseModel)]
+ if not parents:
+ return super_new(cls, name, bases, attrs)
+
+ module = attrs.pop('__module__')
+ new_attrs = {'__module__': module}
+ classcell = attrs.pop('__classcell__', None)
+ if classcell is not None:
+ new_attrs['__classcell__'] = classcell
+
+ new_class = super_new(cls, name, bases, new_attrs)
+ model_name = attrs.get('model_name')
+ new_class.add_to_class('_meta', Options(model_name))
+ if attrs.get('_is_nested', False):
+ setattr(new_class, 'model_name', None)
+ else:
+ setattr(new_class, 'model_name', model_name)
+
+ setattr(new_class, '_is_nested', attrs.pop('_is_nested', False))
+ for obj_name, obj in attrs.items():
+ new_class.add_to_class(obj_name, obj)
+
+ new_class._prepare()
+ register_class(new_class, name, model_name)
+ return new_class
+
+ def add_to_class(cls, name, value):
+ if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
+ value.contribute_to_class(cls, name)
+ else:
+ setattr(cls, name, value)
+
+ def _prepare(cls):
+ opts = cls._meta
+ if cls.__doc__ is None:
+ cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.name for f in opts._fields))
+
+ #if not opts.manager:
+ # if any(f.name == 'objects' for f in opts.fields):
+ # raise ValueError(
+ # "Model %s must specify a custom Manager, because it has a "
+ # "field name 'objects'." % cls.__name__
+ # )
+ # manager = Manager()
+ # manager.auto_created = True
+ # cls.add_to_class('objects', manager)
+ if not opts.model_manager:
+ setattr(cls._meta, 'model_manager', Manager(cls))
+
+ if not isinstance(opts.model_manager, Manager):
+ raise ValueError("Model Manager must be a Manager class.")
+
+
+class Model(object):
+ __metaclass__ = BaseModel
+
+ def __init__(self, **kwargs):
+ if not self._is_nested:
+ self.schema_id = None
+ self.internal_username = None
+ self.last_updated = None
+ self.created = None
+ self.owner = None
+ self.__links = None
+
+ self._uuid = None
+ self._association_ids = []
+ self.name = None
+ self.parent = None
+ #logger.debug('kwargs: %s', json.dumps(kwargs, indent=4))
+ cls = self.__class__
+ opts = self._meta
+ _setattr = setattr
+ #logger.debug('_is_nested: %s', self._is_nested)
+ if self._is_nested:
+ obj_value = kwargs
+ else:
+ obj_value = kwargs.pop('value', {})
+ links = kwargs.pop('_links', {})
+ self._links = Links(links)
+ for attrname, val in six.iteritems(kwargs):
+ attrname = camelcase_to_spinal(attrname)
+ setattr(self, attrname, val)
+
+ for attrname, field in six.iteritems(opts._fields_map):
+ _setattr(self, attrname, self._get_init_value(field, obj_value, attrname))
+
+ for attrname, field in six.iteritems(opts._nested_fields):
+ val = self._get_init_value(field, obj_value, attrname)
+ nested_obj = field.nested_cls(**val)
+ nested_obj.parent = self
+ _setattr(self, attrname, nested_obj)
+
+ for attrname, field in six.iteritems(opts._related_fields):
+ value = self._get_init_value(field, obj_value, attrname)
+ if not value and attrname.endswith('_UUID'):
+ _attr = spinal_to_camelcase(attrname)
+ _attr = ''.join([_attr[:-4], 'UUID'])
+ value = obj_value.get(_attr, None) or value
+
+ _setattr(self, attrname, RelatedQuery(uuids=value, rel_cls=field.related))
+
+ for attrname in opts._reverse_fields:
+ field = getattr(self, attrname)
+ field.uuid = self.uuid
+
+ if self.name is None:
+ self.name = self._meta.model_name
+
+ super(Model, self).__init__()
+
+ def __getattribute__(self, name):
+ opts = object.__getattribute__(self, '_meta')
+ _cls = opts._fields_map.get(name, None)
+ if _cls is not None and hasattr(_cls, 'to_python'):
+ return _cls.to_python(object.__getattribute__(self, name))
+
+ return object.__getattribute__(self, name)
+
+ def _get_init_value(self, field, values, name):
+ attrname = spinal_to_camelcase(name)
+ return values.get(attrname, field.get_default())
+
+ @property
+ def uuid(self):
+ if not self._uuid and self.parent:
+ return self.parent.uuid
+
+ return self._uuid
+
+ @uuid.setter
+ def uuid(self, val):
+ if not self._uuid and self.parent:
+ self.parent.uuid = val
+ else:
+ self._uuid = val
+
+ @property
+ def association_ids(self):
+ if not self._association_ids and self.parent:
+ return self.parent.association_ids
+
+ return self._association_ids
+
+ @association_ids.setter
+ def association_ids(self, val):
+ if not self._association_ids and self.parent:
+ self.parent.association_ids = val
+ else:
+ self._association_ids = val
+
+ def to_dict(self):
+ dict_obj = {}
+ for attrname, value in six.iteritems(self._meta.__dict__):
+ if not attrname.startswith('_'):
+ dict_obj[attrname] = value
+
+ dict_obj['_links'] = {}
+ for attrname, value in six.iteritems(self._meta._links.__dict__):
+ dict_obj['_links'][attrname] = value
+
+ dict_obj['value'] = {}
+ for field in self._meta._fields:
+ dict_obj['value'][field.attname] = getattr(self, field.attname)
+
+ dict_obj.pop('model_manager', None)
+ return dict_obj
+
+ def to_body_dict(self):
+ from designsafe.apps.api.agave.models.fields import ListField
+ dict_obj = {}
+
+ if not self._is_nested:
+ for attrname in self._meta._schema_fields:
+ value = getattr(self, attrname, None)
+ if not inspect.isclass(value):
+ dict_obj[spinal_to_camelcase(attrname)] = value
+ dict_obj['associationIds'] = list(set(self.association_ids))
+
+ dict_obj['_links'] = {}
+ for attrname, value in six.iteritems(self._links.__dict__):
+ dict_obj['_links'][spinal_to_camelcase(attrname)] = value
+
+ #dict_obj['value'] = {}
+ value_dict = {}
+ for field in self._meta._fields:
+ value = getattr(self, field.attname)
+ attrname = spinal_to_camelcase(field.attname)
+ if isinstance(value, RelatedQuery):
+ value_dict[attrname] = list(set(value.uuids))
+ elif isinstance(value, Model):
+ value_dict[attrname] = value.to_body_dict()
+ elif isinstance(field, ListField) and field.list_cls is not None:
+ value_dict[attrname] = [o.to_body_dict() for o in set(value)]
+ elif isinstance(field, ListField):
+ value_dict[attrname] = list(set(value))
+ else:
+ value_dict[attrname] = value
+ if not self._is_nested:
+ dict_obj['value'] = value_dict
+ else:
+ dict_obj = value_dict
+
+ dict_obj.pop('modelManager', None)
+ dict_obj.pop('modelName', None)
+ if ('created' in dict_obj and isinstance(dict_obj['created'], datetime.datetime)):
+ dict_obj['created'] = dict_obj['created'].isoformat()
+
+ if ('lastUpdated' in dict_obj and isinstance(dict_obj['lastUpdated'], datetime.datetime)):
+ dict_obj['lastUpdated'] = dict_obj['lastUpdated'].isoformat()
+
+ return dict_obj
+
+ def save(self, agave_client):
+ if self.parent:
+ self.parent.save(agave_client)
+
+ body = self.to_body_dict()
+ body.pop('_relatedFields', None)
+ if self.uuid is None:
+ logger.debug('Adding Metadata: %s, with: %s', self.name, body)
+ ret = agave_client.meta.addMetadata(body=body)
+ else:
+ logger.debug('Updating Metadata: %s, with: %s', self.uuid, body)
+ ret = agave_client.meta.updateMetadata(uuid=self.uuid, body=body)
+ return ret
+
+ def associate(self, value):
+ _aids = self.association_ids[:]
+ if isinstance(value, basestring):
+ _aids.append(value)
+ else:
+ _aids += value
+
+ self.association_ids = list(set(_aids))
+ return self.association_ids
+
+ @property
+ def manager(self):
+ return self._meta.model_manager
diff --git a/designsafe/apps/api/agave/models/fields.py b/designsafe/apps/api/agave/models/fields.py
new file mode 100644
index 0000000000..eaecb9a3b9
--- /dev/null
+++ b/designsafe/apps/api/agave/models/fields.py
@@ -0,0 +1,149 @@
+""" Base field classes """
+import six
+import datetime
+import dateutil.parser
+import collections
+from decimal import Decimal
+from designsafe.apps.api.agave.models.base import register_lazy_rel
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+class NOT_PROVIDED(object):
+ pass
+
+class BaseField(object):
+ """ Base field class """
+ related_model = None
+
+ def __init__(self, verbose_name=None, name=None,
+ max_length=None, blank=False, null=False,
+ related=None, default=None, choices=None,
+ help_text='', validators=(), error_messages=None,
+ nested_cls=None, related_name=None, list_cls=None):
+ self.verbose_name = verbose_name
+ self.name = name
+ self.max_length = max_length
+ self.blank = blank
+ self.null = null
+ self.related = related
+ self.default = default
+ self.choices = choices
+ self.help_text = help_text
+ self.validators = validators
+ self.error_messages = error_messages
+ self.nested_cls = nested_cls
+ self.attname = None
+ self.related_name = related_name
+ self.list_cls = list_cls
+
+ if not isinstance(self.choices, collections.Iterator):
+ self.choices = []
+ else:
+ self.choices = list( self.choices)
+
+ if not self.name and self.verbose_name:
+ self.name = self.verbose_name.lower()
+ self.name = self.name.replace(' ', '_')
+
+ self.choices = self.choices or []
+
+ def contribute_to_class(self, cls, name):
+ """ Register the field with the model class it belongs to. """
+ if not self.name:
+ self.name = name
+
+ self.attname = name
+ if self.verbose_name is None:
+ self.verbose_name = name.replace('_', ' ')
+ self.model = cls
+ cls._meta.add_field(self)
+
+ def get_default(self):
+ if not isinstance(self.default, NOT_PROVIDED):
+ return self.default
+ else:
+ raise ValueError('No default set')
+
+ def to_python(self, value):
+ return value
+
+ def clean(self, value):
+ return self.to_python(value)
+
+class CharField(BaseField):
+ """ Char Field """
+ def __init__(self, *args, **kwargs):
+ super(CharField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ return unicode(value)
+
+class UuidField(CharField):
+ """ Uuid Field """
+ def __init__(self, *args, **kwargs):
+ kwargs['schema_field'] = True
+ super(UuidField, self).__init__(*args, **kwargs)
+
+class DateTimeField(BaseField):
+ """ Date Time Field """
+ def __init__(self, *args, **kwargs):
+ super(DateTimeField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ if not isinstance(value, datetime):
+ return dateutil.parser.parse(value)
+
+ return value
+
+class IntField(BaseField):
+ """ Int Field """
+ def __init__(self, *args, **kwargs):
+ super(IntField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ return int(value)
+
+class DecimalField(BaseField):
+ """ Decimal Field """
+ def __init__(self, *args, **kwargs):
+ super(DecimalField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ return Decimal(value)
+
+class ListField(BaseField):
+ """ List Field """
+ def __init__(self, verbose_name, list_cls=None, *args, **kwargs):
+ kwargs['default'] = kwargs.get('default', [])
+ kwargs['list_cls'] = list_cls
+ kwargs['verbose_name'] = verbose_name
+ super(ListField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ if self.list_cls is not None:
+ return [self.list_cls(**val) for val in value]
+
+ return list(value)
+
+class NestedObjectField(BaseField):
+ """ Nested Object Field """
+ def __init__(self, nested_cls, *args, **kwargs):
+ kwargs['nested_cls'] = nested_cls
+ kwargs['default'] = kwargs.get('default', {})
+ super(NestedObjectField, self).__init__(*args, **kwargs)
+
+class RelatedObjectField(BaseField):
+ """ Related Object Field """
+ def __init__(self, related, multiple=False, related_name=None, *args, **kwargs):
+ kwargs['related_name'] = related_name
+ kwargs['related'] = related
+ super(RelatedObjectField, self).__init__(*args, **kwargs)
+ self.multiple = multiple
+
+ def contribute_to_class(self, cls, name):
+ """ Register the field with the model class it belongs to. """
+ super(RelatedObjectField, self).contribute_to_class(cls, name)
+ related_name = self.related_name or '%s_set' % cls.__name__.lower()
+ register_lazy_rel(self.related, related_name, cls.model_name, self.multiple, cls)
diff --git a/designsafe/apps/api/agave/models/files.py b/designsafe/apps/api/agave/models/files.py
index 13766c16fa..b1e809d7ef 100644
--- a/designsafe/apps/api/agave/models/files.py
+++ b/designsafe/apps/api/agave/models/files.py
@@ -31,14 +31,14 @@ def __init__(self, agave_client, file_obj=None, **kwargs):
if not meta_objs:
defaults = kwargs
defaults['name'] = 'designsafe.file'
- defaults['value'] = {'fileUUID': file_obj.uuid,
+ defaults['value'] = {'fileUuid': file_obj.uuid,
'keywords': []}
defaults['associationIds'] = [file_obj.uuid]
project_uuid = kwargs.get('project_uuid')
if re.search(r'^project-', file_obj.system) or project_uuid:
project_uuid = project_uuid or file_obj.system.replace('project-', '', 1)
- defaults['value'].update({'projectUUID': project_uuid})
+ defaults['value'].update({'projectUuid': project_uuid})
defaults['associationIds'].append(project_uuid)
else:
defaults = kwargs
@@ -98,7 +98,7 @@ def _update_pems_with_system_roles(self, system_roles, meta_pems):
return meta_pems_users
def match_pems_to_project(self, project_uuid = None):
- project_uuid = project_uuid or self.value.get('projectUUID')
+ project_uuid = project_uuid or self.value.get('projectUUID', self.value.get('projectUuid'))
logger.debug('matchins pems to project: %s', project_uuid)
if not project_uuid:
return self
@@ -117,7 +117,7 @@ def save(self):
super(BaseFileMetadata, self).save()
#self.match_pems_to_project()
if self.value.get('projectUUID'):
- tasks.check_project_meta_pems.apply_async(args=[self.uuid])
+ tasks.check_project_meta_pems.apply_async(args=[self.uuid], queue='api')
else:
super(BaseFileMetadata, self).save()
diff --git a/designsafe/apps/api/agave/views.py b/designsafe/apps/api/agave/views.py
index 9c23628324..9a4ac4f318 100644
--- a/designsafe/apps/api/agave/views.py
+++ b/designsafe/apps/api/agave/views.py
@@ -250,7 +250,8 @@ def put(self, request, file_mgr_name, system_id, file_path):
'dest_resource': external,
'src_file_id': os.path.join(system_id, file_path.strip('/')),
'dest_file_id': body.get('path')
- })
+ },
+ queue='files')
event_data[Notification.MESSAGE] = 'Data copy has been scheduled. This will take a few minutes.'
event_data[Notification.EXTRA] = {
'resource': external,
@@ -414,7 +415,8 @@ def put(self, request, file_mgr_name, system_id, file_path):
if file_listing.previewable:
preview_url = reverse('designsafe_api:files_media',
args=[file_mgr_name, system_id, file_path])
- return JsonResponse({'href': '{}?preview=true'.format(preview_url)})
+ return JsonResponse({'href': '{}?preview=true'.format(preview_url),
+ 'postit': file_listing.download_postit(force=False, lifetime=360)})
else:
return HttpResponseBadRequest('Preview not available for this item')
except HTTPError as e:
@@ -611,7 +613,7 @@ def post(self, request, file_mgr_name, system_id, file_path):
Notification.OPERATION: 'data_depot_share',
Notification.STATUS: Notification.SUCCESS,
Notification.USER: request.user.username,
- Notification.MESSAGE: 'Permissions for a file/folder has been updated.',
+ Notification.MESSAGE: 'Permissions for a file/folder is being updated.',
Notification.EXTRA: {'system': system_id,
'path': file_path,
'username': username,
@@ -629,7 +631,7 @@ def post(self, request, file_mgr_name, system_id, file_path):
Notification.EXTRA: {'message': err.response.text}
}
Notification.objects.create(**event_data)
- return HttpResponseBadRequest(e.response.text)
+ return HttpResponseBadRequest(err.response.text)
return JsonResponse(pem, encoder=AgaveJSONEncoder, safe=False)
return HttpResponseBadRequest("Unsupported operation")
diff --git a/designsafe/apps/api/data/agave/filemanager.py b/designsafe/apps/api/data/agave/filemanager.py
index d72adab2db..7007856bf9 100644
--- a/designsafe/apps/api/data/agave/filemanager.py
+++ b/designsafe/apps/api/data/agave/filemanager.py
@@ -295,7 +295,8 @@ def listing(self, file_id=None, **kwargs):
listing = self._agave_listing(system, file_path, **kwargs)
reindex_agave.apply_async(kwargs = {'username': self.username,
'file_id': file_id,
- 'levels': 1})
+ 'levels': 1},
+ queue='indexing')
except IndexError:
listing = es_listing
return listing
@@ -724,7 +725,8 @@ def share(self, file_id, permissions, recursive = True, **kwargs):
#esf = Object.from_file_path(system, self.username, file_path)
#esf.share(self.username, permissions)
share_agave.apply_async(args=(self.username, file_id, permissions,
- recursive))
+ recursive),
+ queue='indexing')
return f.to_dict()
def transfer(self, file_id, dest_resource, dest_file_id):
@@ -735,7 +737,8 @@ def transfer(self, file_id, dest_resource, dest_file_id):
self.resource,
file_id,
dest_resource,
- dest_file_id))
+ dest_file_id),
+ queue='indexing')
return {'message': 'The requested transfer has been scheduled'}
else:
message = 'The requested transfer from %s to %s ' \
@@ -803,7 +806,8 @@ def upload(self, file_id, files, **kwargs):
'file_id': file_id,
'full_indexing': False,
'pems_indexing': True,
- 'index_full_path': True})
+ 'index_full_path': True},
+ queue='indexing')
return u_file.to_dict()
def from_file_real_path(self, file_real_path):
diff --git a/designsafe/apps/api/data/agave/public_filemanager.py b/designsafe/apps/api/data/agave/public_filemanager.py
index 0b5fd8e0a4..9f01829cd6 100644
--- a/designsafe/apps/api/data/agave/public_filemanager.py
+++ b/designsafe/apps/api/data/agave/public_filemanager.py
@@ -273,7 +273,7 @@ def copy(self, file_id, dest_resource, dest_file_id, **kwargs):
service = lookup_transfer_service(self.resource, dest_resource)
if service:
args = (self.username, self.resource, file_id, dest_resource, dest_file_id)
- service.apply_async(args=args)
+ service.apply_async(args=args, queue='files')
return {'message': 'The requested transfer has been scheduled'}
else:
message = 'The requested transfer from %s to %s ' \
diff --git a/designsafe/apps/api/data/agave/tests/test_filemanager.py b/designsafe/apps/api/data/agave/tests/test_filemanager.py
index eaa705c191..4a29e13cb3 100644
--- a/designsafe/apps/api/data/agave/tests/test_filemanager.py
+++ b/designsafe/apps/api/data/agave/tests/test_filemanager.py
@@ -713,5 +713,5 @@ def test_rename(self, mock_agave_from_file_path, mock_share_task):
self.user.username, path,
agave_client=fm.agave_client)
mock_share_task.assert_called_with(
- args=(self.user.username, file_id, permissions, True))
+ args=(self.user.username, file_id, permissions, True), queue='indexing')
diff --git a/designsafe/apps/api/data/box/filemanager.py b/designsafe/apps/api/data/box/filemanager.py
index d2e6b1a1dc..f7ee0b5849 100644
--- a/designsafe/apps/api/data/box/filemanager.py
+++ b/designsafe/apps/api/data/box/filemanager.py
@@ -167,7 +167,7 @@ def copy(self, file_id, dest_resource, dest_file_id, **kwargs):
args = (self._user.username,
self.resource, file_id,
dest_resource, dest_file_id)
- service.apply_async(args=args)
+ service.apply_async(args=args, queue='files')
return {'message': 'The requested transfer has been scheduled'}
else:
message = 'The requested transfer from %s to %s ' \
@@ -201,7 +201,8 @@ def import_file(self, file_id, from_resource, import_file_id, **kwargs):
file_id,
from_resource,
import_file_id),
- countdown=10)
+ countdown=10,
+ queue='files')
return {'message': 'Your file(s) have been scheduled for upload to box.'}
diff --git a/designsafe/apps/api/data/views.py b/designsafe/apps/api/data/views.py
index 3549d428af..705f8a52c1 100644
--- a/designsafe/apps/api/data/views.py
+++ b/designsafe/apps/api/data/views.py
@@ -190,8 +190,10 @@ def get(self, request, pk, *args, **kwargs):
logger.info('extra: {}'.format(extra))
try:
- target_path = extra['target_path']
+ # target_path = extra['target_path']
+ file_id = '%s%s' % (extra['system'], extra['trail'][-2]['path']) #path of the containing folder
except KeyError as e:
file_id = extra['id']
- target_path = reverse('designsafe_data:data_depot') + 'agave/' + file_id + '/'
+
+ target_path = reverse('designsafe_data:data_depot') + 'agave/' + file_id + '/'
return redirect(target_path)
diff --git a/designsafe/apps/api/decorators.py b/designsafe/apps/api/decorators.py
new file mode 100644
index 0000000000..4d79995349
--- /dev/null
+++ b/designsafe/apps/api/decorators.py
@@ -0,0 +1,80 @@
+""" Decorators used for the api TODO: This is Django specific. We should either move this into some kind of
+ django specific utils or try and make it as general as possible.
+"""
+import logging
+from functools import wraps
+from base64 import b64decode
+from django.utils.six import text_type
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth import login
+import jwt as pyjwt
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.serialization import load_der_public_key
+
+#pylint: disable=invalid-name
+logger = logging.getLogger(__name__)
+#pylint: enable=invalid-name
+
+def _decode_jwt(jwt):
+ """Verified signature on a jwt
+
+ Uses public key to decode the jwt message.
+
+ :param str jwt: JWT string
+ :return: base64-decoded message
+ """
+ #pubkey = settings.AGAVE_JWT_PUBKEY
+ pubkey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUp/oV1vWc8/TkQSiAvTousMzO\nM4asB2iltr2QKozni5aVFu818MpOLZIr8LMnTzWllJvvaA5RAAdpbECb+48FjbBe\n0hseUdN5HpwvnH/DW8ZccGvk53I6Orq7hLCv1ZHtuOCokghz/ATrhyPq+QktMfXn\nRS4HrKGJTzxaCcU7OQIDAQAB'
+ key_der = b64decode(pubkey)
+ key = load_der_public_key(key_der, backend=default_backend())
+ return pyjwt.decode(jwt, key, issuer=settings.AGAVE_JWT_ISSUER)
+ #return pyjwt.decode(jwt, key, verify=False)
+
+def _get_jwt_payload(request):
+ """Return JWT payload as a string
+
+ :param django.http.request request: Django Request
+ :return: JWT payload
+ :rtype: str
+ """
+ payload = request.META.get(settings.AGAVE_JWT_HEADER)
+ if payload and isinstance(payload, text_type):
+ # Header encoding (see RFC5987)
+ payload = payload.encode('iso-8859-1')
+
+ return payload
+
+def agave_jwt_login(func):
+ """Decorator to login user with a jwt
+
+ ..note::
+ It will sliently fail and continue executing the wrapped function
+ if the JWT payload header IS NOT present in the request. If the JWT payload
+ header IS present then it will continue executing the wrapped function passing
+ the request object with the correct user logged-in.
+ Because of this it is assumed that this decorator will be used together with
+ :func:`django.contrib.auth.decorators.login_required` decorator. This way we do
+ not disrupt your usual Django login config.
+ """
+ #pylint: disable=missing-docstring
+ @wraps(func)
+ def decorated_function(request, *args, **kwargs):
+ if request.user.is_authenticated():
+ return func(request, *args, **kwargs)
+
+ payload = _get_jwt_payload(request)
+ if not payload:
+ logger.debug('No JWT payload found. Falling back')
+ return func(request, *args, **kwargs)
+
+ jwt_payload = _decode_jwt(payload)
+ username = jwt_payload.get(settings.AGAVE_JWT_USER_CLAIM_FIELD, '')
+ user = get_user_model().objects.get(username=username)
+ user.backend = 'django.contrib.auth.backends.ModelBackend',
+ login(request, user)
+ return func(request, *args, **kwargs)
+
+ return decorated_function
+ #pylint: enable=missing-docstring
diff --git a/designsafe/apps/api/external_resources/box/filemanager/manager.py b/designsafe/apps/api/external_resources/box/filemanager/manager.py
index 2349ee9719..40257b57e3 100644
--- a/designsafe/apps/api/external_resources/box/filemanager/manager.py
+++ b/designsafe/apps/api/external_resources/box/filemanager/manager.py
@@ -31,8 +31,7 @@ def __init__(self, user_obj, **kwargs):
try:
self.box_api = user_obj.box_user_token.client
except BoxUserToken.DoesNotExist:
- message = 'You need to connect your Box.com account ' \
- 'before you can access your Box.com files.'
+ message = 'Connect your Box account here'
raise ApiException(status=400, message=message, extra={
'action_url': reverse('box_integration:index'),
'action_label': 'Connect Box.com Account'
@@ -220,7 +219,8 @@ def copy(self, username, src_file_id, dest_file_id, **kwargs):
reindex_agave.apply_async(kwargs={
'username': user.username,
'file_id': '{}/{}'.format(agave_system_id, agave_file_path)
- })
+ },
+ queue='indexing')
except:
logger.exception('Unexpected task failure: box_download', extra={
'username': username,
diff --git a/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py b/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py
index 7a05d98b97..b1a1a8adcb 100644
--- a/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py
+++ b/designsafe/apps/api/external_resources/dropbox/filemanager/manager.py
@@ -38,8 +38,7 @@ def __init__(self, user_obj, **kwargs):
self.dropbox_api = Dropbox(dropbox_token.access_token)
except DropboxUserToken.DoesNotExist:
- message = 'You need to connect your Dropbox.com account ' \
- 'before you can access your Dropbox.com files.'
+ message = 'Connect your Dropbox account here'
raise ApiException(status=400, message=message, extra={
'action_url': reverse('dropbox_integration:index'),
'action_label': 'Connect Dropbox.com Account'
@@ -95,7 +94,7 @@ def listing(self, file_id='', **kwargs):
entries = dropbox_item.entries
while True:
- children.extend([DropboxFile(item, item.path_display, parent=dropbox_item).to_dict(default_pems=default_pems)
+ children.extend([DropboxFile(item, item.path_display.encode('utf-8'), parent=dropbox_item).to_dict(default_pems=default_pems)
for item in entries])
if has_more:
folder = self.dropbox_api.files_list_folder_continue(cursor)
@@ -202,7 +201,8 @@ def copy(self, username, src_file_id, dest_file_id, **kwargs):
reindex_agave.apply_async(kwargs={
'username': user.username,
'file_id': '{}/{}'.format(agave_system_id, agave_file_path)
- })
+ },
+ queue='indexing')
except:
logger.exception('Unexpected task failure: dropbox_download', extra={
'username': username,
diff --git a/designsafe/apps/api/external_resources/views.py b/designsafe/apps/api/external_resources/views.py
index 077db08c39..86d8491f53 100644
--- a/designsafe/apps/api/external_resources/views.py
+++ b/designsafe/apps/api/external_resources/views.py
@@ -22,7 +22,7 @@
logger = logging.getLogger(__name__)
-class FilesListView(BaseApiView, SecureMixin):
+class FilesListView(SecureMixin, BaseApiView):
"""Listing view"""
def get(self, request, file_mgr_name, file_id=None):
@@ -37,7 +37,7 @@ def get(self, request, file_mgr_name, file_id=None):
listing = fmgr.listing(file_id)
return JsonResponse(listing, safe=False)
-class FileMediaView(BaseApiView, SecureMixin):
+class FileMediaView(SecureMixin, BaseApiView):
"""File Media View"""
def get(self, request, file_mgr_name, file_id):
@@ -91,7 +91,8 @@ def put(self, request, file_mgr_name, file_id):
'file_mgr_name': file_mgr_name,
'username': request.user.username,
'src_file_id': file_id,
- 'dest_file_id': os.path.join(body['system'], body['path'].strip('/'))})
+ 'dest_file_id': os.path.join(body['system'], body['path'].strip('/'))},
+ queue='files')
return JsonResponse({'status': 200, 'message': 'OK'})
except HTTPError as e:
logger.exception('Unable to copy file')
@@ -110,6 +111,6 @@ def put(self, request, file_mgr_name, file_id):
return HttpResponseBadRequest("Operation not implemented.")
-class FilePermissionsView(BaseApiView, SecureMixin):
+class FilePermissionsView(SecureMixin, BaseApiView):
"""File Permissions View"""
pass
diff --git a/designsafe/apps/api/fixtures/agave-experiment-meta.json b/designsafe/apps/api/fixtures/agave-experiment-meta.json
new file mode 100644
index 0000000000..d46d2019a8
--- /dev/null
+++ b/designsafe/apps/api/fixtures/agave-experiment-meta.json
@@ -0,0 +1,26 @@
+{
+ "uuid" : "9620095064311852570-242ac11e-0001-012",
+ "schemaId" : null,
+ "internalUsername" : null,
+ "associationIds" : [ ],
+ "lastUpdated" : "2017-01-26T09:48:46.760-06:00",
+ "name" : "designsafe.project",
+ "value" : {
+ "title": "experiment title",
+ "experimentType": "other"
+ },
+ "created" : "2016-10-30T15:57:58.970-05:00",
+ "owner" : "ds_admin",
+ "_links" : {
+ "self" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012"
+ },
+ "permissions" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems"
+ },
+ "owner" : {
+ "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin"
+ },
+ "associationIds" : [ ]
+ }
+}
diff --git a/designsafe/apps/api/fixtures/agave-file-meta.json b/designsafe/apps/api/fixtures/agave-file-meta.json
new file mode 100644
index 0000000000..e31bfa129d
--- /dev/null
+++ b/designsafe/apps/api/fixtures/agave-file-meta.json
@@ -0,0 +1,26 @@
+{
+ "uuid" : "8510095064311852681-242ac11e-0001-012",
+ "schemaId" : null,
+ "internalUsername" : null,
+ "associationIds" : [ ],
+ "lastUpdated" : "2017-01-26T09:48:46.760-06:00",
+ "name" : "designsafe.project",
+ "value" : {
+ "keywords": ["one", "two", "three", "four"],
+ "projectUUID": "8510095064311852570-242ac11e-0001-012"
+ },
+ "created" : "2016-10-30T15:57:58.970-05:00",
+ "owner" : "ds_admin",
+ "_links" : {
+ "self" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012"
+ },
+ "permissions" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems"
+ },
+ "owner" : {
+ "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin"
+ },
+ "associationIds" : [ ]
+ }
+}
diff --git a/designsafe/apps/api/fixtures/agave-model-config-meta.json b/designsafe/apps/api/fixtures/agave-model-config-meta.json
new file mode 100644
index 0000000000..46a3359cdd
--- /dev/null
+++ b/designsafe/apps/api/fixtures/agave-model-config-meta.json
@@ -0,0 +1,28 @@
+{
+ "uuid" : "8510095064311852681-242ac11e-0001-015",
+ "schemaId" : null,
+ "internalUsername" : null,
+ "associationIds" : [ ],
+ "lastUpdated" : "2017-01-26T09:48:46.760-06:00",
+ "name" : "designsafe.project",
+ "value" : {
+ "title": "Model config title",
+ "description": "description",
+ "coverage": "coverage",
+ "files": ["adsfasfd", "qewrqwerqwe", "zxczxcvzvxc"]
+ },
+ "created" : "2016-10-30T15:57:58.970-05:00",
+ "owner" : "ds_admin",
+ "_links" : {
+ "self" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012"
+ },
+ "permissions" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems"
+ },
+ "owner" : {
+ "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin"
+ },
+ "associationIds" : [ ]
+ }
+}
diff --git a/designsafe/apps/api/fixtures/agave-project-meta.json b/designsafe/apps/api/fixtures/agave-project-meta.json
new file mode 100644
index 0000000000..0627e1b634
--- /dev/null
+++ b/designsafe/apps/api/fixtures/agave-project-meta.json
@@ -0,0 +1,32 @@
+{
+ "uuid" : "8511125064311852570-242ac11e-0001-012",
+ "schemaId" : null,
+ "internalUsername" : null,
+ "associationIds" : [ ],
+ "lastUpdated" : "2017-01-26T09:48:46.760-06:00",
+ "name" : "designsafe.project",
+ "value" : {
+ "teamMember" : [ "jcoronel", "jcoronel", "jcoronel" ],
+ "projectType": "experimental",
+ "description": "description",
+ "pi" : "erathje",
+ "title" : "Projekt Drei",
+ "awardNumber": "NSF-04897",
+ "associatedProjects": ["asdfasdf", "qewrqwerqwerqew", "zcvzxcvzxcvzxcv"],
+ "ef": "Experimental Facility"
+ },
+ "created" : "2016-10-30T15:57:58.970-05:00",
+ "owner" : "ds_admin",
+ "_links" : {
+ "self" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012"
+ },
+ "permissions" : {
+ "href" : "https://agave.designsafe-ci.org/meta/v2/data/8510095064311852570-242ac11e-0001-012/pems"
+ },
+ "owner" : {
+ "href" : "https://agave.designsafe-ci.org/profiles/v2/ds_admin"
+ },
+ "associationIds" : [ ]
+ }
+}
diff --git a/designsafe/apps/api/mixins.py b/designsafe/apps/api/mixins.py
index 6d8c4a7695..f6225e3270 100644
--- a/designsafe/apps/api/mixins.py
+++ b/designsafe/apps/api/mixins.py
@@ -1,5 +1,6 @@
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
+from designsafe.apps.api.decorators import agave_jwt_login
from django.http import HttpResponse
from django.core.serializers.json import DjangoJSONEncoder
import logging
@@ -20,12 +21,18 @@ def render_to_json_response(self, context, **response_kwargs):
class SecureMixin(object):
- """
- View mixin to use login_required
+ """View mixin to ensure the user has access to a secured view
+
+ This Mixin first checks if the request is done using a JWT. If this is not the case
+ then it will continue and check if the request is using a regular django session cookie.
+ Either way the request will be correctly authenticated. This way we can easily
+ use the same API endpoints and put them behind WSO2.
+
TODO: When moving into Django 1.9 @method_decorator(login_required)
should be a class wrapper @method_decorator(login_required, name='dispatch')
as per: https://docs.djangoproject.com/en/1.9/topics/class-based-views/intro/#decorating-the-class
"""
+ @method_decorator(agave_jwt_login)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(SecureMixin, self).dispatch(request, *args, **kwargs)
diff --git a/designsafe/apps/api/models.py b/designsafe/apps/api/models.py
index 71a8362390..a9c3dc5038 100644
--- a/designsafe/apps/api/models.py
+++ b/designsafe/apps/api/models.py
@@ -1,3 +1,8 @@
from django.db import models
# Create your models here.
+from designsafe.apps.api.agave.models.fields import *
+from designsafe.apps.api.projects.models import *
+
+from designsafe.apps.api.agave.models.base import set_lazy_rels
+set_lazy_rels()
diff --git a/designsafe/apps/api/notifications/views/api.py b/designsafe/apps/api/notifications/views/api.py
index 18c2193037..2e96abc9ae 100644
--- a/designsafe/apps/api/notifications/views/api.py
+++ b/designsafe/apps/api/notifications/views/api.py
@@ -18,22 +18,34 @@
class ManageNotificationsView(SecureMixin, JSONResponseMixin, BaseApiView):
def get(self, request, event_type = None, *args, **kwargs):
- limit = request.GET.get('limit', None)
+ limit = request.GET.get('limit', 0)
+ page = request.GET.get('page', 0)
+
if event_type is not None:
notifs = Notification.objects.filter(event_type = event_type,
deleted = False,
user = request.user.username).order_by('-datetime')
+ total = Notification.objects.filter(event_type = event_type,
+ deleted = False,
+ user = request.user.username).count()
else:
notifs = Notification.objects.filter(deleted = False,
user = request.user.username).order_by('-datetime')
+ total = Notification.objects.filter(deleted = False,
+ user = request.user.username).count()
if limit:
- notifs = notifs[0:limit]
+ limit = int(limit)
+ page = int(page)
+ offset = page * limit
+ notifs = notifs[offset:offset+limit]
+
for n in notifs:
if not n.read:
n.mark_read()
notifs = [n.to_dict() for n in notifs]
- return self.render_to_json_response(notifs)
+ return self.render_to_json_response({'notifs':notifs, 'page':page, 'total': total})
+ # return self.render_to_json_response(notifs)
def post(self, request, *args, **kwargs):
body_json = json.loads(request.body)
diff --git a/designsafe/apps/api/projects/managers/__init__.py b/designsafe/apps/api/projects/managers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/designsafe/apps/api/projects/managers/publication.py b/designsafe/apps/api/projects/managers/publication.py
new file mode 100644
index 0000000000..8f81fa742e
--- /dev/null
+++ b/designsafe/apps/api/projects/managers/publication.py
@@ -0,0 +1,386 @@
+import re
+import logging
+import codecs
+import xml.etree.ElementTree as ET
+from xml.dom import minidom
+from django.contrib.auth import get_user_model
+from django.conf import settings
+import datetime
+import dateutil.parser
+import requests
+
+logger = logging.getLogger(__name__)
+
+USER = settings.EZID_USER
+PASSWORD = settings.EZID_PASS
+CREDS = (USER, PASSWORD)
+BASE_URI = 'https://ezid.cdlib.org/'
+SHOULDER = 'doi:10.5072/FK2'
+
+def pretty_print(xml):
+ """Return a pretty-printed XML string for the Element.
+ """
+ rough_string = ET.tostring(xml, 'utf-8')
+ reparsed = minidom.parseString(rough_string)
+ return reparsed.toprettyxml(indent=" ")
+
+def _project_header():
+ xml_obj = ET.Element("resource")
+ xml_obj.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
+ xml_obj.attrib["xmlns"] = "http://datacite.org/schema/kernel-4"
+ xml_obj.attrib["xsi:schemaLocation"] = "http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"
+ return xml_obj
+
+def _unescape(s):
+ return re.sub("%([0-9A-Fa-f][0-9A-Fa-f])",
+ lambda m: chr(int(m.group(1), 16)), s)
+
+def parse_response(res):
+ response = dict(tuple(_unescape(v).strip() for v in l.split(":", 1)) \
+ for l in res.decode("UTF-8").splitlines())
+ return response
+
+def _escape(s, key=True):
+ if key:
+ return re.sub("[%:\r\n]", lambda c: "%%%02X" % ord(c.group(0)), s)
+ else:
+ return re.sub("[%\r\n]", lambda c: "%%%02X" % ord(c.group(0)), s)
+
+def format_req(metadata):
+ anvl = []
+ for key, value in metadata.items():
+ key = _escape(key)
+ if value.startswith("@") and len(value) > 1:
+ f = codecs.open(value[1:], encoding="UTF-8")
+ value = f.read()
+ f.close()
+ value = _escape(value, False)
+ anvl.append("%s: %s" % (key, value))
+ return "\n".join(anvl)
+
+def _reserve_doi(xml_obj):
+ xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml")
+ metadata = {'_status': 'reserved', 'datacite': xml_str}
+ response = requests.post('{}/shoulder/{}'.format(BASE_URI, SHOULDER),
+ data=format_req(metadata),
+ auth=CREDS,
+ headers={'Content-Type': 'text/plain'})
+ res = parse_response(response.text)
+ if 'success' in res:
+ return res['success']
+ else:
+ raise Exception(res['error'])
+
+def _update_doi(doi, xml_obj=None, status='reserved'):
+ if xml_obj is not None:
+ xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml")
+ metadata = {'_status': status, 'datacite': xml_str}
+ else:
+ metadata = {'_status': status}
+
+ response = requests.post('{}/id/{}'.format(BASE_URI, doi),
+ data=format_req(metadata),
+ auth=CREDS,
+ headers={'Content-Type': 'text/plain'})
+ res = parse_response(response.text)
+ if 'success' in res:
+ return res['success']
+ else:
+ raise Exception(res['error'])
+
+def _project_required_xml(publication):
+ project_body = publication['project']
+ proj = project_body['value']
+ xml_obj = _project_header()
+
+ resource = xml_obj
+ identifier = ET.SubElement(resource, 'identifier')
+ identifier.attrib['identifierType'] = 'DOI'
+ identifier.text = SHOULDER.replace('doi:', '')
+ creators = ET.SubElement(resource, 'creators')
+ um = get_user_model()
+ for author in [proj['pi']] + proj['coPis'] + proj['teamMembers']:
+ try:
+ user = um.objects.get(username=author)
+ creator = ET.SubElement(creators, 'creator')
+ creator_name = ET.SubElement(creator, 'creatorName')
+ creator_name.text = '{}, {}'.format(user.last_name, user.first_name)
+ except um.DoesNotExist as err:
+ logger.debug('User not found: %s', author)
+
+ titles = ET.SubElement(resource, 'titles')
+ title = ET.SubElement(titles, 'title')
+ title.text = proj['title']
+ publisher = ET.SubElement(resource, 'publisher')
+ publisher.text = 'Designsafe-CI'
+
+ now = dateutil.parser.parse(publication['created'])
+ publication_year = ET.SubElement(resource, 'publicationYear')
+ publication_year.text = str(now.year)
+
+ resource_type = ET.SubElement(resource, 'resourceType')
+ resource_type.text = "Project/{}".format(proj['projectType'].title())
+ resource_type.attrib['resourceTypeGeneral'] = 'Dataset'
+ descriptions = ET.SubElement(resource, 'descriptions')
+ desc = ET.SubElement(descriptions, 'description')
+ desc.attrib['descriptionType'] = 'Abstract'
+ desc.text = proj['description']
+ return xml_obj
+
+def _experiment_required_xml(users, experiment, created):
+ exp = experiment['value']
+ xml_obj = _project_header()
+
+ resource = xml_obj
+ identifier = ET.SubElement(resource, 'identifier')
+ identifier.attrib['identifierType'] = 'DOI'
+ identifier.text = SHOULDER.replace('doi:', '')
+ creators = ET.SubElement(resource, 'creators')
+ um = get_user_model()
+ authors = exp.get('authors')
+ authors = authors or users
+ for author in authors:
+ try:
+ if isinstance(author, basestring):
+ user = um.objects.get(username=author)
+ else:
+ user = um.objects.get(username=author['username'])
+
+ creator = ET.SubElement(creators, 'creator')
+ creator_name = ET.SubElement(creator, 'creatorName')
+ creator_name.text = '{}, {}'.format(user.last_name, user.first_name)
+ except um.DoesNotExist as err:
+ logger.debug('User not found: %s', author)
+
+ titles = ET.SubElement(resource, 'titles')
+ title = ET.SubElement(titles, 'title')
+ title.text = exp['title']
+ publisher = ET.SubElement(resource, 'publisher')
+ publisher.text = 'Designsafe-CI'
+
+ now = dateutil.parser.parse(created)
+ publication_year = ET.SubElement(resource, 'publicationYear')
+ publication_year.text = str(now.year)
+
+ resource_type = ET.SubElement(resource, 'resourceType')
+ resource_type.text = "Experiment/{}".format(exp['experimentType'].title())
+ resource_type.attrib['resourceTypeGeneral'] = 'Dataset'
+ descriptions = ET.SubElement(resource, 'descriptions')
+ desc = ET.SubElement(descriptions, 'description')
+ desc.attrib['descriptionType'] = 'Abstract'
+ desc.text = exp['description']
+ return xml_obj
+
+def _analysis_required_xml(users, analysis, created):
+ anl = analysis['value']
+ xml_obj = _project_header()
+
+ resource = xml_obj
+ identifier = ET.SubElement(resource, 'identifier')
+ identifier.attrib['identifierType'] = 'DOI'
+ identifier.text = SHOULDER.replace('doi:', '')
+ creators = ET.SubElement(resource, 'creators')
+ um = get_user_model()
+ for author in users:
+ try:
+ if isinstance(author, basestring):
+ user = um.objects.get(username=author)
+ else:
+ user = um.objects.get(username=author['username'])
+ creator = ET.SubElement(creators, 'creator')
+ creator_name = ET.SubElement(creator, 'creatorName')
+ creator_name.text = '{}, {}'.format(user.last_name, user.first_name)
+ except um.DoesNotExist as err:
+ logger.debug('User not found: %s', author)
+
+ titles = ET.SubElement(resource, 'titles')
+ title = ET.SubElement(titles, 'title')
+ title.text = anl['title']
+ publisher = ET.SubElement(resource, 'publisher')
+ publisher.text = 'Designsafe-CI'
+
+ now = dateutil.parser.parse(created)
+ publication_year = ET.SubElement(resource, 'publicationYear')
+ publication_year.text = str(now.year)
+
+ resource_type = ET.SubElement(resource, 'resourceType')
+ resource_type.text = 'Analysis'
+ resource_type.attrib['resourceTypeGeneral'] = 'Other'
+ descriptions = ET.SubElement(resource, 'descriptions')
+ desc = ET.SubElement(descriptions, 'description')
+ desc.attrib['descriptionType'] = 'Abstract'
+ desc.text = anl['description']
+ return xml_obj
+
+def analysis_reserve_xml(publication, analysis, created):
+ anl = analysis['value']
+ xml_obj = _analysis_required_xml(publication['users'], analysis,
+ created)
+ now = dateutil.parser.parse(created)
+ reserve_res = _reserve_doi(xml_obj)
+ doi, ark = reserve_res.split('|')
+ doi = doi.strip()
+ ark = ark.strip()
+ identifier = xml_obj.find('identifier')
+ identifier.text = doi
+ resource = xml_obj
+ _update_doi(doi, xml_obj)
+ return (doi, ark, xml_obj)
+
+def experiment_reserve_xml(publication, experiment, created):
+ exp = experiment['value']
+ xml_obj = _experiment_required_xml(publication['users'], experiment,
+ created)
+ now = dateutil.parser.parse(created)
+ reserve_res = _reserve_doi(xml_obj)
+ doi, ark = reserve_res.split('|')
+ doi = doi.strip()
+ ark = ark.strip()
+ identifier = xml_obj.find('identifier')
+ identifier.text = doi
+ resource = xml_obj
+ contributors = ET.SubElement(resource, 'contributors')
+ contributor = ET.SubElement(contributors, 'contributor')
+ contributor.attrib['contributorType'] = 'HostingInstitution'
+ name = ET.SubElement(contributor, 'contributorName')
+ name.text = exp['experimentalFacility']
+ subjects = ET.SubElement(resource, 'subjects')
+ exp_type = ET.SubElement(subjects, 'subject')
+ exp_type.text = exp['experimentType'].title()
+ eq_type = ET.SubElement(subjects, 'subject')
+ eq_type.text = exp['equipmentType']
+ events = [event for event in publication['eventsList'] if \
+ experiment['uuid'] in event['associationIds']]
+
+ for event in events:
+ event_subj = ET.SubElement(subjects, 'subject')
+ event_subj.text = event['value']['title']
+
+ mcfs = [mcf for mcf in publication['modelConfigs'] if \
+ experiment['uuid'] in mcf['associationIds']]
+
+ for mcf in mcfs:
+ mcf_subj = ET.SubElement(subjects, 'subject')
+ mcf_subj.text = mcf['value']['title']
+
+ slts = [slt for slt in publication['sensorLists'] if \
+ experiment['uuid'] in slt['associationIds']]
+
+ for slt in slts:
+ slt_subj = ET.SubElement(subjects, 'subject')
+ slt_subj.text = slt['value']['title']
+
+ _update_doi(doi, xml_obj)
+ return (doi, ark, xml_obj)
+
+def project_reserve_xml(publication):
+ project_body = publication['project']
+ proj = project_body['value']
+ xml_obj = _project_required_xml(publication)
+ logger.debug('required xml: %s', pretty_print(xml_obj))
+ now = dateutil.parser.parse(publication['created'])
+ reserve_resp = _reserve_doi(xml_obj)
+ doi, ark = reserve_resp.split('|')
+ doi = doi.strip()
+ ark = ark.strip()
+ #logger.debug('doi: %s', doi)
+ identifier = xml_obj.find('identifier')
+ identifier.text = doi
+
+ #Optional stuff
+ resource = xml_obj
+ subjects = ET.SubElement(resource, 'subjects')
+ for keyword in proj['keywords'].split(','):
+ subject = ET.SubElement(subjects, 'subject')
+ subject.text = keyword.strip().title()
+
+ institutions = publication['institutions']
+ contributors = ET.SubElement(resource, 'contributors')
+ for institution in institutions:
+ contrib = ET.SubElement(contributors, 'contributor')
+ name = ET.SubElement(contrib, 'contributorName')
+ name.text = institution['label']
+ contrib.attrib['contributorType'] = 'HostingInstitution'
+
+ dates = ET.SubElement(resource, 'dates')
+ date_publication = ET.SubElement(dates, 'date')
+ date_publication.attrib['dateType'] = 'Accepted'
+ date_publication.text = '{}-{}-{}'.format(now.year, now.month, now.day)
+
+ language = ET.SubElement(resource, 'language')
+ language.text = 'English'
+
+ alternate_ids = ET.SubElement(resource, 'alternateIdentifiers')
+ if proj['awardNumber']:
+ award_number = ET.SubElement(alternate_ids, 'alternateIdentifier')
+ award_number.attrib['alternateIdentifierType'] = 'NSF Award Number'
+ award_number.text = proj['awardNumber']
+
+ project_id = ET.SubElement(alternate_ids, 'alternateIdentifier')
+ project_id.attrib['alternateIdentifierType'] = 'Project ID'
+ project_id.text = proj['projectId']
+
+ rights_list = ET.SubElement(resource, 'rightsList')
+ rights = ET.SubElement(rights_list, 'rights')
+ rights.attrib['rightsURI'] = 'http://opendatacommons.org/licenses/by/1-0/'
+ rights.text = 'ODC-BY 1.0'
+ logger.debug(pretty_print(xml_obj))
+ _update_doi(doi, xml_obj)
+ return (doi, ark, xml_obj)
+
+def add_related(xml_obj, dois):
+ doi = xml_obj.find('identifier').text
+ resource = xml_obj
+ related_ids = ET.SubElement(resource, 'relatedIdentifiers')
+ for _doi in dois:
+ related = ET.SubElement(related_ids, 'relatedIdentifier')
+ related.attrib['relatedIdentifierType'] = 'DOI'
+ related.attrib['relationType'] = 'IsPartOf'
+ related.text = _doi
+
+ _update_doi(doi, xml_obj)
+ return (doi, xml_obj)
+
+def publish_project(doi, xml_obj):
+ #doi, ark, xml_obj = _project_publish_xml(publication)
+ logger.debug(pretty_print(xml_obj))
+ xml_str = ET.tostring(xml_obj, encoding="UTF-8", method="xml")
+ metadata = {'_status': 'public', 'datacite': xml_str}
+ res = requests.post('{}/id/{}'.format(BASE_URI, doi),
+ format_req(metadata),
+ auth=CREDS,
+ headers={'Content-Type': 'text/plain'})
+ res = parse_response(res.text)
+ if 'success' in res:
+ return res['success'].split('|')
+ else:
+ logger.exception(res['error'])
+ raise Exception(res['error'])
+
+def reserve_publication(publication):
+ proj_doi, proj_ark, proj_xml = project_reserve_xml(publication)
+ exps_dois = []
+ anl_dois = []
+ xmls = {proj_doi: proj_xml}
+ publication['project']['doi'] = proj_doi
+ for exp in publication.get('experimentsList', []):
+ exp_doi, exp_ark, exp_xml = experiment_reserve_xml(publication,
+ exp, publication['created'])
+ add_related(exp_xml, [proj_doi])
+ exps_dois.append(exp_doi)
+ exp['doi'] = exp_doi
+ xmls[exp_doi] = exp_xml
+
+ for anl in publication.get('analysisList', []):
+ anl_doi, anl_ark, anl_xml = analysis_reserve_xml(publication,
+ anl, publication['created'])
+ add_related(anl_xml, [proj_doi])
+ anl_dois.append(anl_doi)
+ anl['doi'] = anl_doi
+ xmls[anl_doi] = anl_xml
+
+ add_related(proj_xml, exps_dois + anl_dois)
+ for _doi in [proj_doi] + exps_dois + anl_dois:
+ logger.debug(_doi)
+ _update_doi(_doi, xmls[_doi], status='public')
+ return publication
diff --git a/designsafe/apps/api/projects/managers/yamz.py b/designsafe/apps/api/projects/managers/yamz.py
new file mode 100644
index 0000000000..a5ee3bdfe0
--- /dev/null
+++ b/designsafe/apps/api/projects/managers/yamz.py
@@ -0,0 +1,30 @@
+import re
+import logging
+import json
+import requests
+from bs4 import BeautifulSoup
+from designsafe.apps.api.mixins import SecureMixin
+from designsafe.apps.api.views import BaseApiView
+from django.http import JsonResponse, HttpResponseBadRequest
+
+logger = logging.getLogger(__name__)
+
+YAMZ_BASE_URL = 'http://www.yamz.net/term/concept='
+
+class YamzBaseView(BaseApiView):
+ def get(self, request, term_id):
+ res = requests.get(''.join([YAMZ_BASE_URL, term_id]))
+ soup = BeautifulSoup(res.text)
+ definition_trs = soup.find_all(string=re.compile(r'^\s*Definition\:\s*$'))
+ if definition_trs:
+ definition = definition_trs[0].findParent('tr').text.encode('utf8').replace('Definition:', '', 1).strip()
+ else:
+ definition = ''
+
+ examples_trs = soup.find_all(string=re.compile(r'^\s*Examples\:\s*$'))
+ if examples_trs:
+ examples = examples_trs[0].findParent('tr').text.encode('utf8').replace('Examples:', '', 1).strip()
+ else:
+ examples = ''
+
+ return JsonResponse({'definition': definition, 'examples': examples})
diff --git a/designsafe/apps/api/projects/models.py b/designsafe/apps/api/projects/models.py
index 6186f8492b..5ea36b76f4 100644
--- a/designsafe/apps/api/projects/models.py
+++ b/designsafe/apps/api/projects/models.py
@@ -1,3 +1,8 @@
+import six
+import json
+import logging
+import xml.etree.ElementTree as ET
+
from designsafe.apps.api.agave.models.metadata import (BaseMetadataResource,
BaseMetadataPermissionResource)
from designsafe.apps.api.agave.models.files import (BaseFileResource,
@@ -6,9 +11,9 @@
from designsafe.apps.api.agave.models.systems import BaseSystemResource
from designsafe.apps.api.agave.models.systems import roles as system_roles
from designsafe.apps.api.agave import to_camel_case
-import six
-import json
-import logging
+from designsafe.apps.api.agave.models.base import Model as MetadataModel
+from designsafe.apps.api.agave.models import fields
+from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
@@ -49,16 +54,26 @@ def list_projects(cls, agave_client):
records = agave_client.meta.listMetadata(q=json.dumps(query), privileged=False)
return [cls(agave_client=agave_client, **r) for r in records]
+ @classmethod
+ def search(cls, q, agave_client):
+ """
+ Search projects
+ """
+ if isinstance(q, basestring):
+ query = q
+ else:
+ query = json.dumps(q)
+ records = agave_client.meta.listMetadata(q=query, privileged=False)
+ return [cls(agave_client=agave_client, **r) for r in records]
+
def team_members(self):
permissions = BaseMetadataPermissionResource.list_permissions(
self.uuid, self._agave)
- logger.debug('self.value: %s', self.value)
pi = self.pi
- co_pis_list = getattr(self, 'co_pis', [])
- co_pis = []
- if co_pis_list:
- co_pis = [x.username for x in permissions if x.username in co_pis_list]
+ co_pis = getattr(self, 'co_pis', [])
+ logger.info(co_pis)
+ # co_pis = [x.username for x in permissions if x.username in co_pis_list]
team_members_list = [x.username for x in permissions if x.username not in co_pis + [pi]]
return {'pi': pi,
@@ -136,6 +151,28 @@ def pi(self, value):
def co_pis(self):
return self.value.get('coPis', [])
+ def add_co_pi(self, username):
+ logger.info('Adding Co PI "{}" to project "{}"'.format(username, self.uuid))
+
+ coPis = self.value.get('coPis', [])
+
+ coPis.append(username)
+ self.value['coPis'] = list(set(coPis))
+ self.add_collaborator(username)
+
+ def remove_co_pi(self, username):
+ logger.info('Removing Co PI "{}" to project "{}"'.format(username, self.uuid))
+
+ coPis = self.value.get('coPis', [])
+ # logger.info(coPis)
+ coPis = [uname for uname in coPis if uname != username]
+
+ self.value['coPis'] = coPis
+ # logger.info(self.value)
+ # Set permissions on the metadata record
+ self.remove_collaborator(username)
+
+
@co_pis.setter
def co_pis(self, value):
# TODO is this assertion valuable?
@@ -179,3 +216,350 @@ def project_data_listing(self, path='/'):
return BaseFileResource.listing(system=self.project_system_id,
path=path,
agave_client=self._agave)
+
+
+class RelatedEntity(MetadataModel):
+ def to_body_dict(self):
+ body_dict = super(RelatedEntity, self).to_body_dict()
+ body_dict['_relatedFields'] = []
+ for attrname, field in six.iteritems(self._meta._related_fields):
+ body_dict['_relatedFields'].append(attrname)
+ return body_dict
+
+class ExperimentalProject(MetadataModel):
+ model_name = 'designsafe.project'
+ team_members = fields.ListField('Team Members')
+ co_pis = fields.ListField('Co PIs')
+ project_type = fields.CharField('Project Type', max_length=255, default='other')
+ project_id = fields.CharField('Project Id')
+ description = fields.CharField('Description', max_length=1024, default='')
+ title = fields.CharField('Title', max_length=255, default='')
+ pi = fields.CharField('PI', max_length=255)
+ award_number = fields.CharField('Award Number', max_length=255)
+ associated_projects = fields.ListField('Associated Project')
+ ef = fields.CharField('Experimental Facility', max_length=512)
+ keywords = fields.CharField('Keywords')
+
+ def to_body_dict(self):
+ body_dict = super(ExperimentalProject, self).to_body_dict()
+ body_dict['_related'] = {}
+ for attrname, field in six.iteritems(self._meta._related_fields):
+ body_dict['_related'][attrname] = field.rel_cls.model_name
+
+ for attrname in self._meta._reverse_fields:
+ field = getattr(self, attrname)
+ body_dict['_related'][attrname] = field.related_obj_name
+
+ return body_dict
+
+
+class FileModel(MetadataModel):
+ model_name = 'designsafe.file'
+ keywords = fields.ListField('Keywords')
+ project_UUID = fields.RelatedObjectField(ExperimentalProject, default=[])
+
+class DataTag(MetadataModel):
+ _is_nested = True
+ file = fields.RelatedObjectField(FileModel, default=[])
+ desc = fields.CharField('Description', max_length=512, default='')
+
+ def __eq__(self, other):
+ return self.file == other.file and self.desc == other.desc
+
+ def __hash__(self):
+ return hash(('file', self.file, 'desc', self.desc))
+
+class Experiment(RelatedEntity):
+ model_name = 'designsafe.project.experiment'
+ experiment_type = fields.CharField('Experiment Type', max_length=255, default='other')
+ description = fields.CharField('Description', max_length=1024, default='')
+ title = fields.CharField('Title', max_length=1024)
+ experimental_facility = fields.CharField('Experimental Facility', max_length=1024)
+ equipment_type = fields.CharField('Equipment Type')
+ authors = fields.ListField('Authors')
+ project = fields.RelatedObjectField(ExperimentalProject)
+
+class AnalysisTagGeneral(MetadataModel):
+ _is_nested = True
+ analysis_data_graph = fields.ListField('Analysis Data Graph', list_cls=DataTag)
+ analysis_data_visualization = fields.ListField('Analysis Data Visualization', list_cls=DataTag)
+ analysis_data_table = fields.ListField('Analysis Data Table', list_cls=DataTag)
+ application = fields.ListField('Application', list_cls=DataTag)
+ application_matlab = fields.ListField('Application MATLAB', list_cls=DataTag)
+ application_r = fields.ListField('Application R', list_cls=DataTag)
+ application_jupiter_notebook = fields.ListField('Application Notebook', list_cls=DataTag)
+ application_other = fields.ListField('Application Other', list_cls=DataTag)
+ application_script = fields.ListField('Application Script', list_cls=DataTag)
+
+class AnalysisTag(MetadataModel):
+ _is_nested = True
+ general = fields.NestedObjectField(AnalysisTagGeneral)
+
+class Analysis(RelatedEntity):
+ model_name = 'designsafe.project.analysis'
+ analysis_type = fields.CharField('Analysis Type', max_length=255, default='other')
+ title = fields.CharField('Title', max_length=1024)
+ description = fields.CharField('Description', max_length=1024, default='')
+ analysis_data = fields.CharField('Analysis Data', max_length=1024, default='')
+ application = fields.CharField('Analysis Data', max_length=1024, default='')
+ script = fields.RelatedObjectField(FileModel, multiple=True)
+ tags = fields.NestedObjectField(AnalysisTag)
+ project = fields.RelatedObjectField(ExperimentalProject)
+ experiments = fields.RelatedObjectField(Experiment)
+ #events = fields.RelatedObjectField(Event)
+ files = fields.RelatedObjectField(FileModel, multiple=True)
+
+class ModelConfigTagCentrifuge(MetadataModel):
+ _is_nested = True
+ triaxial_test = fields.ListField('Triaxal Test', list_cls=DataTag)
+ soil_strenght = fields.ListField('Soil Strength', list_cls=DataTag)
+ hinged_plate_container = fields.ListField('Hinged Plate Container', list_cls=DataTag)
+ rigid_container = fields.ListField('Rigid Container', list_cls=DataTag)
+ flexible_shear_beam_container = fields.ListField('Flexible Shear Beam Container', list_cls=DataTag)
+ structural_model = fields.ListField('Structural Model', list_cls=DataTag)
+ gravel = fields.ListField('Gravel', list_cls=DataTag)
+ sand = fields.ListField('Sand', list_cls=DataTag)
+ silt = fields.ListField('Silt', list_cls=DataTag)
+ clay = fields.ListField('Clay', list_cls=DataTag)
+ pit = fields.ListField('pit', list_cls=DataTag)
+
+class ModelConfigTagShakeTable(MetadataModel):
+ _is_nested = True
+ numerical_model = fields.ListField('Numerical Model', list_cls=DataTag)
+ loading_protocol_intensity = fields.ListField('Loading Protocol', list_cls=DataTag)
+ loading_protocol_ground_motions = fields.ListField('Loading Protocol Ground Motions', list_cls=DataTag)
+ material_test = fields.ListField('Material Test', list_cls=DataTag)
+ structural_model = fields.ListField('Structural Model', list_cls=DataTag)
+ soil = fields.ListField('Soil', list_cls=DataTag)
+ steel = fields.ListField('Steel', list_cls=DataTag)
+ concrete = fields.ListField('Concrete', list_cls=DataTag)
+ wood = fields.ListField('Wood', list_cls=DataTag)
+ masonry = fields.ListField('Masonry', list_cls=DataTag)
+ protective_system_isolation = fields.ListField('Protective System Isolation', list_cls=DataTag)
+ protective_system_rocking = fields.ListField('Protective System Rocking', list_cls=DataTag)
+ protective_system_damping = fields.ListField('Protective System Damping', list_cls=DataTag)
+
+class ModelConfigTagWave(MetadataModel):
+ _is_nested = True
+ large_wave_flume = fields.ListField('Large Wave Flume', list_cls=DataTag)
+ directional_wave_basin = fields.ListField('Directional Wave Basin', list_cls=DataTag)
+ wavemaker_input_file = fields.ListField('Wavemaker Input File', list_cls=DataTag)
+ board_siplacement = fields.ListField('Board Siplacement', list_cls=DataTag)
+ free_surface_height = fields.ListField('Free Surface Height', list_cls=DataTag)
+ hydrodynamic_conditions = fields.ListField('Hydrodynamic Conditions', list_cls=DataTag)
+
+class ModelConfigTagWind(MetadataModel):
+ _is_nested = True
+ bridge = fields.ListField('Bridge', list_cls=DataTag)
+ building_low_rise = fields.ListField('Building Low Rise', list_cls=DataTag)
+ building_tall = fields.ListField('Building Tall', list_cls=DataTag)
+ chimney = fields.ListField('Chimney', list_cls=DataTag)
+ mast = fields.ListField('Mast', list_cls=DataTag)
+ model_aeroelastic = fields.ListField('Model Aeroelastic', list_cls=DataTag)
+ model_full = fields.ListField('Model Full', list_cls=DataTag)
+ model_rigid = fields.ListField('Model Rigid', list_cls=DataTag)
+ model_section = fields.ListField('Model Section', list_cls=DataTag)
+ scale_full = fields.ListField('Scale Full', list_cls=DataTag)
+ scale_large = fields.ListField('Scale Large', list_cls=DataTag)
+ scale_small = fields.ListField('Scale Small', list_cls=DataTag)
+ tower = fields.ListField('Tower', list_cls=DataTag)
+
+class ModelConfigTagGeneral(MetadataModel):
+ _is_nested = True
+ model_drawing = fields.ListField('Model Drawing', list_cls=DataTag)
+ image = fields.ListField('Model Drawing', list_cls=DataTag)
+ video = fields.ListField('Model Drawing', list_cls=DataTag)
+
+class ModelConfigTag(MetadataModel):
+ _is_nested = True
+ centrifuge = fields.NestedObjectField(ModelConfigTagCentrifuge)
+ general = fields.NestedObjectField(ModelConfigTagGeneral)
+ shake_table = fields.NestedObjectField(ModelConfigTagShakeTable)
+ wave = fields.NestedObjectField(ModelConfigTagWave)
+ wind = fields.NestedObjectField(ModelConfigTagWind)
+
+class ModelConfiguration(RelatedEntity):
+ model_name = 'designsafe.project.model_config'
+ title = fields.CharField('Title', max_length=512)
+ description = fields.CharField('Description', max_length=1024, default='')
+ #spatial = fields.CharField('Spatial', max_length=1024)
+ #lat = fields.CharField('Lat', max_length=1024)
+ #lon = fields.CharField('Lon', max_length=1024)
+ #model_drawing = fields.RelatedObjectField(FileModel, multiple=True)
+ #image = fields.NestedObjectField(DataTag)
+ #video = fields.NestedObjectField(DataTag)
+ tags = fields.NestedObjectField(ModelConfigTag)
+ project = fields.RelatedObjectField(ExperimentalProject)
+ experiments = fields.RelatedObjectField(Experiment)
+ #events = fields.RelatedObjectField(Event)
+ files = fields.RelatedObjectField(FileModel, multiple=True)
+
+class SensorListTagCentrifuge(MetadataModel):
+ _is_nested = True
+ strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag)
+ bender_element = fields.ListField('Bender Element', list_cls=DataTag)
+ load_cell = fields.ListField('Load Cell', list_cls=DataTag)
+ lineal_potentiometer = fields.ListField('Lineal Potentiometer', list_cls=DataTag)
+ tactile_pressure = fields.ListField('Tactile Pressure', list_cls=DataTag)
+ pore_pressure_transducer = fields.ListField('Pore Pressure Transducer', list_cls=DataTag)
+ linear_variable_differential_transformer = fields.ListField('Linear Variable Differential Transformer', list_cls=DataTag)
+ accelerometer = fields.ListField('Accelerometer', list_cls=DataTag)
+ sensor_calibration = fields.ListField('Sensor Calibration', list_cls=DataTag)
+
+class SensorListTagShakeTable(MetadataModel):
+ _is_nested = True
+ accelerometer = fields.ListField('Accelerometer', list_cls=DataTag)
+ linear_potentiometer = fields.ListField('Linear Potentiometer', list_cls=DataTag)
+ displacement_sensor = fields.ListField('Displacement Sensor', list_cls=DataTag)
+ load_cell = fields.ListField('Load Cell', list_cls=DataTag)
+ soil_sensor = fields.ListField('Soil Sensor', list_cls=DataTag)
+ strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag)
+
+class SensorListTagWave(MetadataModel):
+ _is_nested = True
+ wave_gauge_calibration = fields.ListField('Wave Gauge Calibration', list_cls=DataTag)
+ synchronization = fields.ListField('Synchronization', list_cls=DataTag)
+ sample_synchronization = fields.ListField('Sample Synchronization', list_cls=DataTag)
+ project_instrumentation_locations = fields.ListField('Project Instrumentation Locations', list_cls=DataTag)
+ self_calibrating = fields.ListField('Self Calibrating', list_cls=DataTag)
+ instrument_survey = fields.ListField('Instrument Survey', list_cls=DataTag)
+ absolute_timing = fields.ListField('Absolute Timing', list_cls=DataTag)
+ wiring_details = fields.ListField('Wiring Details', list_cls=DataTag)
+ calibration_summary = fields.ListField('Calibration Summary', list_cls=DataTag)
+
+class SensorListTagWind(MetadataModel):
+ _is_nested = True
+ accelerometer = fields.ListField('Accelerometer', list_cls=DataTag)
+ component_velocity_and_statistic_pressure_robes = fields.ListField('Component Velocity And Stastic Pressure Robes', list_cls=DataTag)
+ inertial = fields.ListField('Intertial', list_cls=DataTag)
+ laser = fields.ListField('Laser', list_cls=DataTag)
+ linear_variable_differential_transformer = fields.ListField('Linear Variable Differential Transformer', list_cls=DataTag)
+ load_cells = fields.ListField('Load Cells', list_cls=DataTag)
+ particle_image_velocimetry = fields.ListField('Particle Image Velocimetry', list_cls=DataTag)
+ pitot_tube = fields.ListField('Pitot Tube', list_cls=DataTag)
+ pressure_scanner = fields.ListField('Pressure Scanner', list_cls=DataTag)
+ strain_gauge = fields.ListField('Strain Gauge', list_cls=DataTag)
+ string_potentiometer = fields.ListField('String Potentiometer', list_cls=DataTag)
+
+class SensorListTagGeneral(MetadataModel):
+ _is_nested = True
+ sensor_list = fields.ListField('Sensor List', list_cls=DataTag)
+ sensor_drawing = fields.ListField('Sensor Drawing', list_cls=DataTag)
+
+class SensorListTag(MetadataModel):
+ _is_nested = True
+ centrifuge = fields.NestedObjectField(SensorListTagCentrifuge)
+ general = fields.NestedObjectField(SensorListTagGeneral)
+ shake_table = fields.NestedObjectField(SensorListTagShakeTable)
+ wave = fields.NestedObjectField(SensorListTagWave)
+ wind = fields.NestedObjectField(SensorListTagWind)
+
+class SensorList(RelatedEntity):
+ model_name = 'designsafe.project.sensor_list'
+ sensor_list_type = fields.CharField('Sensor List Type', max_length=255, default='other')
+ title = fields.CharField('Title', max_length=1024)
+ description = fields.CharField('Description', max_length=1024, default='')
+ #sensor_drawing = fields.RelatedObjectField(FileModel, multiple=True)
+ tags = fields.NestedObjectField(SensorListTag)
+ project = fields.RelatedObjectField(ExperimentalProject)
+ experiments = fields.RelatedObjectField(Experiment)
+ #events = fields.RelatedObjectField(Event)
+ model_configs = fields.RelatedObjectField(ModelConfiguration)
+ files = fields.RelatedObjectField(FileModel, multiple=True)
+
+class EventTagCentrifuge(MetadataModel):
+ _is_nested=True
+ centrifuge_speed = fields.ListField('Centrifuge Speed', list_cls=DataTag)
+ slow_data = fields.ListField('Slow Data', list_cls=DataTag)
+ fast_data = fields.ListField('Fast Data', list_cls=DataTag)
+ t_bar_test = fields.ListField('T-Bar Test', list_cls=DataTag)
+ bender_element_test = fields.ListField('Bender Element Test', list_cls=DataTag)
+ actuator = fields.ListField('Actuator', list_cls=DataTag)
+ cone_penetrometer = fields.ListField('Cone Penetrometer', list_cls=DataTag)
+ shaking = fields.ListField('Shaking', list_cls=DataTag)
+ raw = fields.ListField('Raw', list_cls=DataTag)
+ calibrated = fields.ListField('Calibrated', list_cls=DataTag)
+
+class EventTagShakeTable(MetadataModel):
+ _is_nested=True
+ shake_table_test = fields.ListField('Shaek Table Test', list_cls=DataTag)
+
+class EventTagWave(MetadataModel):
+ _is_nested=True
+ bathymetric_survey_data = fields.ListField('Bathymetric Survey Data', list_cls=DataTag)
+ instrumet_calibration_data = fields.ListField('Instrument Calibration Data', list_cls=DataTag)
+ experimental_conditions = fields.ListField('Experimental Conditions', list_cls=DataTag)
+ raw = fields.ListField('Raw', list_cls=DataTag)
+ physical_units = fields.ListField('Physical Units', list_cls=DataTag)
+ channel_name = fields.ListField('Channel Name', list_cls=DataTag)
+ matlab_toolbox_source_code = fields.ListField('Matlab Toolbox Source Code', list_cls=DataTag)
+
+class EventTagWind(MetadataModel):
+ _is_nested=True
+ aerodynamic_roughness = fields.ListField('Aerodynamic', list_cls=DataTag)
+ flow_boundary_layer = fields.ListField('Flow Boundary Layer', list_cls=DataTag)
+ flow_profile = fields.ListField('Flow Profile', list_cls=DataTag)
+ flow_steady_gusting = fields.ListField('Flow Steady Gusting', list_cls=DataTag)
+ incident_flow = fields.ListField('Incident Flow', list_cls=DataTag)
+ reynolds_number = fields.ListField('Reynolds Number', list_cls=DataTag)
+ reynolds_stress = fields.ListField('Reynolds Stress', list_cls=DataTag)
+ scale_integral_length = fields.ListField('Scale Inetgral Length', list_cls=DataTag)
+ terrain_open = fields.ListField('Terrain Open', list_cls=DataTag)
+ terrain_urban = fields.ListField('Terrain Urban', list_cls=DataTag)
+ test_aerodynamic = fields.ListField('Test Aerodynamic', list_cls=DataTag)
+ test_complex_topography = fields.ListField('Test Complex Topography', list_cls=DataTag)
+ test_destructive = fields.ListField('Test Destructive', list_cls=DataTag)
+ test_dispersion = fields.ListField('Test Dispersion', list_cls=DataTag)
+ test_environmental = fields.ListField('Test Environmental', list_cls=DataTag)
+ test_external_pressure = fields.ListField('Test External Pressure', list_cls=DataTag)
+ test_high_frequency_force_balance = fields.ListField('Test high Frequency Force Balance', list_cls=DataTag)
+ test_internal_pressure = fields.ListField('Test Internal Pressure', list_cls=DataTag)
+ test_pedestrian_level_winds = fields.ListField('Test Pedestrian Level Winds', list_cls=DataTag)
+ turbulence_profile = fields.ListField('Turbulance Profile', list_cls=DataTag)
+ turbulence_spectrum = fields.ListField('Turbulence Spectrum', list_cls=DataTag)
+ uniform_flow = fields.ListField('Uniform Flow', list_cls=DataTag)
+ velocity_mean = fields.ListField('Velocity Mean', list_cls=DataTag)
+ velocity_profile = fields.ListField('Velocity Profile', list_cls=DataTag)
+ wind_direction = fields.ListField('Wind Direction', list_cls=DataTag)
+ wind_duration = fields.ListField('Wind Duration', list_cls=DataTag)
+ wind_speed = fields.ListField('Wind Speed', list_cls=DataTag)
+ wind_tunnel_open_circuit = fields.ListField('Wind Tunnel Open Circuit', list_cls=DataTag)
+ wind_tunnel_open_jet = fields.ListField('Wind Tunnel Open Jet', list_cls=DataTag)
+ wind_tunnel_closed_circuit = fields.ListField('Wind Tunnel closed Circuit', list_cls=DataTag)
+ three_sec_gust = fields.ListField('Three Sec Gust', list_cls=DataTag)
+
+class EventTagGeneral(MetadataModel):
+ _is_nested=True
+ data_units = fields.ListField('Data Units', list_cls=DataTag)
+ image = fields.ListField('Image', list_cls=DataTag)
+ video = fields.ListField('Video', list_cls=DataTag)
+
+class EventTag(MetadataModel):
+ _is_nested = True
+ centrifuge = fields.NestedObjectField(EventTagCentrifuge)
+ general = fields.NestedObjectField(EventTagGeneral)
+ shake_table = fields.NestedObjectField(EventTagShakeTable)
+ wave = fields.NestedObjectField(EventTagWave)
+ wind = fields.NestedObjectField(EventTagWind)
+
+class Event(RelatedEntity):
+ model_name = 'designsafe.project.event'
+ event_type = fields.CharField('Event Type', max_length=255, default='other')
+ title = fields.CharField('Title', max_length=1024)
+ description = fields.CharField('Description', max_length=1024, default='')
+ #load = fields.RelatedObjectField(FileModel, multiple=True)
+ tags = fields.NestedObjectField(SensorListTag)
+ analysis = fields.RelatedObjectField(Analysis)
+ project = fields.RelatedObjectField(ExperimentalProject)
+ experiments = fields.RelatedObjectField(Experiment)
+ model_configs = fields.RelatedObjectField(ModelConfiguration)
+ sensor_lists = fields.RelatedObjectField(SensorList)
+ files = fields.RelatedObjectField(FileModel, multiple=True)
+
+class Report(RelatedEntity):
+ model_name = 'designsafe.project.report'
+ title = fields.CharField('Title', max_length=1024)
+ description = fields.CharField('Description', max_length=1024, default='')
+ project = fields.RelatedObjectField(ExperimentalProject)
+ experiments = fields.RelatedObjectField(Experiment)
+ files = fields.RelatedObjectField(FileModel, multiple=True)
diff --git a/designsafe/apps/api/projects/urls.py b/designsafe/apps/api/projects/urls.py
index 36b814acc0..7426994479 100644
--- a/designsafe/apps/api/projects/urls.py
+++ b/designsafe/apps/api/projects/urls.py
@@ -11,18 +11,39 @@
"""
from django.conf.urls import patterns, url, include
-from designsafe.apps.api.projects.views import (ProjectCollectionView,
+from designsafe.apps.api.projects.views import (ProjectListingView,
+ ProjectCollectionView,
ProjectDataView,
ProjectCollaboratorsView,
- ProjectInstanceView)
+ ProjectInstanceView,
+ ProjectMetaView,
+ PublicationView)
+from designsafe.apps.api.projects.managers.yamz import YamzBaseView
urlpatterns = [
url(r'^$', ProjectCollectionView.as_view(), name='index'),
- url(r'^(?P[a-z0-9\-]+)/$', ProjectInstanceView.as_view(), name='project'),
+
+ url(r'^yamz/(?P[a-zA-Z0-9]+)/?$', YamzBaseView.as_view(), name='yamz'),
+
+ url(r'^publication/((?P[a-zA-Z0-9\-\_\.]+)/?)?', PublicationView.as_view(), name='publication'),
+
+ url(r'^listing/(?P[a-zA-Z0-9\-_\.]+)/?$', ProjectListingView.as_view(), name='listing'),
+
+ url(r'^meta/(?P[^ \/]+)/?$',
+ ProjectMetaView.as_view(), name='project_meta'),
+
+ url(r'^(?P[a-z0-9\-]+)/meta/(?P[a-zA-Z0-9\.\-_]+)/?$',
+ ProjectMetaView.as_view(), name='project_meta'),
+
+ url(r'^(?P[a-z0-9\-]+)/$',
+ ProjectInstanceView.as_view(), name='project'),
+
url(r'^(?P[a-z0-9\-]+)/collaborators/$',
ProjectCollaboratorsView.as_view(), name='project_collaborators'),
+
url(r'^(?P[a-z0-9\-]+)/data/$',
ProjectDataView.as_view(), name='project_data'),
+
url(r'^(?P[a-z0-9\-]+)/data/(?P.*)/$',
ProjectDataView.as_view(), name='project_data'),
]
diff --git a/designsafe/apps/api/projects/views.py b/designsafe/apps/api/projects/views.py
index 60a1f3fdac..59e3082969 100644
--- a/designsafe/apps/api/projects/views.py
+++ b/designsafe/apps/api/projects/views.py
@@ -1,16 +1,28 @@
from django.core.urlresolvers import reverse
from django.conf import settings
-from django.http import JsonResponse
+from django.http.response import HttpResponseForbidden
+from django.http import JsonResponse, HttpResponseBadRequest
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
+from django.utils.decorators import method_decorator
+from django.contrib.auth.decorators import login_required
+from designsafe.apps.api.decorators import agave_jwt_login
from designsafe.apps.api import tasks
from designsafe.apps.api.views import BaseApiView
from designsafe.apps.api.mixins import SecureMixin
from designsafe.apps.api.projects.models import Project
from designsafe.apps.api.agave import get_service_account_client
+from designsafe.apps.api.agave.models.metadata import BaseMetadataPermissionResource
from designsafe.apps.api.agave.models.files import BaseFileResource
from designsafe.apps.api.agave.models.util import AgaveJSONEncoder
from designsafe.apps.accounts.models import DesignSafeProfile
+from requests.exceptions import HTTPError
+from designsafe.apps.api.projects.models import (ExperimentalProject, FileModel,
+ Experiment, ModelConfiguration,
+ Event, Analysis, SensorList,
+ Report)
+from designsafe.apps.api.agave.filemanager.public_search_index import PublicationManager, Publication
+from designsafe.apps.api import tasks
import logging
import json
@@ -27,8 +39,54 @@ def template_project_storage_system(project):
system_template['storage']['rootDir'].format(project.uuid)
return system_template
+class PublicationView(BaseApiView):
+ def get(self, request, project_id):
+ pub = Publication(project_id=project_id)
+ if pub is not None:
+ return JsonResponse(pub.to_dict())
+ else:
+ return JsonResponse({'status': 404,
+ 'message': 'Not found'},
+ status=404)
+
+ @method_decorator(agave_jwt_login)
+ @method_decorator(login_required)
+ def post(self, request, **kwargs):
+ if request.is_ajax():
+ data = json.loads(request.body)
+
+ else:
+ data = request.POST
+
+ #logger.debug('publication: %s', json.dumps(data, indent=2))
+ pub = PublicationManager().save_publication(data['publication'])
+ tasks.save_publication.apply_async(args=[pub.projectId],queue='files')
+ return JsonResponse({'status': 200,
+ 'message': 'Your publication has been '
+ 'schedule for publication'},
+ status=200)
-class ProjectCollectionView(BaseApiView, SecureMixin):
+class ProjectListingView(SecureMixin, BaseApiView):
+ def get(self, request, username):
+ """Returns a list of Project for a specific user.
+
+ If the requesting user is a super user then we can 'impersonate'
+ another user. Else this is an unauthorized request.
+
+ """
+ if not request.user.is_superuser:
+ return HttpResponseForbidden()
+
+ user = get_user_model().objects.get(username=username)
+ ag = user.agave_oauth.client
+ q = request.GET.get('q', None)
+ if not q:
+ projects = Project.list_projects(agave_client=ag)
+ else:
+ projects = Project.search(q=q, agave_client=ag)
+ return JsonResponse({'projects': projects}, encoder=AgaveJSONEncoder)
+
+class ProjectCollectionView(SecureMixin, BaseApiView):
def get(self, request):
"""
@@ -37,6 +95,7 @@ def get(self, request):
:return: A list of Projects to which the current user has access
:rtype: JsonResponse
"""
+ #raise HTTPError('Custom Error')
ag = request.user.agave_oauth.client
projects = Project.list_projects(agave_client=ag)
data = {'projects': projects}
@@ -79,11 +138,15 @@ def post(self, request):
associated_projects = post_data.get('associatedProjects', {})
description = post_data.get('description', '')
new_pi = post_data.get('pi')
+ keywords = post_data.get('keywords', '')
+ project_id = post_data.get('projectId', '')
p.update(title=title,
award_number=award_number,
project_type=project_type,
associated_projects=associated_projects,
- description=description)
+ description=description,
+ keywords=keywords,
+ projectId=project_id)
p.pi = new_pi
p.save()
@@ -141,11 +204,11 @@ def post(self, request):
try:
collab_user.profile.send_mail(
"[Designsafe-CI] You have been added to a project!",
- "
You have been added to the project {title} as PI
You can visit the project using this url {url}".format(title=p.title,
+ "
You have been added to the project {title} as PI
You can visit the project using this url {url}".format(title=p.title,
url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (p.uuid,))))
except DesignSafeProfile.DoesNotExist as err:
logger.info("Could not send email to user %s", collab_user)
- body = "
You have been added to the project {title} as PI
You can visit the project using this url {url}".format(title=p.title,
+ body = "
You can visit the project using this url {url}".format(title=project.title,
+ "
You have been added to the project {title} as PI
You can visit the project using this url {url}".format(title=project.title,
url=request.build_absolute_uri(reverse('designsafe_data:data_depot') + '/projects/%s/' % (project.uuid,))))
except DesignSafeProfile.DoesNotExist as err:
logger.info("Could not send email to user %s", collab_user)
- body = "
You have been added to the project {title} as PI
You can visit the project using this url {url}".format(title=project.title,
+ body = "
+
diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html
new file mode 100644
index 0000000000..ee4363c141
--- /dev/null
+++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/db-modal.html
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html
new file mode 100644
index 0000000000..cc06eb3d8c
--- /dev/null
+++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/help.html
@@ -0,0 +1,36 @@
+
+
Designsafe Hazmapper Help
+
+
+
+
Layers
+
Maps can be organized using multiple layers. Click on the "New Layer Group" button to create an empty Layer Group.
+ The active layer will be highlighted. Drawn objects will automatically be added to the active layer. Clicking on the
+ checkbox for that layer group will show/hide all features in the group.
+
+
+
- Edit project name / details
+
- Show/hide the layer group tools.
+
- Zoom to feature.
+
- Show/hide the individual features in the group.
+
+
+
+
Tools
+
+
+
+
Description of tools, from top to bottom:
+
+
Measurement tool
+
Base layer picker
+
Line drawing tool
+
Polygon drawing tool
+
Marker tool
+
Edit drawn shape
+
Delete drawn shape
+
+
+
+
+
diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html
new file mode 100644
index 0000000000..796dfb92b2
--- /dev/null
+++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/image-overlay-modal.html
@@ -0,0 +1,52 @@
+
+
Overlay an image
+
+
+
+
+
diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html
new file mode 100644
index 0000000000..bbcbda0579
--- /dev/null
+++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/index.html
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html b/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html
new file mode 100644
index 0000000000..e42d140636
--- /dev/null
+++ b/designsafe/apps/geo/static/designsafe/apps/geo/html/map.html
@@ -0,0 +1,220 @@
+
This initial version of the Discovery Workspace allows users to
perform simulations and analyze data using popular open source simulation
codes OpenSees, ADCIRC, and OpenFOAM, as well as commercial tools such as
- MATLAB (software license verification required). The selection of codes and
- tools will continue to be expanded as seen at the
- Workbench Roadmap.
-
+ MATLAB (software license verification required).
+ I agree and understand that when I publish this data and receive a DOI that this will result in a locked, read-only publication that cannot be changed. If I have changes or revisions, this will result in a new DOI being created for the revised publication. I understand that my dataset meets the minimum metadata requirements per the Data Curation and Publication Guidelines.
+
+ I agree
+
+
+
diff --git a/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html b/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html
new file mode 100644
index 0000000000..dd1f7f5583
--- /dev/null
+++ b/designsafe/static/scripts/ng-designsafe/html/directives/my-data-browser.html
@@ -0,0 +1,71 @@
+
diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html
new file mode 100644
index 0000000000..0f9a661d93
--- /dev/null
+++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-preview-tree.html
@@ -0,0 +1,51 @@
+
+
+ Preview Tree
+
+
+
+
+
+
+
+ {{experiment.value.title}}
+
+
+
+
+ Model Config
+ {{modelConfig.value.title}}
+
+
+
+
+ Sensor
+ {{sensorList.value.title}}
+
+
+
+
+ Event
+ {{event.value.title}}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html
new file mode 100644
index 0000000000..1dea7c53a5
--- /dev/null
+++ b/designsafe/static/scripts/ng-designsafe/html/modals/data-browser-service-categories.html
@@ -0,0 +1,404 @@
+
+
+
+
+
Select from your inventory to assign a category
+
+ To delete a category it is necessary to, first, remove every file from said category. After this select the category below and click on the red "Delete Category" buton.
+