Skip to content

Commit 4a9df7e

Browse files
committed
Merge branch 'release/19.31.0'
2 parents 6d9574c + 5b58c58 commit 4a9df7e

File tree

24 files changed

+3302
-2
lines changed

24 files changed

+3302
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ehthumbs.db
1010
Thumbs.db
1111
*.swp
1212
*~
13+
.ipynb_checkpoints
1314

1415
# R
1516
#######################
@@ -202,3 +203,4 @@ ssl/
202203

203204
# pyenv
204205
.python-version
206+

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
19.31.0 (2019-11-7)
6+
===================
7+
- EGAP: Parse project structure, add contributors, add files, ingest the draft registration, and add a Jupyter notebook
8+
- Modify a Chronos field for proper contributor classification
9+
510
19.30.0 (2019-10-16)
611
===================
712
- Fix weirdness around deleted nodes by not deleing OSF Storage

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ services:
383383
restart: unless-stopped
384384
environment:
385385
DJANGO_SETTINGS_MODULE: api.base.settings
386+
LANG: en_US.UTF-8
386387
volumes:
387388
- ./:/code:cached
388389
- osf_requirements_vol:/usr/lib/python2.7
@@ -396,6 +397,7 @@ services:
396397
restart: unless-stopped
397398
environment:
398399
DJANGO_SETTINGS_MODULE: admin.base.settings
400+
LANG: en_US.UTF-8
399401
volumes:
400402
- ./:/code:cached
401403
- osf_requirements_vol:/usr/lib/python2.7

egap_assets.zip

15.3 KB
Binary file not shown.

osf/external/chronos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def serialize_author(cls, contributor):
105105
if contributor._order == 0:
106106
contribution = 'firstAuthor'
107107
else:
108-
contribution = 'submittingAuthor'
108+
contribution = 'Author'
109109
ret.update({
110110
'CONTRIBUTION': contribution,
111111
'ORGANIZATION': '',
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# -*- coding: utf-8 -*-
2+
import logging
3+
4+
import os
5+
import json
6+
import shutil
7+
import requests
8+
import tempfile
9+
from django.core.management.base import BaseCommand
10+
from osf.utils.permissions import WRITE
11+
from osf.models import (
12+
RegistrationSchema,
13+
Node,
14+
DraftRegistration,
15+
OSFUser
16+
)
17+
from website.project.metadata.schemas import ensure_schema_structure, from_json
18+
from website.settings import WATERBUTLER_INTERNAL_URL
19+
from osf_tests.factories import ApiOAuth2PersonalTokenFactory
20+
from framework.auth.core import Auth
21+
from zipfile import ZipFile
22+
23+
logger = logging.getLogger(__name__)
24+
HERE = os.path.dirname(os.path.abspath(__file__))
25+
26+
27+
class EGAPUploadException(Exception):
28+
pass
29+
30+
31+
def ensure_egap_schema():
32+
schema = ensure_schema_structure(from_json('egap-registration.json'))
33+
schema_obj, created = RegistrationSchema.objects.update_or_create(
34+
name=schema['name'],
35+
schema_version=schema.get('version', 1),
36+
defaults={
37+
'schema': schema,
38+
}
39+
)
40+
if created:
41+
schema_obj.save()
42+
return RegistrationSchema.objects.get(name='EGAP Registration')
43+
44+
45+
def get_creator_auth_header(creator_username):
46+
creator = OSFUser.objects.get(username=creator_username)
47+
token = ApiOAuth2PersonalTokenFactory(owner=creator)
48+
token.save()
49+
return creator, {'Authorization': 'Bearer {}'.format(token.token_id)}
50+
51+
52+
def create_node_from_project_json(egap_assets_path, epag_project_dir, creator):
53+
with open(os.path.join(egap_assets_path, epag_project_dir, 'project.json'), 'r') as fp:
54+
project_data = json.load(fp)
55+
title = project_data['title']
56+
node = Node(title=title, creator=creator)
57+
node.save() # must save before adding contribs for auth reasons
58+
59+
for contributor in project_data['contributors']:
60+
node.add_contributor_registered_or_not(
61+
Auth(creator),
62+
full_name=contributor['name'],
63+
email=contributor['email'],
64+
permissions=WRITE,
65+
send_email='false'
66+
)
67+
68+
node.set_visible(creator, visible=False, log=False, save=True)
69+
70+
return node
71+
72+
73+
def recursive_upload(auth, node, dir_path, parent='', metadata=list()):
74+
try:
75+
for item in os.listdir(dir_path):
76+
item_path = os.path.join(dir_path, item)
77+
base_url = '{}/v1/resources/{}/providers/osfstorage/{}'.format(WATERBUTLER_INTERNAL_URL, node._id, parent)
78+
if os.path.isfile(item_path):
79+
with open(item_path, 'rb') as fp:
80+
url = base_url + '?name={}&kind=file'.format(item)
81+
resp = requests.put(url, data=fp.read(), headers=auth)
82+
else:
83+
url = base_url + '?name={}&kind=folder'.format(item)
84+
resp = requests.put(url, headers=auth)
85+
metadata = recursive_upload(auth, node, item_path, parent=resp.json()['data']['attributes']['path'], metadata=metadata)
86+
87+
if resp.status_code == 409: # if we retry something already uploaded just skip.
88+
continue
89+
90+
if resp.status_code != 201:
91+
raise EGAPUploadException('Error waterbutler response is {}, with {}'.format(resp.status_code, resp.content))
92+
93+
metadata.append(resp.json())
94+
except EGAPUploadException as e:
95+
logger.info(str(e))
96+
metadata = recursive_upload(auth, node, dir_path, parent=parent, metadata=metadata)
97+
98+
return metadata
99+
100+
101+
def get_egap_assets(guid, creator_auth):
102+
node = Node.load(guid)
103+
zip_file = node.files.first()
104+
temp_path = tempfile.mkdtemp()
105+
106+
url = '{}/v1/resources/{}/providers/osfstorage/{}'.format(WATERBUTLER_INTERNAL_URL, guid, zip_file._id)
107+
zip_file = requests.get(url, headers=creator_auth).content
108+
109+
egap_assets_path = os.path.join(temp_path, 'egap_assets.zip')
110+
111+
with open(egap_assets_path, 'w') as fp:
112+
fp.write(zip_file)
113+
114+
with ZipFile(egap_assets_path, 'r') as zipObj:
115+
zipObj.extractall(temp_path)
116+
117+
return temp_path
118+
119+
120+
def main(guid, creator_username):
121+
egap_schema = ensure_egap_schema()
122+
creator, creator_auth = get_creator_auth_header(creator_username)
123+
124+
egap_assets_path = get_egap_assets(guid, creator_auth)
125+
126+
# __MACOSX is a hidden file created by the os when zipping
127+
directory_list = [directory for directory in os.listdir(egap_assets_path) if directory not in ('egap_assets.zip', '__MACOSX')]
128+
129+
for epag_project_dir in directory_list:
130+
node = create_node_from_project_json(egap_assets_path, epag_project_dir, creator=creator)
131+
132+
non_anon_files = os.path.join(egap_assets_path, epag_project_dir, 'data', 'nonanonymous')
133+
non_anon_metadata = recursive_upload(creator_auth, node, non_anon_files)
134+
135+
anon_files = os.path.join(egap_assets_path, epag_project_dir, 'data', 'anonymous')
136+
if os.path.isdir(anon_files):
137+
anon_metadata = recursive_upload(creator_auth, node, anon_files)
138+
else:
139+
anon_metadata = {}
140+
141+
with open(os.path.join(egap_assets_path, epag_project_dir, 'registration-schema.json'), 'r') as fp:
142+
registration_metadata = json.load(fp)
143+
144+
# add selectedFileName Just so filenames are listed in the UI
145+
for data in non_anon_metadata:
146+
data['selectedFileName'] = data['data']['attributes']['name']
147+
148+
for data in anon_metadata:
149+
data['selectedFileName'] = data['data']['attributes']['name']
150+
151+
non_anon_titles = ', '.join([data['data']['attributes']['name'] for data in non_anon_metadata])
152+
registration_metadata['q37'] = {'comments': [], 'extra': non_anon_metadata, 'value': non_anon_titles}
153+
154+
anon_titles = ', '.join([data['data']['attributes']['name'] for data in anon_metadata])
155+
registration_metadata['q38'] = {'comments': [], 'extra': anon_metadata, 'value': anon_titles}
156+
157+
DraftRegistration.create_from_node(
158+
node,
159+
user=creator,
160+
schema=egap_schema,
161+
data=registration_metadata,
162+
)
163+
164+
shutil.rmtree(egap_assets_path)
165+
166+
167+
class Command(BaseCommand):
168+
"""Magically morphs csv data into lovable nodes with draft registrations attached
169+
"""
170+
171+
def add_arguments(self, parser):
172+
super(Command, self).add_arguments(parser)
173+
parser.add_argument(
174+
'-c',
175+
'--creator',
176+
help='This should be the username of the initial adminstrator for the imported nodes',
177+
required=True
178+
)
179+
parser.add_argument(
180+
'-id',
181+
'--guid',
182+
help='This should be the guid of the private project with the directory structure',
183+
required=True
184+
)
185+
186+
def handle(self, *args, **options):
187+
creator_username = options.get('creator', False)
188+
guid = options.get('guid', False)
189+
main(guid, creator_username)

0 commit comments

Comments
 (0)