From 2845b9ac601a18219976e9888778d65d8537de1d Mon Sep 17 00:00:00 2001 From: Alex Schiller Date: Fri, 18 Aug 2017 12:03:27 -0400 Subject: [PATCH 001/108] Change stickiness to use previous 30 days; add more stickiness metrics --- admin/static/js/metrics/metrics.es6.js | 82 +++++++++++++++++++++++- admin/templates/metrics/osf_metrics.html | 44 ++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/admin/static/js/metrics/metrics.es6.js b/admin/static/js/metrics/metrics.es6.js index aae87afdc8c..407b233512e 100644 --- a/admin/static/js/metrics/metrics.es6.js +++ b/admin/static/js/metrics/metrics.es6.js @@ -64,7 +64,31 @@ var getOneDayTimeframe = function(daysBack, monthsBack) { }; }; +/** + * Configure a time frame for a day x days ago (end) and y days prior to x (start) + * + * @method getVariableDayTimeframe + * @param {Integer} endDaysBack - the number of days back to set as the end day + * @param {Integer} totalDays - the number of days back to reach the start day + * @return {Object} the keen-formatted timeframe + */ +var getVariableDayTimeframe = function(endDaysBack, totalDays) { + var start = null; + var end = null; + var date = new Date(); + date.setUTCDate(date.getDate() - endDaysBack); + date.setUTCHours(0, 0, 0, 0, 0); + + end = date.toISOString(); + + date.setDate(date.getDate() - totalDays); + start = date.toISOString(); + return { + "start": start, + "end": end + }; +}; /** * Configure a Title for a chart dealing with the past month or day @@ -322,6 +346,14 @@ var monthlyActiveUsersQuery = new keenAnalysis.Query("count_unique", { timezone: "UTC" }); +// Previous 30 Days Active Users +var thirtyDaysActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: "previous_30_days", + timezone: "UTC" +}); + var dailyActiveUsersQuery = new keenAnalysis.Query("count_unique", { event_collection: "pageviews", target_property: "user.id", @@ -336,6 +368,48 @@ var totalProjectsQuery = new keenAnalysis.Query("sum", { timeframe: "previous_1_days", }); +// 7 Days back Active Users +var weekBackThirtyDaysActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(7, 30), +}); + +// 7 Days back Active Users +var weekBackDailyActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(7, 1), +}); + +// 28 Days back Active Users +var monthBackThirtyDaysActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(28, 30), +}); + +// 28 Days back Active Users +var monthBackDailyActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(28, 1), +}); + +// 364 Days back Active Users +var yearBackThirtyDaysActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(364, 30), +}); + +// 364 Days back Active Users +var yearBackDailyActiveUsersQuery = new keenAnalysis.Query("count_unique", { + eventCollection: "pageviews", + targetProperty: "user.id", + timeframe: getVariableDayTimeframe(364, 1), +}); + // <+><+><+><+><+><+ // user data | // ><+><+><+><+><+>+ @@ -808,7 +882,13 @@ var ActiveUserMetrics = function() { var HealthyUserMetrics = function() { // stickiness ratio - DAU/MAU - renderCalculationBetweenTwoQueries(dailyActiveUsersQuery, monthlyActiveUsersQuery, "#stickiness-ratio", null, "percentage"); + renderCalculationBetweenTwoQueries(dailyActiveUsersQuery, thirtyDaysActiveUsersQuery, "#stickiness-ratio-1-day-ago", null, "percentage"); + // stickiness ratio - DAU/MAU for 1 week ago + renderCalculationBetweenTwoQueries(weekBackDailyActiveUsersQuery, weekBackThirtyDaysActiveUsersQuery , "#stickiness-ratio-1-week-ago", null, "percentage"); + // stickiness ratio - DAU/MAU for 4 weeks ago + renderCalculationBetweenTwoQueries(monthBackDailyActiveUsersQuery, monthBackThirtyDaysActiveUsersQuery , "#stickiness-ratio-4-weeks-ago", null, "percentage"); + // stickiness ratio - DAU/MAU for 52 weeks ago + renderCalculationBetweenTwoQueries(yearBackDailyActiveUsersQuery, yearBackThirtyDaysActiveUsersQuery , "#stickiness-ratio-52-weeks-ago", null, "percentage"); }; diff --git a/admin/templates/metrics/osf_metrics.html b/admin/templates/metrics/osf_metrics.html index a322433e925..0c10648de4a 100644 --- a/admin/templates/metrics/osf_metrics.html +++ b/admin/templates/metrics/osf_metrics.html @@ -577,10 +577,50 @@

Project to User Ratios

Stickiness Ratio
-
+
- Of those that are active in the last month, how many are active daily? Daily Active Users / Monthly Active Users. + Of those that are active in the last 30 days, how many are active daily? + Formula: Daily Active Users (yesterday) / Daily Active Users previous 30 days +
+ + +
+
+
+ Stickiness Ratio 1 week ago +
+
+
+
+
+ Stickiness Ratio for the same day of the week 1 week ago. +
+
+
+
+
+
+ Stickiness Ratio 4 weeks ago +
+
+
+
+
+ Stickiness Ratio for the same day of the week 4 weeks ago. +
+
+
+
+
+
+ Stickiness Ratio 52 weeks ago +
+
+
+
+
+ Stickiness Ratio for the same day of the week 52 weeks ago.
From ce62f18212707f1294d7b85ffec967e759091afd Mon Sep 17 00:00:00 2001 From: Nan Chen Date: Wed, 23 Aug 2017 10:24:27 -0400 Subject: [PATCH 002/108] move the address info to footer in html mako template email --- .../templates/emails/archive_copy_error_user.html.mako | 9 --------- .../emails/archive_file_not_found_user.html.mako | 9 --------- .../emails/archive_size_exceeded_user.html.mako | 9 --------- website/templates/emails/archive_success.html.mako | 9 --------- .../emails/archive_uncaught_error_user.html.mako | 9 --------- website/templates/emails/comment_replies.html.mako | 9 --------- .../templates/emails/file_operation_failed.html.mako | 10 +--------- .../templates/emails/file_operation_success.html.mako | 10 +--------- website/templates/emails/file_updated.html.mako | 9 --------- website/templates/emails/mentions.html.mako | 9 --------- website/templates/emails/new_public_project.html.mako | 5 ----- website/templates/emails/no_addon.html.mako | 5 ----- website/templates/emails/no_login.html.mako | 5 ----- website/templates/emails/notify_base.mako | 3 ++- .../emails/prereg_challenge_accepted.html.mako | 5 ----- website/templates/emails/transactional.html.mako | 9 --------- website/templates/emails/welcome.html.mako | 5 ----- website/templates/emails/welcome_osf4i.html.mako | 6 ------ website/templates/emails/welcome_osf4m.html.mako | 6 ------ 19 files changed, 4 insertions(+), 137 deletions(-) diff --git a/website/templates/emails/archive_copy_error_user.html.mako b/website/templates/emails/archive_copy_error_user.html.mako index 2cc90fe121f..2a2772c3ffd 100644 --- a/website/templates/emails/archive_copy_error_user.html.mako +++ b/website/templates/emails/archive_copy_error_user.html.mako @@ -13,13 +13,4 @@ We cannot archive ${src.title} at this time because there were errors copying files from some of the linked third-party services. It's possible that this is due to temporary unavailability of one or more of these services and that retrying the registration may resolve this issue. Our development team is investigating this failure. We're sorry for any inconvenience this may have caused. - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/archive_file_not_found_user.html.mako b/website/templates/emails/archive_file_not_found_user.html.mako index 0d5ff1e527e..611ceeb298a 100644 --- a/website/templates/emails/archive_file_not_found_user.html.mako +++ b/website/templates/emails/archive_file_not_found_user.html.mako @@ -21,13 +21,4 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/archive_size_exceeded_user.html.mako b/website/templates/emails/archive_size_exceeded_user.html.mako index 866123e243a..d30498bc222 100644 --- a/website/templates/emails/archive_size_exceeded_user.html.mako +++ b/website/templates/emails/archive_size_exceeded_user.html.mako @@ -12,13 +12,4 @@ We cannot archive ${src.title} at this time because the projected size of the registration exceeds our usage limits. You should receive a followup email from our support team shortly. We're sorry for any inconvenience this may have caused. - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/archive_success.html.mako b/website/templates/emails/archive_success.html.mako index 9de303f093e..2825623a048 100644 --- a/website/templates/emails/archive_success.html.mako +++ b/website/templates/emails/archive_success.html.mako @@ -14,13 +14,4 @@ You can view the registration here. - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/archive_uncaught_error_user.html.mako b/website/templates/emails/archive_uncaught_error_user.html.mako index 8c227f83792..561895cdf8d 100644 --- a/website/templates/emails/archive_uncaught_error_user.html.mako +++ b/website/templates/emails/archive_uncaught_error_user.html.mako @@ -13,13 +13,4 @@ We cannot archive ${src.title} at this time because there were errors copying files to the registration. Our development team is investigating this failure. We're sorry for any inconvenience this may have caused. - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/comment_replies.html.mako b/website/templates/emails/comment_replies.html.mako index 2f08e5437e8..7203046075f 100644 --- a/website/templates/emails/comment_replies.html.mako +++ b/website/templates/emails/comment_replies.html.mako @@ -14,13 +14,4 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/file_operation_failed.html.mako b/website/templates/emails/file_operation_failed.html.mako index 6382c9dba51..1f02037421f 100644 --- a/website/templates/emails/file_operation_failed.html.mako +++ b/website/templates/emails/file_operation_failed.html.mako @@ -63,15 +63,6 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - @@ -80,6 +71,7 @@ diff --git a/website/templates/emails/file_operation_success.html.mako b/website/templates/emails/file_operation_success.html.mako index dd24d2e1361..dd667c85d8c 100644 --- a/website/templates/emails/file_operation_success.html.mako +++ b/website/templates/emails/file_operation_success.html.mako @@ -58,15 +58,6 @@ The ${'folder' if source_path.endswith('/') else 'file'} "${source_path.strip('/')}" has been successfully ${'moved' if action == 'move' else 'copied'} from ${source_addon} in ${source_node.title} to ${destination_addon}. - < - - @@ -79,6 +70,7 @@

Copyright © 2017 Center For Open Science, All rights reserved. | Privacy Policy

+

210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

You received this email because you are subscribed to email notifications.
Update Subscription Preferences

diff --git a/website/templates/emails/file_updated.html.mako b/website/templates/emails/file_updated.html.mako index ca024660118..3a7b2a8a122 100644 --- a/website/templates/emails/file_updated.html.mako +++ b/website/templates/emails/file_updated.html.mako @@ -14,13 +14,4 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/mentions.html.mako b/website/templates/emails/mentions.html.mako index 4adbfc4d8e7..728c1828071 100644 --- a/website/templates/emails/mentions.html.mako +++ b/website/templates/emails/mentions.html.mako @@ -14,13 +14,4 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/new_public_project.html.mako b/website/templates/emails/new_public_project.html.mako index e58549da085..0a2527aa0da 100644 --- a/website/templates/emails/new_public_project.html.mako +++ b/website/templates/emails/new_public_project.html.mako @@ -23,11 +23,6 @@
COS Support Team -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
diff --git a/website/templates/emails/no_addon.html.mako b/website/templates/emails/no_addon.html.mako index c23f5b3f90c..4d935049559 100644 --- a/website/templates/emails/no_addon.html.mako +++ b/website/templates/emails/no_addon.html.mako @@ -16,11 +16,6 @@ Best wishes,
COS Support Team -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
diff --git a/website/templates/emails/no_login.html.mako b/website/templates/emails/no_login.html.mako index de29780b605..0276ecb1390 100644 --- a/website/templates/emails/no_login.html.mako +++ b/website/templates/emails/no_login.html.mako @@ -20,11 +20,6 @@
COS Support Team -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
diff --git a/website/templates/emails/notify_base.mako b/website/templates/emails/notify_base.mako index af8f6a60aa4..bc5e455764c 100644 --- a/website/templates/emails/notify_base.mako +++ b/website/templates/emails/notify_base.mako @@ -63,8 +63,9 @@

Copyright © 2017 Center For Open Science, All rights reserved. | Privacy Policy - ${self.footer()}

+

210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

+

${self.footer()}

diff --git a/website/templates/emails/prereg_challenge_accepted.html.mako b/website/templates/emails/prereg_challenge_accepted.html.mako index 82788445156..a513a5b4ee2 100644 --- a/website/templates/emails/prereg_challenge_accepted.html.mako +++ b/website/templates/emails/prereg_challenge_accepted.html.mako @@ -22,8 +22,3 @@ Thank you for entering the Preregistration Challenge. Feel free to submit anothe Sincerely,
The team at the Center for Open Science -
-Center for Open Science
-210 Ridge McIntire Road
-Suite 500
-Charlottesville, VA 22903-5083
diff --git a/website/templates/emails/transactional.html.mako b/website/templates/emails/transactional.html.mako index c8ec25ac6bc..5a66b4c9a34 100644 --- a/website/templates/emails/transactional.html.mako +++ b/website/templates/emails/transactional.html.mako @@ -27,15 +27,6 @@ - - -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
- - diff --git a/website/templates/emails/welcome.html.mako b/website/templates/emails/welcome.html.mako index f461e38526b..78114ac1a13 100644 --- a/website/templates/emails/welcome.html.mako +++ b/website/templates/emails/welcome.html.mako @@ -41,11 +41,6 @@ Sincerely,

The Center for Open Science Team
-
-Center for Open Science
-210 Ridge McIntire Road
-Suite 500
-Charlottesville, VA 22903-5083
diff --git a/website/templates/emails/welcome_osf4i.html.mako b/website/templates/emails/welcome_osf4i.html.mako index 3690ae67bef..c53e0c1c800 100644 --- a/website/templates/emails/welcome_osf4i.html.mako +++ b/website/templates/emails/welcome_osf4i.html.mako @@ -43,12 +43,6 @@ Sincerely,

The Center for Open Science Team
-
-Center for Open Science
-210 Ridge McIntire Road
-Suite 500
-Charlottesville, VA 22903-5083
- diff --git a/website/templates/emails/welcome_osf4m.html.mako b/website/templates/emails/welcome_osf4m.html.mako index c6e1207b1e5..f2fea0d872a 100644 --- a/website/templates/emails/welcome_osf4m.html.mako +++ b/website/templates/emails/welcome_osf4m.html.mako @@ -24,12 +24,6 @@
COS Support Team -
- Center for Open Science
- 210 Ridge McIntire Road
- Suite 500
- Charlottesville, VA 22903-5083
-

P.S. Got questions? Just send us an email! From 4c61b97d92d1320fc9e0eac11cc51ba13fac8095 Mon Sep 17 00:00:00 2001 From: Nan Chen Date: Wed, 23 Aug 2017 10:42:04 -0400 Subject: [PATCH 003/108] add back the change for txt file --- website/templates/emails/comment_replies.txt.mako | 9 ++++++++- website/templates/emails/comments.txt.mako | 9 ++++++++- website/templates/emails/conference_failed.txt.mako | 8 +++++++- website/templates/emails/conference_inactive.txt.mako | 6 ++++++ website/templates/emails/conference_submitted.txt.mako | 6 ++++++ website/templates/emails/confirm.txt.mako | 4 ++++ website/templates/emails/confirm_erpc.txt.mako | 6 ++++++ website/templates/emails/confirm_merge.txt.mako | 9 ++++++++- .../templates/emails/confirm_preprints_branded.txt.mako | 6 ++++++ website/templates/emails/confirm_preprints_osf.txt.mako | 6 ++++++ website/templates/emails/confirm_prereg.txt.mako | 6 ++++++ website/templates/emails/confirm_registries_osf.txt.mako | 6 ++++++ .../templates/emails/contributor_added_default.txt.mako | 7 +++++++ .../contributor_added_preprint_node_from_osf.txt.mako | 7 +++++++ .../emails/contributor_added_preprints_branded.txt.mako | 7 +++++++ .../emails/contributor_added_preprints_osf.txt.mako | 7 +++++++ .../emails/email_contributors_of_registrations.txt.mako | 5 +++++ website/templates/emails/email_removed.txt.mako | 8 +++++++- .../templates/emails/external_confirm_create.txt.mako | 4 ++++ website/templates/emails/external_confirm_link.txt.mako | 4 ++++ .../templates/emails/external_confirm_success.txt.mako | 4 ++++ website/templates/emails/file_operation_failed.txt.mako | 6 ++++++ website/templates/emails/file_operation_success.txt.mako | 6 ++++++ website/templates/emails/forgot_password.txt.mako | 7 +++++++ website/templates/emails/forward_invite.txt.mako | 7 +++++++ .../templates/emails/forward_invite_registered.txt.mako | 7 +++++++ website/templates/emails/initial_confirm.txt.mako | 4 ++++ website/templates/emails/invite_default.txt.mako | 7 +++++++ .../templates/emails/invite_preprints_branded.txt.mako | 7 +++++++ website/templates/emails/invite_preprints_osf.txt.mako | 7 +++++++ website/templates/emails/mentions.txt.mako | 9 ++++++++- website/templates/emails/password_reset.txt.mako | 8 +++++++- website/templates/emails/pending_embargo_admin.txt.mako | 6 ++++++ .../templates/emails/pending_embargo_non_admin.txt.mako | 6 ++++++ .../emails/pending_embargo_termination_admin.txt.mako | 6 ++++++ .../pending_embargo_termination_non_admin.txt.mako | 6 ++++++ website/templates/emails/pending_invite.txt.mako | 6 ++++++ website/templates/emails/pending_registered.txt.mako | 6 ++++++ .../templates/emails/pending_registration_admin.txt.mako | 6 ++++++ .../emails/pending_registration_non_admin.txt.mako | 6 ++++++ .../templates/emails/pending_retraction_admin.txt.mako | 6 ++++++ .../emails/pending_retraction_non_admin.txt.mako | 6 ++++++ .../templates/emails/prereg_challenge_rejected.txt.mako | 6 ++++++ website/templates/emails/primary_email_changed.txt.mako | 8 +++++++- .../emails/security_permissions_change.txt.mako | 5 +++++ website/templates/emails/spam_user_banned.txt.mako | 7 ++++++- website/templates/emails/transactional.txt.mako | 2 +- 47 files changed, 287 insertions(+), 10 deletions(-) diff --git a/website/templates/emails/comment_replies.txt.mako b/website/templates/emails/comment_replies.txt.mako index bdb8473b5e7..c6c0a6b0319 100644 --- a/website/templates/emails/comment_replies.txt.mako +++ b/website/templates/emails/comment_replies.txt.mako @@ -1,3 +1,10 @@ ${user} replied to your comment "${parent_comment}" on your project "${title}": ${content}. -${'\t'}To view this on the Open Science Framework, please visit: ${url}. \ No newline at end of file +${'\t'}To view this on the Open Science Framework, please visit: ${url}. + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/comments.txt.mako b/website/templates/emails/comments.txt.mako index ead982ce28f..df6cdc84937 100644 --- a/website/templates/emails/comments.txt.mako +++ b/website/templates/emails/comments.txt.mako @@ -1,3 +1,10 @@ ${user} at ${localized_timestamp}: ${content}. -${'\t'}To view this on the Open Science Framework, please visit: ${url}. \ No newline at end of file +${'\t'}To view this on the Open Science Framework, please visit: ${url}. + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/conference_failed.txt.mako b/website/templates/emails/conference_failed.txt.mako index 1e245290c9e..40d4e2fa260 100644 --- a/website/templates/emails/conference_failed.txt.mako +++ b/website/templates/emails/conference_failed.txt.mako @@ -4,4 +4,10 @@ You recently tried to create a project on the Open Science Framework via email, Sincerely yours, -The OSF Robot \ No newline at end of file +The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/conference_inactive.txt.mako b/website/templates/emails/conference_inactive.txt.mako index 4417c5e9cb3..63520e0e446 100644 --- a/website/templates/emails/conference_inactive.txt.mako +++ b/website/templates/emails/conference_inactive.txt.mako @@ -5,3 +5,9 @@ You recently tried to create a project on the Open Science Framework via email, Sincerely yours, The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/conference_submitted.txt.mako b/website/templates/emails/conference_submitted.txt.mako index 99a0f469a89..1736ee67a95 100644 --- a/website/templates/emails/conference_submitted.txt.mako +++ b/website/templates/emails/conference_submitted.txt.mako @@ -29,3 +29,9 @@ To learn more about the OSF, visit: http://help.osf.io/ Sincerely yours, The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/confirm.txt.mako b/website/templates/emails/confirm.txt.mako index 527792f5daa..a3395f5c0d6 100644 --- a/website/templates/emails/confirm.txt.mako +++ b/website/templates/emails/confirm.txt.mako @@ -9,3 +9,7 @@ ${confirmation_url} The OSF Team Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/confirm_erpc.txt.mako b/website/templates/emails/confirm_erpc.txt.mako index 8ee7506560b..1e8f78e6b4e 100644 --- a/website/templates/emails/confirm_erpc.txt.mako +++ b/website/templates/emails/confirm_erpc.txt.mako @@ -5,3 +5,9 @@ Welcome to the Open Science Framework and the Election Research Preacceptance Co ${confirmation_url} From the team at the Center for Open Science + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/confirm_merge.txt.mako b/website/templates/emails/confirm_merge.txt.mako index 8bec37cd4cc..02893a2dbdf 100644 --- a/website/templates/emails/confirm_merge.txt.mako +++ b/website/templates/emails/confirm_merge.txt.mako @@ -6,4 +6,11 @@ Both ${user.username} and ${email} can be used to log into the account. However, This action is irreversible. To confirm this account merge, click this link: ${confirmation_url}. -If you do not wish to merge these accounts, no action is required on your part. If you have any questions about this email, please direct them to support@osf.io. \ No newline at end of file +If you do not wish to merge these accounts, no action is required on your part. If you have any questions about this email, please direct them to support@osf.io. + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/confirm_preprints_branded.txt.mako b/website/templates/emails/confirm_preprints_branded.txt.mako index fd60fc0af7b..ca5c6e4d5fe 100644 --- a/website/templates/emails/confirm_preprints_branded.txt.mako +++ b/website/templates/emails/confirm_preprints_branded.txt.mako @@ -7,3 +7,9 @@ ${confirmation_url} Sincerely, Your ${branded_preprints_provider} and OSF teams + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/confirm_preprints_osf.txt.mako b/website/templates/emails/confirm_preprints_osf.txt.mako index 419f164fc01..9ec81a1c2a0 100644 --- a/website/templates/emails/confirm_preprints_osf.txt.mako +++ b/website/templates/emails/confirm_preprints_osf.txt.mako @@ -7,3 +7,9 @@ ${confirmation_url} Sincerely, Open Science Framework Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/confirm_prereg.txt.mako b/website/templates/emails/confirm_prereg.txt.mako index 7f448e0fdc3..975c79f31e0 100644 --- a/website/templates/emails/confirm_prereg.txt.mako +++ b/website/templates/emails/confirm_prereg.txt.mako @@ -5,3 +5,9 @@ Welcome to the Open Science Framework and the Preregistration Challenge. To cont ${confirmation_url} From the team at the Center for Open Science + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/confirm_registries_osf.txt.mako b/website/templates/emails/confirm_registries_osf.txt.mako index da0d6f3a192..4bca2c6da2a 100644 --- a/website/templates/emails/confirm_registries_osf.txt.mako +++ b/website/templates/emails/confirm_registries_osf.txt.mako @@ -7,3 +7,9 @@ ${confirmation_url} Sincerely, Open Science Framework Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/contributor_added_default.txt.mako b/website/templates/emails/contributor_added_default.txt.mako index da34d46f9d0..9b0e177cae3 100644 --- a/website/templates/emails/contributor_added_default.txt.mako +++ b/website/templates/emails/contributor_added_default.txt.mako @@ -16,6 +16,13 @@ Sincerely, Open Science Framework Robot +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/contributor_added_preprint_node_from_osf.txt.mako b/website/templates/emails/contributor_added_preprint_node_from_osf.txt.mako index dfc78741d2f..ae7bae7ba26 100644 --- a/website/templates/emails/contributor_added_preprint_node_from_osf.txt.mako +++ b/website/templates/emails/contributor_added_preprint_node_from_osf.txt.mako @@ -17,6 +17,13 @@ Sincerely, Open Science Framework Robot +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. diff --git a/website/templates/emails/contributor_added_preprints_branded.txt.mako b/website/templates/emails/contributor_added_preprints_branded.txt.mako index d040631f86e..68bd33995be 100644 --- a/website/templates/emails/contributor_added_preprints_branded.txt.mako +++ b/website/templates/emails/contributor_added_preprints_branded.txt.mako @@ -16,6 +16,13 @@ Sincerely, Your ${branded_service.name} and OSF teams +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/preprints/${branded_service._id} to learn about ${branded_service.name} or https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email support+${branded_service._id}@osf.io diff --git a/website/templates/emails/contributor_added_preprints_osf.txt.mako b/website/templates/emails/contributor_added_preprints_osf.txt.mako index b27bcdd3b4b..720cd1be5df 100644 --- a/website/templates/emails/contributor_added_preprints_osf.txt.mako +++ b/website/templates/emails/contributor_added_preprints_osf.txt.mako @@ -16,6 +16,13 @@ Sincerely, Open Science Framework Robot +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/email_contributors_of_registrations.txt.mako b/website/templates/emails/email_contributors_of_registrations.txt.mako index f27e83c01bd..315e07b2889 100644 --- a/website/templates/emails/email_contributors_of_registrations.txt.mako +++ b/website/templates/emails/email_contributors_of_registrations.txt.mako @@ -15,6 +15,11 @@ Please let us know if you have any questions or would like help addressing your Regards, The OSF Team + Center for Open Science +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + We may send you occasional Service-related emails that you may not opt-out of (e.g. changes or updates to features of our Services that have security or privacy implications, technical and security notices, account verification); this is one of those emails. diff --git a/website/templates/emails/email_removed.txt.mako b/website/templates/emails/email_removed.txt.mako index 7205b72093b..96bd2a5fc3c 100644 --- a/website/templates/emails/email_removed.txt.mako +++ b/website/templates/emails/email_removed.txt.mako @@ -7,4 +7,10 @@ If you did not request this action, let us know at contact@cos.io. Sincerely yours, -The OSF Robot \ No newline at end of file +The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/external_confirm_create.txt.mako b/website/templates/emails/external_confirm_create.txt.mako index 40830dbf74d..ba7f7a12b49 100644 --- a/website/templates/emails/external_confirm_create.txt.mako +++ b/website/templates/emails/external_confirm_create.txt.mako @@ -9,3 +9,7 @@ ${confirmation_url} The OSF Team Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/external_confirm_link.txt.mako b/website/templates/emails/external_confirm_link.txt.mako index 838de3934ae..cbc539b9610 100644 --- a/website/templates/emails/external_confirm_link.txt.mako +++ b/website/templates/emails/external_confirm_link.txt.mako @@ -9,3 +9,7 @@ ${confirmation_url} The OSF Team Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/external_confirm_success.txt.mako b/website/templates/emails/external_confirm_success.txt.mako index bdb54ab3307..258d7ec68f7 100644 --- a/website/templates/emails/external_confirm_success.txt.mako +++ b/website/templates/emails/external_confirm_success.txt.mako @@ -5,3 +5,7 @@ Congratulations! You have successfully linked your ${external_id_provider} accou The OSF Team Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/file_operation_failed.txt.mako b/website/templates/emails/file_operation_failed.txt.mako index 57ede6ce1a8..82d525b5175 100644 --- a/website/templates/emails/file_operation_failed.txt.mako +++ b/website/templates/emails/file_operation_failed.txt.mako @@ -3,3 +3,9 @@ Hello, An error has occurred, and the ${'folder' if source_path.endswith('/') else 'file'} from ${source_node.title} on The Open Science Framework was not successfully ${'moved' if action == 'move' else 'copied'}. Please log in and try this action again. If the problem persists, please email support@osf.io. The Open Science Framework Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/file_operation_success.txt.mako b/website/templates/emails/file_operation_success.txt.mako index 915a45b779e..b87a75cc549 100644 --- a/website/templates/emails/file_operation_success.txt.mako +++ b/website/templates/emails/file_operation_success.txt.mako @@ -3,3 +3,9 @@ Hello, The ${'folder' if source_path.endswith('/') else 'file'} "${source_path.strip('/')}" has been successfully ${'moved' if action == 'move' else 'copied'} from ${source_addon} in ${source_node.title} to ${destination_addon} in ${destination_node.title}. The Open Science Framework Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/forgot_password.txt.mako b/website/templates/emails/forgot_password.txt.mako index dfda8005975..816e2749eae 100644 --- a/website/templates/emails/forgot_password.txt.mako +++ b/website/templates/emails/forgot_password.txt.mako @@ -1,3 +1,10 @@ Follow this link to reset your password ${reset_link} + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/forward_invite.txt.mako b/website/templates/emails/forward_invite.txt.mako index 818a58c547b..aac2c4fde60 100644 --- a/website/templates/emails/forward_invite.txt.mako +++ b/website/templates/emails/forward_invite.txt.mako @@ -25,4 +25,11 @@ Sincerely, The OSF Team +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ or https://cos.io/ for information about the Open Science Framework and its supporting organization, the Center for Open Science. Questions? Email contact@osf.io. diff --git a/website/templates/emails/forward_invite_registered.txt.mako b/website/templates/emails/forward_invite_registered.txt.mako index 1c6939ce9ac..622ceb184e2 100644 --- a/website/templates/emails/forward_invite_registered.txt.mako +++ b/website/templates/emails/forward_invite_registered.txt.mako @@ -21,4 +21,11 @@ Sincerely, The OSF Team +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ or https://cos.io/ for information about the Open Science Framework and its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/initial_confirm.txt.mako b/website/templates/emails/initial_confirm.txt.mako index b0e756b212a..be7b832e28d 100644 --- a/website/templates/emails/initial_confirm.txt.mako +++ b/website/templates/emails/initial_confirm.txt.mako @@ -9,3 +9,7 @@ ${confirmation_url} The OSF Team Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/invite_default.txt.mako b/website/templates/emails/invite_default.txt.mako index fe087f6d4d8..340c2c71d37 100644 --- a/website/templates/emails/invite_default.txt.mako +++ b/website/templates/emails/invite_default.txt.mako @@ -22,6 +22,13 @@ Sincerely, Open Science Framework Robot +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/invite_preprints_branded.txt.mako b/website/templates/emails/invite_preprints_branded.txt.mako index c2f9a5b4b51..6fc52744d74 100644 --- a/website/templates/emails/invite_preprints_branded.txt.mako +++ b/website/templates/emails/invite_preprints_branded.txt.mako @@ -22,6 +22,13 @@ Sincerely, Your ${branded_service.name} and OSF teams +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/preprints/${branded_service._id} to learn about ${branded_service.name} or https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email support+${branded_service._id}@osf.io diff --git a/website/templates/emails/invite_preprints_osf.txt.mako b/website/templates/emails/invite_preprints_osf.txt.mako index 67c59c8417a..4b187a53701 100644 --- a/website/templates/emails/invite_preprints_osf.txt.mako +++ b/website/templates/emails/invite_preprints_osf.txt.mako @@ -22,6 +22,13 @@ Sincerely, Open Science Framework Robot +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + + Want more information? Visit https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/mentions.txt.mako b/website/templates/emails/mentions.txt.mako index ead982ce28f..df6cdc84937 100644 --- a/website/templates/emails/mentions.txt.mako +++ b/website/templates/emails/mentions.txt.mako @@ -1,3 +1,10 @@ ${user} at ${localized_timestamp}: ${content}. -${'\t'}To view this on the Open Science Framework, please visit: ${url}. \ No newline at end of file +${'\t'}To view this on the Open Science Framework, please visit: ${url}. + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/password_reset.txt.mako b/website/templates/emails/password_reset.txt.mako index 700957fd84b..f83ecb6497d 100644 --- a/website/templates/emails/password_reset.txt.mako +++ b/website/templates/emails/password_reset.txt.mako @@ -10,4 +10,10 @@ If you need additional help or have questions, let us know at contact@cos.io. Sincerely yours, -The OSF Robot \ No newline at end of file +The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/pending_embargo_admin.txt.mako b/website/templates/emails/pending_embargo_admin.txt.mako index 6e2ebb98a33..f013b23683c 100644 --- a/website/templates/emails/pending_embargo_admin.txt.mako +++ b/website/templates/emails/pending_embargo_admin.txt.mako @@ -21,3 +21,9 @@ private and enter into an embargoed state. Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_embargo_non_admin.txt.mako b/website/templates/emails/pending_embargo_non_admin.txt.mako index 34922b018a0..cdd0eb3bfdd 100644 --- a/website/templates/emails/pending_embargo_non_admin.txt.mako +++ b/website/templates/emails/pending_embargo_non_admin.txt.mako @@ -8,3 +8,9 @@ private until it is withdrawn, manually made public, or the embargo end date has Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_embargo_termination_admin.txt.mako b/website/templates/emails/pending_embargo_termination_admin.txt.mako index d57d39657fe..67c5802f06b 100644 --- a/website/templates/emails/pending_embargo_termination_admin.txt.mako +++ b/website/templates/emails/pending_embargo_termination_admin.txt.mako @@ -13,3 +13,9 @@ To cancel this change, click the following link: ${disapproval_link}. Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_embargo_termination_non_admin.txt.mako b/website/templates/emails/pending_embargo_termination_non_admin.txt.mako index 016913db5cb..fa638c37d19 100644 --- a/website/templates/emails/pending_embargo_termination_non_admin.txt.mako +++ b/website/templates/emails/pending_embargo_termination_non_admin.txt.mako @@ -7,3 +7,9 @@ If approved, the embargo will be ened and the registration and all of its compon Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_invite.txt.mako b/website/templates/emails/pending_invite.txt.mako index 6a55199c86b..bd98195e375 100644 --- a/website/templates/emails/pending_invite.txt.mako +++ b/website/templates/emails/pending_invite.txt.mako @@ -12,6 +12,12 @@ Sincerely, The OSF Team +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + More information? Visit https://osf.io/ and https://cos.io/ for information about the Open Science Framework and its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/pending_registered.txt.mako b/website/templates/emails/pending_registered.txt.mako index a6594db719a..dc22b314449 100644 --- a/website/templates/emails/pending_registered.txt.mako +++ b/website/templates/emails/pending_registered.txt.mako @@ -12,6 +12,12 @@ Sincerely, The OSF Team +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + More information? Visit https://osf.io/ and https://cos.io/ for information about the Open Science Framework and its supporting organization, the Center for Open Science. Questions? Email contact@osf.io diff --git a/website/templates/emails/pending_registration_admin.txt.mako b/website/templates/emails/pending_registration_admin.txt.mako index 3d9ec7a9b3c..8ace0cfd92b 100644 --- a/website/templates/emails/pending_registration_admin.txt.mako +++ b/website/templates/emails/pending_registration_admin.txt.mako @@ -20,3 +20,9 @@ automatically approved and made public. This operation is irreversible. Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_registration_non_admin.txt.mako b/website/templates/emails/pending_registration_non_admin.txt.mako index ebd4ea89f05..a9434676789 100644 --- a/website/templates/emails/pending_registration_non_admin.txt.mako +++ b/website/templates/emails/pending_registration_non_admin.txt.mako @@ -8,3 +8,9 @@ public until it is withdrawn. Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_retraction_admin.txt.mako b/website/templates/emails/pending_retraction_admin.txt.mako index 45fe982c105..24d7f7527ad 100644 --- a/website/templates/emails/pending_retraction_admin.txt.mako +++ b/website/templates/emails/pending_retraction_admin.txt.mako @@ -16,3 +16,9 @@ Note: Clicking the disapproval link will immediately cancel the pending withdraw Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/pending_retraction_non_admin.txt.mako b/website/templates/emails/pending_retraction_non_admin.txt.mako index 7bbe0a8cf2c..4275eb2a12e 100644 --- a/website/templates/emails/pending_retraction_non_admin.txt.mako +++ b/website/templates/emails/pending_retraction_non_admin.txt.mako @@ -7,3 +7,9 @@ If approved, the registration will be marked as withdrawn. Its content will be r Sincerely yours, The OSF Robots + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/prereg_challenge_rejected.txt.mako b/website/templates/emails/prereg_challenge_rejected.txt.mako index 3f8ce9af5eb..024f6e066cb 100644 --- a/website/templates/emails/prereg_challenge_rejected.txt.mako +++ b/website/templates/emails/prereg_challenge_rejected.txt.mako @@ -11,3 +11,9 @@ Prereg Challenge administrators and reviewers review the submitted study design Sincerely, The team at the Center for Open Science + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md diff --git a/website/templates/emails/primary_email_changed.txt.mako b/website/templates/emails/primary_email_changed.txt.mako index 934e4d05395..e4d470fd422 100644 --- a/website/templates/emails/primary_email_changed.txt.mako +++ b/website/templates/emails/primary_email_changed.txt.mako @@ -6,4 +6,10 @@ If you did not request this action, let us know at contact@cos.io. Sincerely yours, -The OSF Robot \ No newline at end of file +The OSF Robot + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/security_permissions_change.txt.mako b/website/templates/emails/security_permissions_change.txt.mako index 53d8c2cae44..33034396c03 100644 --- a/website/templates/emails/security_permissions_change.txt.mako +++ b/website/templates/emails/security_permissions_change.txt.mako @@ -17,6 +17,11 @@ Please do let us know if you have any questions or would like help in addressing Regards, The OSF Team + Center for Open Science +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + Note: In system-wide security messages, we will not ask you to click on links or ask you for information - passwords or otherwise. diff --git a/website/templates/emails/spam_user_banned.txt.mako b/website/templates/emails/spam_user_banned.txt.mako index 6046938fb97..0ff954353bd 100644 --- a/website/templates/emails/spam_user_banned.txt.mako +++ b/website/templates/emails/spam_user_banned.txt.mako @@ -5,4 +5,9 @@ Your account on the Open Science Framework has been flagged as spam and disabled Regards, The OSF Team -Center for Open Science \ No newline at end of file + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md \ No newline at end of file diff --git a/website/templates/emails/transactional.txt.mako b/website/templates/emails/transactional.txt.mako index 5b89c5afd1c..ae46dd8faad 100644 --- a/website/templates/emails/transactional.txt.mako +++ b/website/templates/emails/transactional.txt.mako @@ -2,4 +2,4 @@ Recent Activity: ${message} -From the Open Science Framework +From the Open Science Framework \ No newline at end of file From 17cb77490e7a94dfaa2c5c3b114fb58b57854013 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 23 Aug 2017 12:31:42 -0400 Subject: [PATCH 004/108] linkify wiki markdown --- website/static/js/markdown.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/static/js/markdown.js b/website/static/js/markdown.js index 957a756a640..aacf431f181 100644 --- a/website/static/js/markdown.js +++ b/website/static/js/markdown.js @@ -32,8 +32,9 @@ var oldMarkdownList = function(md) { }; // Full markdown renderer for views / wiki pages / pauses between typing -var markdown = new MarkdownIt('commonmark', { - highlight: highlighter +var markdown = new MarkdownIt({ + highlight: highlighter, + linkify: true }) .use(require('markdown-it-video')) .use(require('@centerforopenscience/markdown-it-toc')) @@ -45,7 +46,7 @@ var markdown = new MarkdownIt('commonmark', { // Fast markdown renderer for active editing to prevent slow loading/rendering tasks -var markdownQuick = new MarkdownIt(('commonmark'), { }) +var markdownQuick = new MarkdownIt({ linkify: true }) .use(require('markdown-it-sanitizer')) .disable('link') .disable('image') @@ -55,7 +56,7 @@ var markdownQuick = new MarkdownIt(('commonmark'), { }) .disable('strikethrough'); // Markdown renderer for older wikis rendered before switch date -var markdownOld = new MarkdownIt(('commonmark'), { }) +var markdownOld = new MarkdownIt({ linkify: true}) .use(require('markdown-it-sanitizer')) .use(insDel) .enable('table') From c36d735dce918e332df7ee42331d12af0b687915 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Thu, 24 Aug 2017 12:01:18 -0400 Subject: [PATCH 005/108] Change spacing and word-wrap to accommodate new bibliographic contributor checkbox --- .../project/modal_add_contributor.mako | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/website/templates/project/modal_add_contributor.mako b/website/templates/project/modal_add_contributor.mako index c2a05274324..6a5a8a68d53 100644 --- a/website/templates/project/modal_add_contributor.mako +++ b/website/templates/project/modal_add_contributor.mako @@ -40,7 +40,7 @@
-
+
Results Add all @@ -71,7 +71,7 @@ - +
@@ -145,7 +145,7 @@
-
+
Adding Remove all @@ -155,8 +155,17 @@ - - + + + +
NameName + Bibliographic Contributor + + Permissions - + (unregistered) + + +
@@ -71,7 +71,7 @@ -
+
@@ -152,7 +152,7 @@ - +
@@ -189,7 +189,7 @@ - -
+ Date: Fri, 1 Sep 2017 14:46:35 -0400 Subject: [PATCH 011/108] Add social links to prereg email --- website/static/img/fa-envelope-blue.png | Bin 0 -> 2780 bytes website/static/img/fa-facebook-blue.png | Bin 0 -> 1385 bytes website/static/img/fa-linkedin-blue.png | Bin 0 -> 2448 bytes website/static/img/fa-twitter-blue.png | Bin 0 -> 3117 bytes website/templates/base.mako | 2 +- .../emails/prereg_challenge_accepted.html.mako | 16 +++++++++++++++- 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 website/static/img/fa-envelope-blue.png create mode 100644 website/static/img/fa-facebook-blue.png create mode 100644 website/static/img/fa-linkedin-blue.png create mode 100644 website/static/img/fa-twitter-blue.png diff --git a/website/static/img/fa-envelope-blue.png b/website/static/img/fa-envelope-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..75e8ccf8b988aebf477eb36c2451b2b8e6b37759 GIT binary patch literal 2780 zcmeHJdpHvcAKuK2VTLf*8Io2pw{p*A$z?9nq$v!um^jK%F15K1C4_B7YO-dX=t87U zhvJypTtjqXr&i`#os@E^)8F6o{qa4|_wRXr@ALfL-@otk{+{dv#MEr7`8JE~=p8$ESz2MNZEWrAckgj< zbaHlab=&Li>4n|rz2DbAATTHxA3``7c8GZRNJL~*bWALnax^|6@mSI?DaTW3X&EO^ zWo4f}L(k1CC}b9wo-I3HQCU^ZVqd&eclippp^?WI+_>3t>rQK1`~A*GUENQ5`-D$L z1B1h(V-u58e@;tYOW(=n7M9+B{PcPC+xq53*cBxJ0CLS2>p?*1E_I)a6|EXVKCG^0 zu#WgBqPXPZ>$mRtwVC54UDBT7T*ua`nzkQ^9S`8c>Z0p*i}QVyx-Uv9nJy8_(?4GI z#F}K(XRU2o-e1p-vN}`~NBmEO^zK-#eK2MeGoS=+F`U``3^H8h`bu>CN9GgbnzNN{4D#IAHhk4m1v88E@9L{jj+Y3P(odj)#w@c{-r z?PBq^@7|5!DhgU5H|-HCj$_n4037mjre;?nVNs55)x<%{c$8F3g&?WVQ;;s_+%#ps zQ6~W7gT_4L8>u-mF4aRNxJ`Qn-}b4)e++hoA=p$X;)jFB@om|-M#@dV8vg|nV9*$I z7eEMZG0NMYOGrd2{N~^0XDyrvYSjDCa}aHh402H8kEpub%|(m5Q(63A!$^y=;5s2z zk$v^1c1>dsKwR6HwJ$IM27i`CjkDNE9L3{n)&58{Ob!}Q1Pd&cL%obHRIKsX9U@4xvzNdx$75YkNp<* zRsQSqJP!PA{ZZ{EeF#3mxn8cm_!#|I8k1=9Q2mZpv>WJAilrL&@eUDmVw;E5xm%|T z)hRH}swYY`L3pBm5unIX=5F^{yRYM(Zh6uhj3nizt+CCz1J5wc7@hn&Me>ZQg01!_ zBSy4%-rFu>(M?-%1;UKl*yocj%o58~c}z*20=L)IAa5;Ys zt|_Azxhi`IO zx9PVdcGCGPu)cf}7<=j{Lf=AmqflrdK?E=4#YgLaugL08!Ntk577Ij`ik2Fa;4djC z?DrN2!6Q|7yY@O8%;j(-)<4scsxa;;-QawrA8#9ESfo@mNay95PhTQ**mN%#_I;tF zUb+X_Emp1z+bYhFkI}SUJ`)G8o{ll;>Uoav5fzDUSsD0<0?ie0Lt+gkuiYlkY1V_V zVX7%_j=rO1GOK9&t_sOL7i|v39f?)PDtEHgbDkONMGf{ad=ZBr_-P`=Jpzb*zhbLI z3VF1H9v{jyQvN01;)lTnIrHloy4V8NT+_k@e8Gk0tglYk(PfMliRxxuF3s^` z)eRQ)ERv~RTkL}hM5hb_jVB&O*fE`VOS^G8O!*A zNh9g4vAhW5-a4%{9xD{JQ2_64D^gSxwm_e6zD&X(3AW z=RuU7`ca-mjrlKbH7+_H%fJvC=Dl$ND}rKFYP!D0GL(RG@k>=DmI zD0(^J@VKpAm0S=u=QaeoO6{?2G#8?B?GL~Q|AxB)GP&a(M|yX_#vxu;Nt@kTz07Bh zfkE`?uT5K;(MgSa+|OfockjDox}qrKlma`3dYYKAS@*=@~97P(FLKXdcj*Di+{+F{+95o9(^>R&B*B_S$S+K z-($cPoi-u44JHVnK{+1PU*71B`V?5|yUhL`ke0k=jg{=KPU!E$eD@ZNNc#Lm9_TwW zq-)<+`Pjo++!LqQ%b~O+8)k3QcZOqg^Ay9PWj^oBpE3k#Kv&lB@pH4l<-Y}I$_9W&011Kfk!vFvP literal 0 HcmV?d00001 diff --git a/website/static/img/fa-facebook-blue.png b/website/static/img/fa-facebook-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..d85d96da8877e064c065f07e53439b454d193cdb GIT binary patch literal 1385 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fn={z$e5N$ThCoK8olW0!(Fm z<-l;SE(!7rW?*DuW?^Mx=iuVz;pGz$6cH7dkdl#=Q&3b=QPa}aF)%bTHZirZvaxk= za(4Cd_VEu24U3M6jY~>SNz2U5FDNc4t*ET7t8Z%S=mSDUbbSxmaW@& z?%8+X;NcTz&R@QM>(0FgkDfex@$&WC_a8rh{r>a!-+4QfRT&tVH9cJ%Ln>~)y?ZlA zI8eg%;bq=Q0*X9cZoN$%56&!bbQE-FQ5Rts3Amu4)z#r};E(?M-tA#eO_omBe7&k6 za zsGN!o=XO@{cnB?DLIJ}JaLfS2ckCQA8EXZVDvh4`tF<{w&-- zWffb)Vb0^z)pos#<=|DB75|3gndyy{ET6CY?JD^7aDTm>?1g_f>i;|WI{c13_WvLM z%U|k09bZ%g{El7z{@=|F-*(r^d8q^5{td@2Fr@wYw!UTC z{mOZ4)gm$n7Rfv07ciWzWBB=i!6v@BKxJvO6u3XtuTJY@c0#j_*F)$tBl`PGB3Hxs8rbC?gS=w;>6+`VShT~TlXI*7{ zb%e1FD7wh%z!Iwi8>|ihrRP{VG+R07@NEFf#jR$(6U;6V!2TkDJtLIeV>R=R*nnM| z{BS;nKw<}%5p5si8D^Yi=(`#SG)-nZ9zpYwj5_c`xOUvC#hf))V) zpy=jG^9KOh2q7ScY`B=}U^)P>uCFKEX(QctuJIrHM}hyVfcfX_A2&{<{*1qO0Dxcw zCnJlOlP4%_QdCmjyhTM-O?|6|<~A+u?L-}2J%gQwM#d()c7J1Pw#S^b*J7XLw^r6R z`)$c~-yN{0I5<*iPR=fF?jD|A-afv5{`7-^LBWTPgd9B<`h8e;z6?QgRBKcHvTbMrQVvYk38QMa8#nmvZlw-LI&u`svZnPwE>Q`Asct*wbITx_kQi z{}^~VJTfX67ruV`_xqW-4<8qo#h=$Cso#%E0gz?7(H!UrqjP^{hSCqJ;peS8`q9pc zi%&Fhn{GYvg0oT`QcbnG|0Bx-?pfvhaz}cefBKf-u$?A*X2(7wYfBFgs1y3g;#O;R zKfm2x7}77^*K(6R(JGEE&iaa;QJkTU4W~6_{@vu-`JiHg>KD$pp+naAn1zMI73Mjj zxf;J<!379_1Qm`wgwyL#q}Zv$jH9D~C4Ste4yGZ0X*mzl-^ z^qJyQ4Sk>lgARS5X5?I&1#IMjXEyLn@O-Qf`?>>0AdX=Vp>856Y@iRe;XrH_ps9i= z6-X|EykkYIs!tHZJ7B%;d6p(-~J@(iKnfeaV1w4k$u`-z~yDw3lZej;Nza_Aqh z64$FN?+3bcF*yP-r$Rp9q`!I7q{^1=(J%gz#)S1gz@Pcl-jp9GEk1v7f`&<_)-H~D zzR-rLv5oBo)qYpzZrqaiI7QI{T4uea!%3D;+stt7lr`P@=pohN=GZmr z&80EvzGOao>qLuK5+9n|X>#}Go%j^{^)Tq|gJkX5n%>cS4=eu%36^bl$tC+`4AmPB zzA>yB;$;Bs!v2#-ZZ9|9X+1u;`UZRdEQKzOdVP9jyWMlM{4w6&PSotqaOZ8-MZ2Gx%akZ>y_4;_<$iz zza>r0d|PEe&(LTPr}}N1uiMV`K zEmQq!G13umVizUVci=IHw4jU{;Gzi9aHkQUbLVNO_t#31M2k9RM}<2u}b zIj4X<^9ou?F^Xe4^!K|NOnWEbvAOcbM;|k1gEyPc&BLh2ju}P9kB7IKwFR9+?_Pa% zHVR_ndPA=hx$$+&C*Op{$c6rlMqpVt{LY0UCt9q>Tn%S=UW)u|>56ozwFFQn*G3lQ!VB^Fi0^iVTR~ zpu;CE|9mIS!Ye~Sg_#a;LANZ6kk|yD%HkciLhVd3x!4w?5uoxIbZfXa_Q-Bdm!Y&d zeyL^xC>XpxkgqfXUc3W_!p@mIMQ~n+zB9HZb|f%^u{N9*H+9qq5%vrOoP@s8P)^6|?L<^unb#NNpB!g|&3)tw&U|M?FWGq$$64f^9OcB^G zfKGj-*nOqkA)_7;3BQ7pv11}2BY+5eL@f^g?=B*v9cC!`_7R1EjVkkz0v6unjS?%D zjJXJ)9v)I~8oyFR#*78f?ytkB0;r0I&`;wlCzX7Ai9!+^H4s44JY?uJ-lm7hO(jzd z*r<^R{4Rj5ZIqr8g*(}3fB-toL+VfAqo$Nt#bj)o2%O*}B~wZ-`ia~NWQs8xT@*k~ zJS3@;DAZ@88(S3ektG&>rG$*Bo5Q|-8<;G%2j_MUTKA+oAK&`!AMU BMJfOQ literal 0 HcmV?d00001 diff --git a/website/static/img/fa-twitter-blue.png b/website/static/img/fa-twitter-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..bed6cee7c473c2df369be16926593ceb6d02432f GIT binary patch literal 3117 zcmeHJXEYlM1C7|TR_v5m2}+B2HOgyji4kJAHhET4C8|52EVN;q4{6@+ra$7?UVdQ_rKGH)vienReM4jO->vPPZ@YT>1_p|p)3bB) z3rj1XSJyYUzS8!-9UPsUo?pCE0-XQ=ET2sD5!OP&w5~Wk{v#ms{@z_|$lRJED?hv; z59wTPp(=hxkbL#Ns9_2@as`9J#PibjVTtH+=TL@1;Ro^g(%tn6ZOdaM`Zq2RY}Usm1n z0voIpu51vzss=M#erKAK8r{ph(y1Yr~6rm9RC+3Gloh zaVIo{h>LhsTHu(fs5VFlu}QhF^DHJM5xN`|RC1((s#MMyxK#2ZgWmWHG&MZ{33}ap zDKg!^XK~B1RogRI5XE`o+rV#PuuctsJk&&-*fDFl)`xaP{ZB= zAf>SS=sNM)9-re{0~7!7d46t>0}awG790G7-7y*8$42>7_UMS zx575b;ilf=0gCL*vtnpkmTTqK;u zxbC@C?|?)Hmg_z;l0k+nslmGCBUYgSIL|HRh%*tAeEQFjdcusvj;)!nJ4Hy}CD7-_ z*{$JE3xhEo#13+PB@4Y-743_tkgGz>G=}RIjl!_ez4_{a2lSmG$00Z-qarFXP_7FK zyp)dHUfFZ!;g}jtX=gm&DWs*M_HT|ZSa|%TT63g+j^$$gAWbe_6k*?wS%8Ktt?1o# z<<9ki$G9ao`@HV-3Ww#?HR<%zC3)1cP`6$HwPMxp9M2u!tqCeq&}33@C5XC43gi11 zZ*y4oT84>7t#uYkrFseZUNH)4;}D=?@VyuzLsp|^#9J%&t=Ki8Q6Z82AV?KH-26kV z+Zr`?HPF5+o9hRl=Y4@PRH_z92i{jUl<-FXbg(Z@{r)JHXR*j~1LYcL0b5i`*pW>9 zSl$B^gnhhUd@o#f;S^dwYxK?;q50K&=+|Y$b0XQFXEIAK?KmbSy^bue4BA?wR;^;e z4w-r{a7kU>1Ruw-c%X1)p?)d7kPTQhz_0vfw5?TMGNYm0NWy|c2}J6Zpl6iaz?;tA7#gtuwWL$pBaJDZVTBB{mEm3uGT>?Bes6V zy$Sg{dt~-vwTQrC*KS!y*u9nt7O8l2lDa8NuTQo9#doU+H`bvEPSxjBLV5p*VB)lL zuNQKHRkhb$69V>Q1ugOm~m#m&L_X-|{i@Oc$ z>Wnw~VOdTTQ4>cxL}k#<-r3T|kQ5Nss^5Hz)8nbS^~RjXz}EdBSJP*gZ5Sd`4`n%- z8pqQ#U8it_$=Pcasd_IY2k%MBQwawJBmH)028<45sW0 zl#nS1{lONPJtdBbV95P~WwF302Z5EHl*-eKo*5SnOexnSAox^dvo`5|A=ZI*TaOW* zkOhD=0NA}g4A&A>rT%u+smce!0JBM(a z6aJmXR9`KQw(7#6`QQ&VS!@a;rnqj;Q~#%oa=ZolSdELgi)2tN(y6m)!1O`cZTrhF^=7(a#TjJufsU%g(E#0 zxq?ClYp_s;JN3#Rv+BxxA`DFz>&aY|hRr66@T;7%$!IC%ClNi!)dJhGI~2TVK|;<6 zhFrr|G58B+u=N>))}M3JTp>Wn`8G}CtjpWzt`=|yzV{E9#FOb@&7u$`vFlixjOLm@ z!zkP6Jk?~wpCm~EIL1H{`6OHp{p3G?#;beWrUwP!L~<~JQCwO_>N%iA3udtJr47<@!URuUvm`q5 zco>8X*;@Xc*v%!Cw=X1_jTA(eyr< - + diff --git a/website/templates/emails/prereg_challenge_accepted.html.mako b/website/templates/emails/prereg_challenge_accepted.html.mako index a513a5b4ee2..f182700443f 100644 --- a/website/templates/emails/prereg_challenge_accepted.html.mako +++ b/website/templates/emails/prereg_challenge_accepted.html.mako @@ -1,18 +1,32 @@ +<% from website import settings %> + Dear ${user.fullname},

We are happy to let you know that your research plan has been verified for completeness and registered on the OSF at the following URL: ${registration_url}.

-What happens now? +What's next? +
+
+Reach out to others: The Prereg Challenge seeks to change how research is conducted for the better. Take part in the movement by getting the word out and telling your peers about why this practice matters. Consider emailing your department, your professional society, or other outlets. Let them know you preregistered your research and tell them to check out the Prereg Leaderboard. Share it now! +
+
+Share on social media: Tell your friends through twitter + LinkedIn +facebook + or email

Conduct your study: It's time to start your study and its analysis exactly as specified in your preregistration.
+
Publish your study: In order to remain eligible for the Preregistration Challenge, any deviations from your preregistration (e.g. sample size, timing, analysis) must be documented and appear in the final publication. Any additional analyses must be noted separately from the registered, confirmatory, hypothesis testing analyses. Such new analyses must be described as hypothesis generating or exploratory tests. You must also refer to your preregistration in the publication by using its URL: ${registration_url}. Publication must occur in an eligible journal.
+
Submit your article for review: We will review your final, published article once you submit it on the OSF. We will verify that your study and its analyses were conducted as specified in your preregistration. In order to avoid any unintended oversights, please reach out to us (prereg@cos.io) and refer to our guidelines and FAQ on our website when writing up your results.
+
Receive the prize! $1,000 rewards will be distributed to eligible entrants according to the schedule on our website.

From 56cd00404ed4177c21c3924c7d2d9142ea4f383f Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 6 Sep 2017 14:08:09 -0400 Subject: [PATCH 012/108] Add test behavior to ensure users aren't re-imported to nodes they are already on. --- tests/test_contributors_views.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_contributors_views.py b/tests/test_contributors_views.py index a657bf5585d..e0e4722aca4 100644 --- a/tests/test_contributors_views.py +++ b/tests/test_contributors_views.py @@ -123,12 +123,30 @@ def test_get_contributors_from_parent(self): auth=self.auth, visible=False, ) - self.project.save() component = NodeFactory(parent=self.project, creator=self.user) + + user_already_on_component = AuthUserFactory() + component.add_contributor( + user_already_on_component, + auth=self.auth, + visible=True, + ) + self.project.add_contributor( + user_already_on_component, + auth=self.auth, + visible=True, + ) + + self.project.save() + component.save() + url = component.api_url_for('get_contributors_from_parent') res = self.app.get(url, auth=self.user.auth) # Should be all contributors, client-side handles marking # contributors that are already added to the child. + + ids = [contrib['id'] for contrib in res.json['contributors']] + assert_not_in(user_already_on_component.id, ids) assert_equal( len(res.json['contributors']), 2, From 693d0bb25f6bce992f9e41d2f71984b85d75d000 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 6 Sep 2017 14:09:24 -0400 Subject: [PATCH 013/108] fix behavior to include all contributors on node, not just visible ones. --- website/project/views/contributor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/project/views/contributor.py b/website/project/views/contributor.py index 95f2e81b39f..196c29874e7 100644 --- a/website/project/views/contributor.py +++ b/website/project/views/contributor.py @@ -128,7 +128,7 @@ def get_contributors_from_parent(auth, node, **kwargs): contribs = [ profile_utils.add_contributor_json(contrib, node=node) - for contrib in parent.contributors if contrib not in node.visible_contributors + for contrib in parent.contributors if contrib not in node.contributors ] return {'contributors': contribs} From 344a457fa24d812e626253702e3f572e0d6b51ad Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 6 Sep 2017 15:52:45 -0400 Subject: [PATCH 014/108] re-wrap text, add keep-all class. --- website/static/css/style.css | 5 +++++ website/templates/project/modal_add_contributor.mako | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/website/static/css/style.css b/website/static/css/style.css index 6dcb742e6c5..85e65a02592 100644 --- a/website/static/css/style.css +++ b/website/static/css/style.css @@ -422,6 +422,11 @@ a { word-break: break-all; } + +.keep-all { + word-break: keep-all; +} + select { word-wrap: normal; } diff --git a/website/templates/project/modal_add_contributor.mako b/website/templates/project/modal_add_contributor.mako index 44a0f1c8a88..d66ada4da49 100644 --- a/website/templates/project/modal_add_contributor.mako +++ b/website/templates/project/modal_add_contributor.mako @@ -71,8 +71,8 @@
- + +
@@ -153,7 +153,7 @@ - + @@ -189,7 +189,7 @@ -
Name + Date: Mon, 11 Sep 2017 10:35:17 -0400 Subject: [PATCH 015/108] Remove private project information from pointers in api; anonymize inaccessible pointer logs --- api/logs/serializers.py | 14 ++++++- api_tests/nodes/views/test_node_logs.py | 51 +++++++++++++++++++++++-- website/static/js/logTextParser.js | 16 ++------ 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/api/logs/serializers.py b/api/logs/serializers.py index 199a266cd98..53a05e25942 100644 --- a/api/logs/serializers.py +++ b/api/logs/serializers.py @@ -70,7 +70,7 @@ class NodeLogParamsSerializer(RestrictedDictSerializer): params_node = ser.SerializerMethodField(read_only=True) params_project = ser.SerializerMethodField(read_only=True) path = ser.CharField(read_only=True) - pointer = ser.DictField(read_only=True) + pointer = ser.SerializerMethodField(read_only=True) preprint = ser.CharField(read_only=True) preprint_provider = ser.SerializerMethodField(read_only=True) previous_institution = NodeLogInstitutionSerializer(read_only=True) @@ -112,6 +112,18 @@ def get_params_project(self, obj): return {'id': project_id, 'title': node['title']} return None + def get_pointer(self, obj): + print self.context + user = self.context['request'].user + pointer = obj.get('pointer', None) + if pointer: + pointer_node = AbstractNode.objects.get(guids___id=pointer['id']) + if not pointer_node.is_deleted: + if pointer_node.is_public or (user.is_authenticated and pointer_node.has_permission(user, osf_permissions.READ)): + pointer['title'] = pointer_node.title + return pointer + return None + def get_contributors(self, obj): contributor_info = [] diff --git a/api_tests/nodes/views/test_node_logs.py b/api_tests/nodes/views/test_node_logs.py index c756bb9cc1f..f9292bacd19 100644 --- a/api_tests/nodes/views/test_node_logs.py +++ b/api_tests/nodes/views/test_node_logs.py @@ -43,8 +43,8 @@ def NodeLogFactory(self): return ProjectFactory() @pytest.fixture() - def pointer(self): - return ProjectFactory() + def pointer(self, user): + return ProjectFactory(creator=user) @pytest.fixture() def private_project(self, user): @@ -141,14 +141,57 @@ def test_remove_addon(self, app, user, user_auth, public_project, public_url): assert len(res.json['data']) == public_project.logs.count() assert res.json['data'][API_LATEST]['attributes']['action'] == 'addon_removed' - def test_add_pointer(self, app, user_auth, public_project, pointer, public_url): + def test_pointers(self, app, user, user_auth, contrib, public_project, pointer, public_url): public_project.add_pointer(pointer, auth=user_auth, save=True) assert public_project.logs.latest().action == 'pointer_created' - res = app.get(public_url, auth=user_auth) + res = app.get(public_url, auth=user.auth) assert res.status_code == 200 assert len(res.json['data']) == public_project.logs.count() assert res.json['data'][API_LATEST]['attributes']['action'] == 'pointer_created' + # Confirm pointer contains correct data for creator + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer._id + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url, auth=contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + # Make pointer public and check data + pointer.is_public = True + pointer.save() + + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer._id + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer._id + + res = app.get(public_url, auth=contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer._id + + # Delete pointer and make sure no data shown + pointer.remove_node(Auth(user)) + pointer.save() + + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url, auth=contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + @pytest.mark.django_db class TestNodeLogFiltering(TestNodeLogList): diff --git a/website/static/js/logTextParser.js b/website/static/js/logTextParser.js index 248a7313ac1..4760a454161 100644 --- a/website/static/js/logTextParser.js +++ b/website/static/js/logTextParser.js @@ -300,12 +300,12 @@ var LogPieces = { pointer: { view: function (ctrl, logObject) { var linked_node = logObject.embeds.linked_node; - if(paramIsReturned(linked_node, logObject)){ + if (paramIsReturned(linked_node, logObject) && !linked_node.errors) { return m('a', {href: $osf.toRelativeUrl(linked_node.data.links.html, window)}, linked_node.data.attributes.title); } // Applicable when pointer has been deleted var pointer_info = logObject.attributes.params.pointer; - if (paramIsReturned(pointer_info, logObject)) { + if (pointer_info && paramIsReturned(pointer_info, logObject)) { return m('span', pointer_info.title); } return m('span','a project'); @@ -315,21 +315,13 @@ var LogPieces = { pointer_category: { view: function (ctrl, logObject) { var linked_node = logObject.embeds.linked_node; - if(paramIsReturned(linked_node, logObject)){ + if (paramIsReturned(linked_node, logObject) && !linked_node.errors) { var category = linked_node.data.attributes.category; if (category !== '') { return m('span', linked_node.data.attributes.category); } } - - var linkedNodeParams = logObject.attributes.params.pointer; - if (paramIsReturned(linkedNodeParams, logObject)) { - if (linkedNodeParams.category !== '') { - return m('span', linkedNodeParams.category); - } - - } - return m('span','project'); + return m('span', ''); } }, // Node that acted as template to create a new node involved From 40c40c325623d71c18b81712cf71b7940851d865 Mon Sep 17 00:00:00 2001 From: Alex Schiller Date: Mon, 11 Sep 2017 11:09:56 -0400 Subject: [PATCH 016/108] Remove unused deletion handling --- website/static/js/logTextParser.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/website/static/js/logTextParser.js b/website/static/js/logTextParser.js index 4760a454161..69980df3f7b 100644 --- a/website/static/js/logTextParser.js +++ b/website/static/js/logTextParser.js @@ -303,12 +303,7 @@ var LogPieces = { if (paramIsReturned(linked_node, logObject) && !linked_node.errors) { return m('a', {href: $osf.toRelativeUrl(linked_node.data.links.html, window)}, linked_node.data.attributes.title); } - // Applicable when pointer has been deleted - var pointer_info = logObject.attributes.params.pointer; - if (pointer_info && paramIsReturned(pointer_info, logObject)) { - return m('span', pointer_info.title); - } - return m('span','a project'); + return m('span', 'a project'); } }, // Pointer category From 61cdd698e07ab817384fc16200df7ffc910692bc Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 11 Sep 2017 13:47:02 -0400 Subject: [PATCH 017/108] Allow admins to import json from preprint providers when JSON is from old models. --- admin/preprint_providers/views.py | 3 ++- admin_tests/preprint_providers/test_views.py | 21 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/admin/preprint_providers/views.py b/admin/preprint_providers/views.py index ad97906d49b..72d732d4ca6 100644 --- a/admin/preprint_providers/views.py +++ b/admin/preprint_providers/views.py @@ -231,8 +231,9 @@ def post(self, request, *args, **kwargs): if form.is_valid(): file_str = self.parse_file(request.FILES['file']) file_json = json.loads(file_str) + current_fields = [f.name for f in PreprintProvider._meta.get_fields()] # make sure not to import an exported access token for SHARE - cleaned_result = {key: value for key, value in file_json['fields'].iteritems() if key not in FIELDS_TO_NOT_IMPORT_EXPORT} + cleaned_result = {key: value for key, value in file_json['fields'].iteritems() if key not in FIELDS_TO_NOT_IMPORT_EXPORT and key in current_fields} preprint_provider = self.create_or_update_provider(cleaned_result) return redirect('preprint_providers:detail', preprint_provider_id=preprint_provider.id) diff --git a/admin_tests/preprint_providers/test_views.py b/admin_tests/preprint_providers/test_views.py index 70fdd511e51..c395670be1a 100644 --- a/admin_tests/preprint_providers/test_views.py +++ b/admin_tests/preprint_providers/test_views.py @@ -255,6 +255,27 @@ def test_export_to_import_new_provider(self): # nt.assert_equal(new_provider.subjects.all()[0].text, self.subject.text) nt.assert_equal(new_provider.licenses_acceptable.all()[0].license_id, 'NONE') + def test_export_to_import_new_provider_with_models_out_of_sync(self): + update_taxonomies('test_bepress_taxonomy.json') + + res = self.view.get(self.request) + content_dict = json.loads(res.content) + + content_dict['fields']['_id'] = 'new_id' + content_dict['fields']['new_field'] = "this is a new field, not in the model" + del content_dict['fields']['description'] # this is a old field, removed from the model JSON + + data = StringIO(unicode(json.dumps(content_dict), 'utf-8')) + self.import_request.FILES['file'] = InMemoryUploadedFile(data, None, 'data', 'application/json', 500, None, {}) + + res = self.import_view.post(self.import_request) + + provider_id = ''.join([i for i in res.url if i.isdigit()]) + new_provider = PreprintProvider.objects.get(id=provider_id) + + nt.assert_equal(res.status_code, 302) + nt.assert_equal(new_provider._id, 'new_id') + def test_update_provider_existing_subjects(self): # If there are existing subjects for a provider, imported subjects are ignored self.import_view.kwargs = {'preprint_provider_id': self.preprint_provider.id} From 264c0fe8a4669b0bf70afc255f8211c72fe254c1 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 11 Sep 2017 14:07:45 -0400 Subject: [PATCH 018/108] flake fix --- admin_tests/preprint_providers/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin_tests/preprint_providers/test_views.py b/admin_tests/preprint_providers/test_views.py index c395670be1a..8610e9ed7a2 100644 --- a/admin_tests/preprint_providers/test_views.py +++ b/admin_tests/preprint_providers/test_views.py @@ -262,8 +262,8 @@ def test_export_to_import_new_provider_with_models_out_of_sync(self): content_dict = json.loads(res.content) content_dict['fields']['_id'] = 'new_id' - content_dict['fields']['new_field'] = "this is a new field, not in the model" - del content_dict['fields']['description'] # this is a old field, removed from the model JSON + content_dict['fields']['new_field'] = 'this is a new field, not in the model' + del content_dict['fields']['description'] # this is a old field, removed from the model JSON data = StringIO(unicode(json.dumps(content_dict), 'utf-8')) self.import_request.FILES['file'] = InMemoryUploadedFile(data, None, 'data', 'application/json', 500, None, {}) From d125d4c3cbc25096867ea65f87f40d6a9954efdd Mon Sep 17 00:00:00 2001 From: Alex Schiller Date: Thu, 14 Sep 2017 14:05:46 -0400 Subject: [PATCH 019/108] cleanup --- api/logs/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/logs/serializers.py b/api/logs/serializers.py index 53a05e25942..7fa69023951 100644 --- a/api/logs/serializers.py +++ b/api/logs/serializers.py @@ -113,7 +113,6 @@ def get_params_project(self, obj): return None def get_pointer(self, obj): - print self.context user = self.context['request'].user pointer = obj.get('pointer', None) if pointer: From f5c1b009371744cb5ae391a493c63a1d6a05a1cf Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 14 Sep 2017 17:53:42 -0400 Subject: [PATCH 020/108] Downsize images --- website/static/img/fa-envelope-blue.png | Bin 2780 -> 1165 bytes website/static/img/fa-facebook-blue.png | Bin 1385 -> 1104 bytes website/static/img/fa-linkedin-blue.png | Bin 2448 -> 1207 bytes website/static/img/fa-twitter-blue.png | Bin 3117 -> 1376 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/website/static/img/fa-envelope-blue.png b/website/static/img/fa-envelope-blue.png index 75e8ccf8b988aebf477eb36c2451b2b8e6b37759..054f694e23a985afe263e1e380ece1701aeb4591 100644 GIT binary patch literal 1165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DWNs30;kB%&n3*T*V3KUXg?B|j-uuOhbqsG5Pnrosxy%uOvxRH(?!$t$+1 zuvG%9umZ9{!um=IU?nBlwn`Dc0SeCfMX3s=dM0`XN_Jcd3JNwwDQQ+gE^bimK%T8q zMoCG5mA-y?dAVM>v0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIXX$YJ0ilN>DdQcxEqi?8Zpbs}2sMQ7}YUN*) znF_QSWWJrD4Ok_L7}OpceURlyj)C|TEDH3h9hZ$hJlO2Gd=9BcGB7Zm^>lFzu@GDv zx;r~KP~!i&$99vJeRN&UB`R`cAxCF@!y@O$4^}u8iru!#p3T>I>(zslOhNYLe2c0t z9X-AxfjexDXo+^ytR*}9i_bs*J~v|Hvb7@B5AIaI`~G9@`&z~4v7TGJ}f`0`IeD~oYJtaAC)`=?6&C8Q}m zZgy2>JX*WHV8-51zn}}%`m?77XsmxPVA;nnQQ$+AqWPtXc=$7|$D2ad_OH9Qx8kzP+0z-Dr=1d&ICEiZ8M}m%^}NK$OPR~(UK3^0 zzPqm9Yw6*opF)xoQ@4Hc`E8N0_*${(w`_qfBb7^W0gD2^m8^?TFMQ3Ka`xcWofDTY zef4P%*CEx+$r7EGVe?ki8yU{4vJT(r&}4ps!+U0O_T+W0=T;rLsH0SA;^#I;d{@OJ zo?kCoTRD#vl{)=Xzr@|%BECn@SNDG8Oxem6DOF8gnZD2Ye>i2GoPFLW*h@}*!_lIx zhRi|xC&m@@7~YWf%_E;v`K3zIDwEoTq?#qvt zq!dc03h;T$@)_Sdv#MEr7`8JE~=p8$ESz2MNZEWrAckgj< zbaHlab=&Li>4n|rz2DbAATTHxA3``7c8GZRNJL~*bWALnax^|6@mSI?DaTW3X&EO^ zWo4f}L(k1CC}b9wo-I3HQCU^ZVqd&eclippp^?WI+_>3t>rQK1`~A*GUENQ5`-D$L z1B1h(V-u58e@;tYOW(=n7M9+B{PcPC+xq53*cBxJ0CLS2>p?*1E_I)a6|EXVKCG^0 zu#WgBqPXPZ>$mRtwVC54UDBT7T*ua`nzkQ^9S`8c>Z0p*i}QVyx-Uv9nJy8_(?4GI z#F}K(XRU2o-e1p-vN}`~NBmEO^zK-#eK2MeGoS=+F`U``3^H8h`bu>CN9GgbnzNN{4D#IAHhk4m1v88E@9L{jj+Y3P(odj)#w@c{-r z?PBq^@7|5!DhgU5H|-HCj$_n4037mjre;?nVNs55)x<%{c$8F3g&?WVQ;;s_+%#ps zQ6~W7gT_4L8>u-mF4aRNxJ`Qn-}b4)e++hoA=p$X;)jFB@om|-M#@dV8vg|nV9*$I z7eEMZG0NMYOGrd2{N~^0XDyrvYSjDCa}aHh402H8kEpub%|(m5Q(63A!$^y=;5s2z zk$v^1c1>dsKwR6HwJ$IM27i`CjkDNE9L3{n)&58{Ob!}Q1Pd&cL%obHRIKsX9U@4xvzNdx$75YkNp<* zRsQSqJP!PA{ZZ{EeF#3mxn8cm_!#|I8k1=9Q2mZpv>WJAilrL&@eUDmVw;E5xm%|T z)hRH}swYY`L3pBm5unIX=5F^{yRYM(Zh6uhj3nizt+CCz1J5wc7@hn&Me>ZQg01!_ zBSy4%-rFu>(M?-%1;UKl*yocj%o58~c}z*20=L)IAa5;Ys zt|_Azxhi`IO zx9PVdcGCGPu)cf}7<=j{Lf=AmqflrdK?E=4#YgLaugL08!Ntk577Ij`ik2Fa;4djC z?DrN2!6Q|7yY@O8%;j(-)<4scsxa;;-QawrA8#9ESfo@mNay95PhTQ**mN%#_I;tF zUb+X_Emp1z+bYhFkI}SUJ`)G8o{ll;>Uoav5fzDUSsD0<0?ie0Lt+gkuiYlkY1V_V zVX7%_j=rO1GOK9&t_sOL7i|v39f?)PDtEHgbDkONMGf{ad=ZBr_-P`=Jpzb*zhbLI z3VF1H9v{jyQvN01;)lTnIrHloy4V8NT+_k@e8Gk0tglYk(PfMliRxxuF3s^` z)eRQ)ERv~RTkL}hM5hb_jVB&O*fE`VOS^G8O!*A zNh9g4vAhW5-a4%{9xD{JQ2_64D^gSxwm_e6zD&X(3AW z=RuU7`ca-mjrlKbH7+_H%fJvC=Dl$ND}rKFYP!D0GL(RG@k>=DmI zD0(^J@VKpAm0S=u=QaeoO6{?2G#8?B?GL~Q|AxB)GP&a(M|yX_#vxu;Nt@kTz07Bh zfkE`?uT5K;(MgSa+|OfockjDox}qrKlma`3dYYKAS@*=@~97P(FLKXdcj*Di+{+F{+95o9(^>R&B*B_S$S+K z-($cPoi-u44JHVnK{+1PU*71B`V?5|yUhL`ke0k=jg{=KPU!E$eD@ZNNc#Lm9_TwW zq-)<+`Pjo++!LqQ%b~O+8)k3QcZOqg^Ay9PWj^oBpE3k#Kv&lB@pH4l<-Y}I$_9W&011Kfk!vFvP diff --git a/website/static/img/fa-facebook-blue.png b/website/static/img/fa-facebook-blue.png index d85d96da8877e064c065f07e53439b454d193cdb..98d80f5531a771d898765e4f77f18a3bdab87b58 100644 GIT binary patch literal 1104 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DWNs30;kB%&n3*T*V3KUXg?B|j-uuOhbqsG5Pnrosxy%uOvxRH(?!$t$+1 zuvG%9umZ9{!um=IU?nBlwn`Dc0SeCfMX3s=dM0`XN_Jcd3JNwwDQQ+gE^bimK%T8q zMoCG5mA-y?dAVM>v0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIXX$YJ0ilN>DdQcxEqi?8Zpbs}2sMQ7}YUN*) znF_QSWWJrD4Ok_L7}OpceURlyj)C|TEDH3h9hZ$hJlO2Gd=9BcGB7Yr@N{tuu@KxF zd_FtaQDEN9+S^ym0=Bx?Xino&ZCvESlK*wd-j|AxZZ^+j6-N_a`)pi$G!_qzAj^9v0?Y-&G+l_zs;`Xs9h5>>-y7A zn$;gpJ>Kh4!OOT@Vjnw$Im3(YO%;#6zI}XELOsN5nMt=JyPn;x^&X*TF7z3%Xx?al zLnq7lok&{h{-6_yXEv%PG8sIZCV9_J>gx5ZCps_A6fE#z3pjaIqfIsY&>WW|oX$rN z%W}?tlVh^7YekuG;hZ~K{?~sdo_-@e`{uVAO`Bb8E^FAhn3!hnT07-@^33M_N~Wt$ z_6b-nXWx7(?}}VV+Vb~*d-ErI+I&5E+W&Y>jH%Qo$8S#`|B+Zyy3?dGv2cgLIsSq> z?5bkXNS1*}08TJ{R_GOAzAiEOj=o{Q7X;ftisin-*N* zcx`-kd6#xi%Hr>D_CK2VG$(xR%{^O0=J{OTFh#%mt_g47E5_nk+y{3>7o7FocRtfy zFj2r~0hihoNtSiL(zn}{+>D<`X78GU} zRC?}>ZsUm#9h;udTDgreZ}Rv2m$;5voqnp3?%7~#tYM)0uGUj%B>>YH725k5;caaB?5l`$vzCA3r(E%Rf`tS z;Lr0dmx>zh)ZDvq|I^Zw(ZA9>teoC$;K^D$)l6rhSJxDS63f4wCtdDrn}6cs^|V~m hM_qqj?7F}CFYgZCKh_)%N-aRy+|$+1Wt~$(6968M%0d7D literal 1385 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fn={z$e5N$ThCoK8olW0!(Fm z<-l;SE(!7rW?*DuW?^Mx=iuVz;pGz$6cH7dkdl#=Q&3b=QPa}aF)%bTHZirZvaxk= za(4Cd_VEu24U3M6jY~>SNz2U5FDNc4t*ET7t8Z%S=mSDUbbSxmaW@& z?%8+X;NcTz&R@QM>(0FgkDfex@$&WC_a8rh{r>a!-+4QfRT&tVH9cJ%Ln>~)y?ZlA zI8eg%;bq=Q0*X9cZoN$%56&!bbQE-FQ5Rts3Amu4)z#r};E(?M-tA#eO_omBe7&k6 za zsGN!o=XO@{cnB?DLIJ}JaLfS2ckCQA8EXZVDvh4`tF<{w&-- zWffb)Vb0^z)pos#<=|DB75|3gndyy{ET6CY?JD^7aDTm>?1g_f>i;|WI{c13_WvLM z%U|k09bZ%g{El7z{@=|F-*(r^d8q^5{td@2Fr@wYw!UTC z{mOZ4)gm$n7Rfv07ciWzWBB=i!6v@BKxJvO6u3XtuTJY@c0#j_*F)$tBl`PGB3Hxs8rbC?gS=w;>6+`VShT~TlXI*7{ zb%e1FD7wh%z!Iwi8>|ihrRP{VG+R07@NEFf#jR$(6U;6V!2TkDJtLIeV>R=R*nnM| z{v0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIXX$YJ0ilN>DdQcxEqi?8Zpbs}2sMQ7}YUN*) znF_QSWWJrD4Ok_L7}OpceURlyj)C|TEDH3h9hZ$hJlO2Gd=9BcGB7ZG_jGX#u@Iaa za@IQ}P{jWEzVFX|+s^e;EPS}CH*lfbMWscqN-SGv7DTe#T4mij*LJ@-8cLi zGF-PTxE#AVS}wG_Xr30-)#=x3d~Q#Fb^h}`fg6un7^HuESNr^NdHwgA7?w%tJkLVd=DtyGaXT z%H8I_tV_>p&eQpOU}xt;28{*f6^FK1^JQ*p{ne+)`8eXg+u0}jlUF{Ksm}ACE6QXw zHB8mvu;GD-A2V#X7bMl3;;ebIe2x?s>&A)8l8?s!jamIdjD7v2e=FQv0)FkDbnmo3 zzn+fa3QZovw41rh|9-M^dBglbbejA{U$K-1cEKY{I*v9zG!b1Bv6iJzcU|y0|4daz zj()@DQ^z9ipE#4CcYD6M>5HHM-Jk}YiHfev!z|2~EU#I)e^XT6!uT$>=}q$`HilRm zGCi$+vLxo4XxN#+y02e9-p=36I$eBXwBKxx^5teuWve))G!B)i$R6vBSQH!9)qQI9 zK1NmXR>{swCk0=I#u~pnBJ#^zYKvQh$FXULx|F`YV)tL2Tzs>!d(Wx^v&AQB+?UEY zpWp0<_X?l19e16qU0y5aMz&mw$_qX5eCnwU)hC#ZF4y09V$x{BzMEz1f==FU!3(E4 z`6^aj3e(~})N?D$=E;Mwz9zj$Ol67P5re)gK^Hr}(p!q@-! jz3|$LMeec-7XIO%%pEd+rNWIfpwh(C)z4*}Q$iB}(Z=jp literal 2448 zcmeHI`#02iAOC#6-(eV`Tp}7oF12KmMaE{BalbR6Qf84@lO0`*iTF~BS;nKw<}%5p5si8D^Yi=(`#SG)-nZ9zpYwj5_c`xOUvC#hf))V) zpy=jG^9KOh2q7ScY`B=}U^)P>uCFKEX(QctuJIrHM}hyVfcfX_A2&{<{*1qO0Dxcw zCnJlOlP4%_QdCmjyhTM-O?|6|<~A+u?L-}2J%gQwM#d()c7J1Pw#S^b*J7XLw^r6R z`)$c~-yN{0I5<*iPR=fF?jD|A-afv5{`7-^LBWTPgd9B<`h8e;z6?QgRBKcHvTbMrQVvYk38QMa8#nmvZlw-LI&u`svZnPwE>Q`Asct*wbITx_kQi z{}^~VJTfX67ruV`_xqW-4<8qo#h=$Cso#%E0gz?7(H!UrqjP^{hSCqJ;peS8`q9pc zi%&Fhn{GYvg0oT`QcbnG|0Bx-?pfvhaz}cefBKf-u$?A*X2(7wYfBFgs1y3g;#O;R zKfm2x7}77^*K(6R(JGEE&iaa;QJkTU4W~6_{@vu-`JiHg>KD$pp+naAn1zMI73Mjj zxf;J<!379_1Qm`wgwyL#q}Zv$jH9D~C4Ste4yGZ0X*mzl-^ z^qJyQ4Sk>lgARS5X5?I&1#IMjXEyLn@O-Qf`?>>0AdX=Vp>856Y@iRe;XrH_ps9i= z6-X|EykkYIs!tHZJ7B%;d6p(-~J@(iKnfeaV1w4k$u`-z~yDw3lZej;Nza_Aqh z64$FN?+3bcF*yP-r$Rp9q`!I7q{^1=(J%gz#)S1gz@Pcl-jp9GEk1v7f`&<_)-H~D zzR-rLv5oBo)qYpzZrqaiI7QI{T4uea!%3D;+stt7lr`P@=pohN=GZmr z&80EvzGOao>qLuK5+9n|X>#}Go%j^{^)Tq|gJkX5n%>cS4=eu%36^bl$tC+`4AmPB zzA>yB;$;Bs!v2#-ZZ9|9X+1u;`UZRdEQKzOdVP9jyWMlM{4w6&PSotqaOZ8-MZ2Gx%akZ>y_4;_<$iz zza>r0d|PEe&(LTPr}}N1uiMV`K zEmQq!G13umVizUVci=IHw4jU{;Gzi9aHkQUbLVNO_t#31M2k9RM}<2u}b zIj4X<^9ou?F^Xe4^!K|NOnWEbvAOcbM;|k1gEyPc&BLh2ju}P9kB7IKwFR9+?_Pa% zHVR_ndPA=hx$$+&C*Op{$c6rlMqpVt{LY0UCt9q>Tn%S=UW)u|>56ozwFFQn*G3lQ!VB^Fi0^iVTR~ zpu;CE|9mIS!Ye~Sg_#a;LANZ6kk|yD%HkciLhVd3x!4w?5uoxIbZfXa_Q-Bdm!Y&d zeyL^xC>XpxkgqfXUc3W_!p@mIMQ~n+zB9HZb|f%^u{N9*H+9qq5%vrOoP@s8P)^6|?L<^unb#NNpB!g|&3)tw&U|M?FWGq$$64f^9OcB^G zfKGj-*nOqkA)_7;3BQ7pv11}2BY+5eL@f^g?=B*v9cC!`_7R1EjVkkz0v6unjS?%D zjJXJ)9v)I~8oyFR#*78f?ytkB0;r0I&`;wlCzX7Ai9!+^H4s44JY?uJ-lm7hO(jzd z*r<^R{4Rj5ZIqr8g*(}3fB-toL+VfAqo$Nt#bj)o2%O*}B~wZ-`ia~NWQs8xT@*k~ zJS3@;DAZ@88(S3ektG&>rG$*Bo5Q|-8<;G%2j_MUTKA+oAK&`!AMU BMJfOQ diff --git a/website/static/img/fa-twitter-blue.png b/website/static/img/fa-twitter-blue.png index bed6cee7c473c2df369be16926593ceb6d02432f..96b0911740e8a79b1ec03c49e468a70a9033aa0b 100644 GIT binary patch literal 1376 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DWNs30;kB%&n3*T*V3KUXg?B|j-uuOhbqsG5Pnrosxy%uOvxRH(?!$t$+1 zuvG%9umZ9{!um=IU?nBlwn`Dc0SeCfMX3s=dM0`XN_Jcd3JNwwDQQ+gE^bimK%T8q zMoCG5mA-y?dAVM>v0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIXX$YJ0ilN>DdQcxEqi?8Zpbs}2sMQ7}YUN*) znF_QSWWJrD4Ok_L7}OpceURlyj)C|TEDH3h9hZ$hJlO2Gd=9BcGB7YN@N{tuu@Gzx z@y!VF6j=A_-S;##!@w(V5=B`)CM{aw6LO>3?WQwNmGLR9caAQpN4wb#FCGp~GOLP> zh~%sgsJ z>21NDl^+wrgK{5yo5Q#$_r2uZ{MMZgdf6xR)$R&6KJr=oynaTn7@y*!88<549o$qR zVt1>m?u$BG@`YC)?#??{~xttT!Q#3uT7JIoO|6`v`ziTQ(0#DWdNlYXjn zUp%Bd;X`}SHGWS4uJiuQrrS5~Rc=-|mS2^254?Vuw zBv$7o^de;6{>@i4ug#V7Gc`|Szsb93!s@jb1%oV`<639=lihTbo%kn1NW|nh#k)nve&cRoGYpkeLYwy zxVPu`hocVH>mBA!u`Y1X-!_?TbE8*!`Og%s{`_y-7dG2!neSWRT@%LsrX=Ep9os7w zv9$C3F~KiY)qRDUJDuxVk1|KT?+AA72t6ERujv=Ru*mZG-6Iz_?h9f2ll(zjaO?Mb z_Y(rYKKt`(n`i#i|DSn*Z#lyuV*Op>|bs(7WlH&AG3Crn@qIOfxAn=CJF2 zbs~28?8aL@oLMWD{P-ERZ~yBJ%#RZ`i12)#@wRfqT$cS8Z#d<>-n&dRWX0zN;#~(! zpS69Fn3I~n;)|0|K8z$$~$^Sfl);(WQ ze6O+Z_?Eu)i|!Y#y0HE6&fQH6N1x1J{%Y0V*I`V4){{P;W?dqB>`Kwh55aaf!j~}1 zr1t-R`gHyL2lkh3QzBz_7rgkxf6Z&P#VW()>wj%sy~XslzO&)9g9k4kGXxcTp00i_ I>zopr0CzQ1o&W#< literal 3117 zcmeHJXEYlM1C7|TR_v5m2}+B2HOgyji4kJAHhET4C8|52EVN;q4{6@+ra$7?UVdQ_rKGH)vienReM4jO->vPPZ@YT>1_p|p)3bB) z3rj1XSJyYUzS8!-9UPsUo?pCE0-XQ=ET2sD5!OP&w5~Wk{v#ms{@z_|$lRJED?hv; z59wTPp(=hxkbL#Ns9_2@as`9J#PibjVTtH+=TL@1;Ro^g(%tn6ZOdaM`Zq2RY}Usm1n z0voIpu51vzss=M#erKAK8r{ph(y1Yr~6rm9RC+3Gloh zaVIo{h>LhsTHu(fs5VFlu}QhF^DHJM5xN`|RC1((s#MMyxK#2ZgWmWHG&MZ{33}ap zDKg!^XK~B1RogRI5XE`o+rV#PuuctsJk&&-*fDFl)`xaP{ZB= zAf>SS=sNM)9-re{0~7!7d46t>0}awG790G7-7y*8$42>7_UMS zx575b;ilf=0gCL*vtnpkmTTqK;u zxbC@C?|?)Hmg_z;l0k+nslmGCBUYgSIL|HRh%*tAeEQFjdcusvj;)!nJ4Hy}CD7-_ z*{$JE3xhEo#13+PB@4Y-743_tkgGz>G=}RIjl!_ez4_{a2lSmG$00Z-qarFXP_7FK zyp)dHUfFZ!;g}jtX=gm&DWs*M_HT|ZSa|%TT63g+j^$$gAWbe_6k*?wS%8Ktt?1o# z<<9ki$G9ao`@HV-3Ww#?HR<%zC3)1cP`6$HwPMxp9M2u!tqCeq&}33@C5XC43gi11 zZ*y4oT84>7t#uYkrFseZUNH)4;}D=?@VyuzLsp|^#9J%&t=Ki8Q6Z82AV?KH-26kV z+Zr`?HPF5+o9hRl=Y4@PRH_z92i{jUl<-FXbg(Z@{r)JHXR*j~1LYcL0b5i`*pW>9 zSl$B^gnhhUd@o#f;S^dwYxK?;q50K&=+|Y$b0XQFXEIAK?KmbSy^bue4BA?wR;^;e z4w-r{a7kU>1Ruw-c%X1)p?)d7kPTQhz_0vfw5?TMGNYm0NWy|c2}J6Zpl6iaz?;tA7#gtuwWL$pBaJDZVTBB{mEm3uGT>?Bes6V zy$Sg{dt~-vwTQrC*KS!y*u9nt7O8l2lDa8NuTQo9#doU+H`bvEPSxjBLV5p*VB)lL zuNQKHRkhb$69V>Q1ugOm~m#m&L_X-|{i@Oc$ z>Wnw~VOdTTQ4>cxL}k#<-r3T|kQ5Nss^5Hz)8nbS^~RjXz}EdBSJP*gZ5Sd`4`n%- z8pqQ#U8it_$=Pcasd_IY2k%MBQwawJBmH)028<45sW0 zl#nS1{lONPJtdBbV95P~WwF302Z5EHl*-eKo*5SnOexnSAox^dvo`5|A=ZI*TaOW* zkOhD=0NA}g4A&A>rT%u+smce!0JBM(a z6aJmXR9`KQw(7#6`QQ&VS!@a;rnqj;Q~#%oa=ZolSdELgi)2tN(y6m)!1O`cZTrhF^=7(a#TjJufsU%g(E#0 zxq?ClYp_s;JN3#Rv+BxxA`DFz>&aY|hRr66@T;7%$!IC%ClNi!)dJhGI~2TVK|;<6 zhFrr|G58B+u=N>))}M3JTp>Wn`8G}CtjpWzt`=|yzV{E9#FOb@&7u$`vFlixjOLm@ z!zkP6Jk?~wpCm~EIL1H{`6OHp{p3G?#;beWrUwP!L~<~JQCwO_>N%iA3udtJr47<@!URuUvm`q5 zco>8X*;@Xc*v%!Cw=X1_jTA(eyr< Date: Fri, 15 Sep 2017 17:49:38 -0400 Subject: [PATCH 021/108] Fix unregistered contributors registration records --- osf/models/node.py | 5 ++ osf/models/registrations.py | 9 ++ osf_tests/test_node.py | 1 + osf_tests/test_registrations.py | 42 ++++++++++ scripts/fix_registration_unclaimed_records.py | 69 +++++++++++++++ ...test_fix_registration_unclaimed_records.py | 84 +++++++++++++++++++ 6 files changed, 210 insertions(+) create mode 100644 scripts/fix_registration_unclaimed_records.py create mode 100644 scripts/tests/test_fix_registration_unclaimed_records.py diff --git a/osf/models/node.py b/osf/models/node.py index 2300d71fa0c..402c3c086ec 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -1328,6 +1328,7 @@ def remove_contributor(self, contributor, auth, log=True): # remove unclaimed record if necessary if self._primary_key in contributor.unclaimed_records: del contributor.unclaimed_records[self._primary_key] + contributor.save() # If user is the only visible contributor, return False if not self.contributor_set.exclude(user=contributor).filter(visible=True).exists(): @@ -1649,9 +1650,13 @@ def register_node(self, schema, auth, data, parent=None): log.clone_node_log(registered._id) registered.is_public = False + # Copy unclaimed records to unregistered users for parent + registered.copy_unclaimed_records() for node in registered.get_descendants_recursive(): node.is_public = False node.save() + # Copy unclaimed records to unregistered users for children + node.copy_unclaimed_records() if parent: node_relation = NodeRelation.objects.get(parent=parent.registered_from, child=original) diff --git a/osf/models/registrations.py b/osf/models/registrations.py index 04b3131914d..bf607ba7333 100644 --- a/osf/models/registrations.py +++ b/osf/models/registrations.py @@ -326,6 +326,15 @@ def retract_registration(self, user, justification=None, save=True): self.save() return retraction + def copy_unclaimed_records(self): + """Copies unclaimed_records to unregistered contributors from the registered_from node""" + registered_from_id = self.registered_from._id + for contributor in self.contributors.filter(is_registered=False): + record = contributor.unclaimed_records.get(registered_from_id) + if record: + contributor.unclaimed_records[self._id] = record + contributor.save() + def delete_registration_tree(self, save=False): logger.debug('Marking registration {} as deleted'.format(self._id)) self.is_deleted = True diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py index 9d32c451877..122f41df867 100644 --- a/osf_tests/test_node.py +++ b/osf_tests/test_node.py @@ -848,6 +848,7 @@ def test_remove_unregistered_conributor_removes_unclaimed_record(self, node, aut contributor=new_user ) node.save() + new_user.refresh_from_db() assert node._primary_key not in new_user.unclaimed_records def test_is_contributor(self, node): diff --git a/osf_tests/test_registrations.py b/osf_tests/test_registrations.py index 489ffb01458..6e05458e014 100644 --- a/osf_tests/test_registrations.py +++ b/osf_tests/test_registrations.py @@ -299,6 +299,48 @@ def test_legacy_private_registrations_can_be_made_public(self, registration, aut assert registration.is_public +class TestRegisterNodeContributors: + + @pytest.fixture() + def project_two(self, user, auth): + return factories.ProjectFactory(creator=user) + + @pytest.fixture() + def component(self, user, auth, project_two): + return factories.NodeFactory( + creator=user, + parent=project_two, + ) + + @pytest.fixture() + def contributor_unregistered(self, user, auth, project_two): + ret = project_two.add_unregistered_contributor(fullname='Johnny Git Gud', email='ford.prefect@hitchhikers.com', auth=auth) + project_two.save() + return ret + + @pytest.fixture() + def contributor_unregistered_no_email(self, user, auth, project_two, component): + ret = component.add_unregistered_contributor(fullname='Johnny B. Bard', email='', auth=auth) + component.save() + return ret + + @pytest.fixture() + def registration(self, project_two, component, contributor_unregistered, contributor_unregistered_no_email): + with mock_archive(project_two, autoapprove=True) as registration: + return registration + + def test_unregistered_contributors_unclaimed_records_get_copied(self, user, project, component, registration, contributor_unregistered, contributor_unregistered_no_email): + contributor_unregistered.refresh_from_db() + contributor_unregistered_no_email.refresh_from_db() + assert registration.contributors.filter(id=contributor_unregistered.id).exists() + assert registration._id in contributor_unregistered.unclaimed_records + + # component + component_registration = registration.nodes[0] + assert component_registration.contributors.filter(id=contributor_unregistered_no_email.id).exists() + assert component_registration._id in contributor_unregistered_no_email.unclaimed_records + + # copied from tests/test_registrations class TestNodeSanctionStates: diff --git a/scripts/fix_registration_unclaimed_records.py b/scripts/fix_registration_unclaimed_records.py new file mode 100644 index 00000000000..ec1c9a3ba8d --- /dev/null +++ b/scripts/fix_registration_unclaimed_records.py @@ -0,0 +1,69 @@ +import logging +import sys + +from django.db import transaction + +from framework.auth.core import generate_verification_key +from website.app import setup_django +setup_django() +from osf.models import Registration, OSFUser +from scripts import utils as script_utils + + +logger = logging.getLogger(__name__) + +def main(): + dry = '--dry' in sys.argv + if not dry: + # If we're not running in dry mode log everything to a file + script_utils.add_file_logger(logger, __file__) + with transaction.atomic(): + qs = Registration.objects.filter(_contributors__is_registered=False) + logger.info('Found {} registrations with unregistered contributors'.format(qs.count())) + for registration in qs: + registration_id = registration._id + logger.info('Adding unclaimed_records for unregistered contributors on {}'.format(registration_id)) + registered_from_id = registration.registered_from._id + + # Update unclaimed records for all unregistered contributors in the registration + for contributor in registration.contributors.filter(is_registered=False): + contrib_id = contributor._id + + # Most unregistered users will have a record for the registration's node + record = contributor.unclaimed_records.get(registered_from_id) + + if not record: + # Old unregistered contributors that have been removed from the original node will not have a record + logger.info('No record for node {} for user {}, inferring from other data'.format(registered_from_id, contrib_id)) + + # Get referrer id from logs + for log in registration.logs.filter(action='contributor_added').order_by('date'): + if contrib_id in log.params['contributors']: + referrer_id = str(OSFUser.objects.get(id=log.user_id)._id) + break + else: + # This should not get hit. Worst outcome is that resent claim emails will fail to send via admin for this record + logger.info('No record of {} in {}\'s logs.'.format(contrib_id, registration_id)) + referrer_id = None + + verification_key = generate_verification_key(verification_type='claim') + + # name defaults back to name given in first unclaimed record + record = { + 'name': contributor.given_name, + 'referrer_id': referrer_id, + 'token': verification_key['token'], + 'expires': verification_key['expires'], + 'email': None, + } + + logger.info('Writing new unclaimed_record entry for user {} for registration {}.'.format(contrib_id, registration_id)) + contributor.unclaimed_records[registration_id] = record + contributor.save() + + if dry: + raise Exception('Abort Transaction - Dry Run') + print('Done') + +if __name__ == '__main__': + main() diff --git a/scripts/tests/test_fix_registration_unclaimed_records.py b/scripts/tests/test_fix_registration_unclaimed_records.py new file mode 100644 index 00000000000..c0194ae2d5e --- /dev/null +++ b/scripts/tests/test_fix_registration_unclaimed_records.py @@ -0,0 +1,84 @@ +import pytest + +from framework.auth.core import Auth +from osf_tests.factories import PreprintFactory, UserFactory, ProjectFactory +from scripts.fix_registration_unclaimed_records import main as fix_records_script +from osf_tests.utils import mock_archive + +pytestmark = pytest.mark.django_db + +class TestFixRegistrationUnclaimedRecords: + + @pytest.fixture() + def user(self): + return UserFactory() + + @pytest.fixture() + def project(self, user, auth, fake): + return ret + + @pytest.fixture() + def auth(self, user): + return Auth(user) + + @pytest.fixture() + def project(self, user, auth): + return ProjectFactory(creator=user) + + @pytest.fixture() + def contributor_unregistered(self, user, auth, project): + ret = project.add_unregistered_contributor(fullname='Johnny Git Gud', email='ford.prefect@hitchhikers.com', auth=auth) + project.save() + return ret + + @pytest.fixture() + def contributor_unregistered_no_email(self, user, auth, project): + ret = project.add_unregistered_contributor(fullname='Johnny B. Bard', email='', auth=auth) + project.save() + return ret + + @pytest.fixture() + def registration(self, project, contributor_unregistered, contributor_unregistered_no_email): + with mock_archive(project, autoapprove=True) as registration: + return registration + + def test_migrate_bad_data(self, user, project, registration, contributor_unregistered, contributor_unregistered_no_email): + contributor_unregistered.refresh_from_db() + contributor_unregistered_no_email.refresh_from_db() + + # confirm data exists + assert contributor_unregistered.unclaimed_records.get(registration._id, None) + assert contributor_unregistered_no_email.unclaimed_records.get(registration._id, None) + + # clear registration data + del contributor_unregistered.unclaimed_records[registration._id] + contributor_unregistered.save() + + # clear registration AND node data for second contributor + contributor_unregistered_no_email.unclaimed_records = {} + + # change name to ensure given name passes on + contributor_unregistered_no_email.given_name = 'Shenanigans' + contributor_unregistered_no_email.save() + + # Force refresh and confirm data gone + contributor_unregistered.refresh_from_db() + contributor_unregistered_no_email.refresh_from_db() + assert not contributor_unregistered.unclaimed_records.get(registration._id, False) + assert contributor_unregistered_no_email.unclaimed_records == {} + + # Run script + fix_records_script() + + # reload again + contributor_unregistered.refresh_from_db() + contributor_unregistered_no_email.refresh_from_db() + + record_one = contributor_unregistered.unclaimed_records.get(registration._id) + assert record_one + assert record_one == contributor_unregistered.unclaimed_records.get(project._id) + + record_two = contributor_unregistered_no_email.unclaimed_records.get(registration._id) + assert record_two + assert record_two['name'] == 'Shenanigans' + assert record_two['email'] is None From effbce5cd7f787e8af30223c4e997614bfac4471 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Wed, 6 Sep 2017 13:45:20 -0400 Subject: [PATCH 022/108] Add institutions to registration settings --- website/templates/project/settings.mako | 145 ++++++++++++------------ 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/website/templates/project/settings.mako b/website/templates/project/settings.mako index d2b3358aa1e..e2eb507abed 100644 --- a/website/templates/project/settings.mako +++ b/website/templates/project/settings.mako @@ -32,10 +32,6 @@
  • Commenting
  • % endif - % if enable_institutions: -
  • Project Affiliation / Branding
  • - % endif -
  • Email Notifications
  • Redirect Link
  • @@ -50,6 +46,10 @@ % endif + % if enable_institutions: +
  • Project Affiliation / Branding
  • + % endif + % endif @@ -313,75 +313,6 @@ %endif % endif ## End Configure Commenting - % if not node['is_registration']: - % if enable_institutions: -
    - -
    -

    Project Affiliation / Branding

    -
    -
    -
    - % if 'write' not in user['permissions']: -

    Contributors with read-only permissions to this project cannot add or remove institutional affiliations.

    - % endif: - - Projects can be affiliated with institutions that have created OSF for Institutions accounts. - This allows: -
      -
    • institutional logos to be displayed on public projects
    • -
    • public projects to be discoverable on specific institutional landing pages
    • -
    • single sign-on to the OSF with institutional credentials
    • -
    • FAQ
    • -
    - -
    - - - - - - - - - - - - - -
    - % if 'admin' in user['permissions']: - - % elif 'write' in user['permissions']: - - - - % endif -
    -
    - - - - - - - - - % if 'write' in user['permissions']: - - % endif - - - -
    - -
    -
    - % endif - % endif % if user['has_read_permissions']: ## Begin Configure Email Notifications % if not node['is_registration']: @@ -531,6 +462,74 @@ % endif ## End Retract Registration + % if enable_institutions: +
    + +
    +

    Project Affiliation / Branding

    +
    +
    +
    + % if 'write' not in user['permissions']: +

    Contributors with read-only permissions to this project cannot add or remove institutional affiliations.

    + % endif: + + Projects can be affiliated with institutions that have created OSF for Institutions accounts. + This allows: +
      +
    • institutional logos to be displayed on public projects
    • +
    • public projects to be discoverable on specific institutional landing pages
    • +
    • single sign-on to the OSF with institutional credentials
    • +
    • FAQ
    • +
    + +
    + + + + + + + + + + + + + +
    + % if 'admin' in user['permissions']: + + % elif 'write' in user['permissions']: + + + + % endif +
    +
    + + + + + + + + + % if 'write' in user['permissions']: + + % endif + + + +
    + +
    +
    + % endif + From d64b11b8ee413b3d09cba5e1609dedeece8f2b81 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 7 Sep 2017 16:36:00 -0400 Subject: [PATCH 023/108] Add relationships endpoint for registrations --- api/institutions/serializers.py | 49 ++++++++++- api/institutions/urls.py | 1 + api/institutions/views.py | 88 ++++++++++++++++++- osf/models/institution.py | 8 ++ .../static/js/institutionProjectSettings.js | 5 +- 5 files changed, 144 insertions(+), 7 deletions(-) diff --git a/api/institutions/serializers.py b/api/institutions/serializers.py index 7b20ccac999..7994ad282ff 100644 --- a/api/institutions/serializers.py +++ b/api/institutions/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers as ser from rest_framework import exceptions -from osf.models import AbstractNode +from osf.models import Node, Registration from website.util import permissions as osf_permissions from api.base.serializers import JSONAPISerializer, RelationshipField, LinksField, JSONAPIRelationshipSerializer, \ @@ -75,9 +75,9 @@ def create(self, validated_data): changes_flag = False for node_dict in node_dicts: - node = AbstractNode.load(node_dict['_id']) + node = Node.load(node_dict['_id']) if not node: - raise exceptions.NotFound(detail='AbstractNode with id "{}" was not found'.format(node_dict['_id'])) + raise exceptions.NotFound(detail='Node with id "{}" was not found'.format(node_dict['_id'])) if not node.has_permission(user, osf_permissions.WRITE): raise exceptions.PermissionDenied(detail='Write permission on node {} required'.format(node_dict['_id'])) if not node.is_affiliated_with_institution(inst): @@ -91,3 +91,46 @@ def create(self, validated_data): 'data': list(inst.nodes.filter(is_deleted=False, type='osf.node')), 'self': inst } + +class RegistrationRelated(JSONAPIRelationshipSerializer): + id = ser.CharField(source='_id', required=False, allow_null=True) + class Meta: + type_ = 'registrations' + +class InstitutionRegistrationsRelationshipSerializer(BaseAPISerializer): + data = ser.ListField(child=RegistrationRelated()) + links = LinksField({'self': 'get_self_url', + 'html': 'get_related_url'}) + + def get_self_url(self, obj): + return obj['self'].registrations_relationship_url + + def get_related_url(self, obj): + return obj['self'].registrations_url + + class Meta: + type_ = 'registrations' + + def create(self, validated_data): + inst = self.context['view'].get_object()['self'] + user = self.context['request'].user + registration_dicts = validated_data['data'] + + changes_flag = False + for registration_dict in registration_dicts: + registration = Registration.load(registration_dict['_id']) + if not registration: + raise exceptions.NotFound(detail='Registration with id "{}" was not found'.format(registration_dict['_id'])) + if not registration.has_permission(user, osf_permissions.WRITE): + raise exceptions.PermissionDenied(detail='Write permission on registration {} required'.format(registration_dict['_id'])) + if not registration.is_affiliated_with_institution(inst): + registration.add_affiliated_institution(inst, user, save=True) + changes_flag = True + + if not changes_flag: + raise RelationshipPostMakesNoChanges + + return { + 'data': list(inst.nodes.filter(is_deleted=False, type='osf.registration')), + 'self': inst + } diff --git a/api/institutions/urls.py b/api/institutions/urls.py index 39714b34231..5cf5c1dcaf4 100644 --- a/api/institutions/urls.py +++ b/api/institutions/urls.py @@ -8,6 +8,7 @@ url(r'^(?P\w+)/$', views.InstitutionDetail.as_view(), name=views.InstitutionDetail.view_name), url(r'^(?P\w+)/nodes/$', views.InstitutionNodeList.as_view(), name=views.InstitutionNodeList.view_name), url(r'^(?P\w+)/registrations/$', views.InstitutionRegistrationList.as_view(), name=views.InstitutionRegistrationList.view_name), + url(r'^(?P\w+)/relationships/registrations/$', views.InstitutionRegistrationsRelationship.as_view(), name=views.InstitutionRegistrationsRelationship.view_name), url(r'^(?P\w+)/relationships/nodes/$', views.InstitutionNodesRelationship.as_view(), name=views.InstitutionNodesRelationship.view_name), url(r'^(?P\w+)/users/$', views.InstitutionUserList.as_view(), name=views.InstitutionUserList.view_name), ] diff --git a/api/institutions/views.py b/api/institutions/views.py index 47757b452da..4230a46a357 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -6,7 +6,7 @@ from framework.auth.oauth_scopes import CoreScopes -from osf.models import OSFUser, Node, Institution +from osf.models import OSFUser, Node, Institution, Registration from website.util import permissions as osf_permissions from api.base import permissions as base_permissions @@ -26,7 +26,7 @@ from api.registrations.serializers import RegistrationSerializer from api.institutions.authentication import InstitutionAuthentication -from api.institutions.serializers import InstitutionSerializer, InstitutionNodesRelationshipSerializer +from api.institutions.serializers import InstitutionSerializer, InstitutionNodesRelationshipSerializer, InstitutionRegistrationsRelationshipSerializer from api.institutions.permissions import UserIsAffiliated class InstitutionMixin(object): @@ -226,6 +226,90 @@ def get_default_queryset(self): def get_queryset(self): return self.get_queryset_from_request() +class InstitutionRegistrationsRelationship(JSONAPIBaseView, generics.RetrieveDestroyAPIView, generics.CreateAPIView, InstitutionMixin): + """ Relationship Endpoint for Institution -> Registrations Relationship + + Used to set, remove, update and retrieve the affiliated_institution of registrations with this institution + + ##Actions + + ###Create + + Method: POST + URL: /links/self + Query Params: + Body (JSON): { + "data": [{ + "type": "registrations", # required + "id": # required + }] + } + Success: 201 + + This requires write permissions on the registrations requested and for the user making the request to + have the institution affiliated in their account. + + ###Destroy + + Method: DELETE + URL: /links/self + Query Params: + Body (JSON): { + "data": [{ + "type": "registrations", # required + "id": # required + }] + } + Success: 204 + + This requires write permissions in the registrations requested. + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + UserIsAffiliated + ) + required_read_scopes = [CoreScopes.NULL] + required_write_scopes = [CoreScopes.NULL] + serializer_class = InstitutionRegistrationsRelationshipSerializer + parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON, ) + + view_category = 'institutions' + view_name = 'institution-relationships-registrations' + + def get_object(self): + inst = self.get_institution() + auth = get_user_auth(self.request) + registrations = inst.nodes.filter(is_deleted=False, type='osf.registration').can_view(user=auth.user, private_link=auth.private_link) + ret = { + 'data': registrations, + 'self': inst + } + self.check_object_permissions(self.request, ret) + return ret + + def perform_destroy(self, instance): + data = self.request.data['data'] + user = self.request.user + ids = [datum['id'] for datum in data] + registrations = [] + for id_ in ids: + registration = Registration.load(id_) + if not registration.has_permission(user, osf_permissions.WRITE): + raise exceptions.PermissionDenied(detail='Write permission on registration {} required'.format(id_)) + registrations.append(registration) + + for registration in registrations: + registration.remove_affiliated_institution(inst=instance['self'], user=user) + registration.save() + + def create(self, *args, **kwargs): + try: + ret = super(InstitutionRegistrationsRelationship, self).create(*args, **kwargs) + except RelationshipPostMakesNoChanges: + return Response(status=status.HTTP_204_NO_CONTENT) + return ret + class InstitutionNodesRelationship(JSONAPIBaseView, generics.RetrieveDestroyAPIView, generics.CreateAPIView, InstitutionMixin): """ Relationship Endpoint for Institution -> Nodes Relationship diff --git a/osf/models/institution.py b/osf/models/institution.py index 6fe8dbff4df..f06e9e45458 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -83,6 +83,14 @@ def nodes_url(self): def nodes_relationship_url(self): return self.absolute_api_v2_url + 'relationships/nodes/' + @property + def registrations_url(self): + return self.absolute_api_v2_url + 'registrations/' + + @property + def registrations_relationship_url(self): + return self.absolute_api_v2_url + 'relationships/registrations/' + @property def logo_path(self): if self.logo_name: diff --git a/website/static/js/institutionProjectSettings.js b/website/static/js/institutionProjectSettings.js index f4bcf944883..54cfb90f822 100644 --- a/website/static/js/institutionProjectSettings.js +++ b/website/static/js/institutionProjectSettings.js @@ -32,6 +32,7 @@ var ViewModel = function(data) { self.nodeId = ko.observable(data.node.id); self.rootId = ko.observable(data.node.rootId); self.childExists = ko.observable(data.node.childExists); + self.isRegistration = ko.observable(data.node.isRegistration); //user chooses to delete all nodes self.modifyChildren = ko.observable(false); @@ -155,9 +156,9 @@ var ViewModel = function(data) { message: '

    Updating affiliation... this may take a minute.

    ', }); var index; - var url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/nodes/'; + var url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/' + (self.isRegistration() ? 'registrations/' : 'nodes/'); var ajaxJSONType = self.isAddInstitution() ? 'POST': 'DELETE'; - var nodesToModify = [{'type': 'nodes', 'id': self.nodeId()}]; + var nodesToModify = [{'type': (self.isRegistration() ? 'registrations' : 'nodes'), 'id': self.nodeId()}]; self.loading(true); if (self.modifyChildren()) { for (var node in self.childNodes()) { From 3e18b80411729823fbbcab575903d56ecb534fe4 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Mon, 11 Sep 2017 16:55:02 -0400 Subject: [PATCH 024/108] Add registration institution relationship tests --- .../test_institution_relationship_nodes.py | 273 +++++++++++++++++- 1 file changed, 272 insertions(+), 1 deletion(-) diff --git a/api_tests/institutions/views/test_institution_relationship_nodes.py b/api_tests/institutions/views/test_institution_relationship_nodes.py index dec5c9820eb..c6becd65f1a 100644 --- a/api_tests/institutions/views/test_institution_relationship_nodes.py +++ b/api_tests/institutions/views/test_institution_relationship_nodes.py @@ -1,8 +1,9 @@ import pytest from api.base.settings.defaults import API_BASE -from framework.auth import Auth from osf_tests.factories import ( + WithdrawnRegistrationFactory, + RegistrationFactory, InstitutionFactory, AuthUserFactory, NodeFactory, @@ -15,6 +16,12 @@ def make_payload(*node_ids): ] return {'data': data} +def make_registration_payload(*node_ids): + data = [ + {'type': 'registrations', 'id': id_} for id_ in node_ids + ] + return {'data': data} + @pytest.mark.django_db class TestInstitutionRelationshipNodes: @@ -323,3 +330,267 @@ def test_delete_user_is_affiliated_with_inst_and_mixed_permissions_on_nodes(self ) assert res.status_code == 403 + + def test_add_non_node(self, app, user, institution, url_institution_nodes): + registration = RegistrationFactory(creator=user, is_public=True) + registration.affiliated_institutions.add(institution) + registration.save() + + res = app.post_json_api( + url_institution_nodes, + make_payload(registration._id), + expect_errors=True, + auth=user.auth + ) + + assert res.status_code == 404 + + +@pytest.mark.django_db +class TestInstitutionRelationshipRegistrations: + + @pytest.fixture() + def institution(self): + return InstitutionFactory() + + @pytest.fixture() + def admin(self, institution): + user = AuthUserFactory() + user.affiliated_institutions.add(institution) + user.save() + return user + + @pytest.fixture() + def registration_no_owner(self): + return RegistrationFactory(is_public=True) + + @pytest.fixture() + def registration_no_affiliation(self, admin): + return RegistrationFactory(creator=admin) + + @pytest.fixture() + def registration_pending(self, admin, institution): + registration = RegistrationFactory(creator=admin) + registration.affiliated_institutions.add(institution) + registration.save() + return registration + + @pytest.fixture() + def registration_public(self, admin, institution): + registration = RegistrationFactory(creator=admin, is_public=True) + registration.affiliated_institutions.add(institution) + registration.save() + return registration + + @pytest.fixture() + def url_institution_registrations(self, institution): + return '/{}institutions/{}/relationships/registrations/'.format(API_BASE, institution._id) + + + def test_auth_get_registrations(self, app, admin, registration_no_owner, registration_no_affiliation, registration_pending, registration_public, url_institution_registrations): + + #test getting registrations without auth (for complete registrations) + res = app.get(url_institution_registrations) + assert res.status_code == 200 + registration_ids = [reg['id'] for reg in res.json['data']] + assert registration_no_affiliation._id not in registration_ids + assert registration_pending._id not in registration_ids + assert registration_no_owner._id not in registration_ids + assert registration_public._id in registration_ids + + #Withdraw a registration, make sure it still shows up + WithdrawnRegistrationFactory(registration=registration_public, user=admin) + + #test getting registrations with auth (for embargoed and pending) + res = app.get(url_institution_registrations, auth=admin.auth) + assert res.status_code == 200 + registration_ids = [reg['id'] for reg in res.json['data']] + assert registration_no_affiliation._id not in registration_ids + assert registration_pending._id in registration_ids + assert registration_public._id in registration_ids + assert registration_no_owner._id not in registration_ids + + def test_add_on_nonexistent_registration(self, app, admin, registration_no_affiliation, url_institution_registrations): + #test registration does not exist + res = app.post_json_api( + url_institution_registrations, + make_registration_payload('notIdatAll'), + expect_errors=True, + auth=admin.auth + ) + + assert res.status_code == 404 + + res = app.post_json_api( + url_institution_registrations, + {'data': [{'type': 'nodes', 'id': registration_no_affiliation._id}]}, + expect_errors=True, + auth=admin.auth + ) + + assert res.status_code == 409 + + def test_add_no_permissions(self, app, registration_no_affiliation, url_institution_registrations, institution): + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + expect_errors=True, + auth = AuthUserFactory().auth + ) + + assert res.status_code == 403 + registration_no_affiliation.reload() + assert institution not in registration_no_affiliation.affiliated_institutions.all() + + def test_add_user_is_admin(self, admin, app, registration_no_affiliation, url_institution_registrations, institution): + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + auth=admin.auth + ) + + assert res.status_code == 201 + registration_no_affiliation.reload() + assert institution in registration_no_affiliation.affiliated_institutions.all() + + def test_add_withdrawn_registration(self, app, url_institution_registrations, admin, registration_no_affiliation, institution): + WithdrawnRegistrationFactory(registration=registration_no_affiliation, user=admin) + + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + auth=admin.auth + ) + + assert res.status_code == 201 + registration_no_affiliation.reload() + assert institution in registration_no_affiliation.affiliated_institutions.all() + + def test_add_user_is_read_write(self, app, registration_no_affiliation, url_institution_registrations, institution): + user = AuthUserFactory() + user.affiliated_institutions.add(institution) + registration_no_affiliation.add_contributor(user) + registration_no_affiliation.save() + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + auth=user.auth + ) + + assert res.status_code == 201 + registration_no_affiliation.reload() + assert institution in registration_no_affiliation.affiliated_institutions.all() + + def test_add_already_added(self, admin, app, registration_pending, url_institution_registrations, institution): + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_pending._id), + auth=admin.auth + ) + + assert res.status_code == 204 + registration_pending.reload() + assert institution in registration_pending.affiliated_institutions.all() + + def test_add_user_is_read_only(self, app, registration_no_affiliation, url_institution_registrations, institution): + user = AuthUserFactory() + user.affiliated_institutions.add(institution) + registration_no_affiliation.add_contributor(user, permissions=[permissions.READ]) + registration_no_affiliation.save() + + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + auth=user.auth, + expect_errors=True + ) + + assert res.status_code == 403 + registration_no_affiliation.reload() + assert institution not in registration_no_affiliation.affiliated_institutions.all() + + def test_add_user_is_admin_but_not_affiliated(self, app, url_institution_registrations, institution): + user = AuthUserFactory() + registration = RegistrationFactory(creator=user) + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration._id), + expect_errors=True, + auth=user.auth + ) + + assert res.status_code == 403 + registration.reload() + assert institution not in registration.affiliated_institutions.all() + + def test_add_some_with_permissions_others_without(self, admin, registration_no_affiliation, registration_no_owner, app, url_institution_registrations, institution): + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_owner._id, registration_no_affiliation._id), + expect_errors=True, + auth=admin.auth + ) + + assert res.status_code == 403 + registration_no_owner.reload() + registration_no_affiliation.reload() + assert institution not in registration_no_owner.affiliated_institutions.all() + assert institution not in registration_no_affiliation.affiliated_institutions.all() + + def test_delete_user_is_admin(self, app, url_institution_registrations, registration_pending, admin, institution): + res = app.delete_json_api( + url_institution_registrations, + make_registration_payload(registration_pending._id), + auth=admin.auth + ) + registration_pending.reload() + assert res.status_code == 204 + assert institution not in registration_pending.affiliated_institutions.all() + + def test_delete_user_is_read_write(self, app, registration_pending, url_institution_registrations, institution): + user = AuthUserFactory() + registration_pending.add_contributor(user) + registration_pending.save() + + res = app.delete_json_api( + url_institution_registrations, + make_registration_payload(registration_pending._id), + auth=user.auth + ) + registration_pending.reload() + + assert res.status_code == 204 + assert institution not in registration_pending.affiliated_institutions.all() + + def test_delete_user_is_read_only(self, registration_pending, app, url_institution_registrations, institution): + user = AuthUserFactory() + registration_pending.add_contributor(user, permissions='read') + registration_pending.save() + + res = app.delete_json_api( + url_institution_registrations, + make_registration_payload(registration_pending._id), + auth=user.auth, + expect_errors=True + ) + registration_pending.reload() + + assert res.status_code == 403 + assert institution in registration_pending.affiliated_institutions.all() + + def test_delete_user_is_admin_but_not_affiliated_with_inst(self, institution, app, url_institution_registrations): + user = AuthUserFactory() + registration = RegistrationFactory(creator=user) + registration.affiliated_institutions.add(institution) + registration.save() + assert institution in registration.affiliated_institutions.all() + + res = app.delete_json_api( + url_institution_registrations, + make_registration_payload(registration._id), + auth=user.auth + ) + + assert res.status_code == 204 + registration.reload() + assert institution not in registration.affiliated_institutions.all() \ No newline at end of file From 87b87809bdbc55c8f44c61d19a737de890b5b6a7 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 12 Sep 2017 09:20:02 -0400 Subject: [PATCH 025/108] Remove redundant check of isRegistration --- .../static/js/institutionProjectSettings.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/website/static/js/institutionProjectSettings.js b/website/static/js/institutionProjectSettings.js index 54cfb90f822..031fb328b34 100644 --- a/website/static/js/institutionProjectSettings.js +++ b/website/static/js/institutionProjectSettings.js @@ -155,18 +155,23 @@ var ViewModel = function(data) { closeButton: false, message: '

    Updating affiliation... this may take a minute.

    ', }); - var index; - var url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/' + (self.isRegistration() ? 'registrations/' : 'nodes/'); - var ajaxJSONType = self.isAddInstitution() ? 'POST': 'DELETE'; - var nodesToModify = [{'type': (self.isRegistration() ? 'registrations' : 'nodes'), 'id': self.nodeId()}]; - self.loading(true); - if (self.modifyChildren()) { - for (var node in self.childNodes()) { - if (self.childNodes()[node].hasPermissions) { - nodesToModify.push({'type': 'nodes', 'id': node}); + var index, url, nodesToModify; + if (self.isRegistration()) { + url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/registrations/'; + nodesToModify = [{'type': 'registrations', 'id': self.nodeId()}]; + } else { + url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/nodes/'; + nodesToModify = [{'type': 'nodes', 'id': self.nodeId()}]; + if (self.modifyChildren()) { + for (var node in self.childNodes()) { + if (self.childNodes()[node].hasPermissions) { + nodesToModify.push({'type': 'nodes', 'id': node}); + } } } } + self.loading(true); + var ajaxJSONType = self.isAddInstitution() ? 'POST': 'DELETE'; return $osf.ajaxJSON( ajaxJSONType, url, From 085d85a65a5d101b00cb3bd1736aa7c654c3c8d4 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 14 Sep 2017 14:23:37 -0400 Subject: [PATCH 026/108] Make settings visable for write contributors --- website/templates/project/project_header.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/templates/project/project_header.mako b/website/templates/project/project_header.mako index 9ccee659d6a..1398e60911d 100644 --- a/website/templates/project/project_header.mako +++ b/website/templates/project/project_header.mako @@ -72,7 +72,7 @@
  • Contributors
  • % endif - % if user['has_read_permissions'] and not node['is_registration'] or (node['is_registration'] and 'admin' in user['permissions']): + % if user['has_read_permissions'] and not node['is_registration'] or (node['is_registration'] and 'write' in user['permissions']):
  • Settings
  • % endif % endif From fc8b8b86cd69d832f5e40469b96e0ef734bd40c7 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 14 Sep 2017 15:20:50 -0400 Subject: [PATCH 027/108] Add user fixture --- .../test_institution_relationship_nodes.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/api_tests/institutions/views/test_institution_relationship_nodes.py b/api_tests/institutions/views/test_institution_relationship_nodes.py index c6becd65f1a..8b058dc3528 100644 --- a/api_tests/institutions/views/test_institution_relationship_nodes.py +++ b/api_tests/institutions/views/test_institution_relationship_nodes.py @@ -360,6 +360,10 @@ def admin(self, institution): user.save() return user + @pytest.fixture() + def user(self, institution): + return AuthUserFactory() + @pytest.fixture() def registration_no_owner(self): return RegistrationFactory(is_public=True) @@ -466,8 +470,7 @@ def test_add_withdrawn_registration(self, app, url_institution_registrations, ad registration_no_affiliation.reload() assert institution in registration_no_affiliation.affiliated_institutions.all() - def test_add_user_is_read_write(self, app, registration_no_affiliation, url_institution_registrations, institution): - user = AuthUserFactory() + def test_add_user_is_read_write(self, app, user, registration_no_affiliation, url_institution_registrations, institution): user.affiliated_institutions.add(institution) registration_no_affiliation.add_contributor(user) registration_no_affiliation.save() @@ -492,8 +495,7 @@ def test_add_already_added(self, admin, app, registration_pending, url_instituti registration_pending.reload() assert institution in registration_pending.affiliated_institutions.all() - def test_add_user_is_read_only(self, app, registration_no_affiliation, url_institution_registrations, institution): - user = AuthUserFactory() + def test_add_user_is_read_only(self, app, user, registration_no_affiliation, url_institution_registrations, institution): user.affiliated_institutions.add(institution) registration_no_affiliation.add_contributor(user, permissions=[permissions.READ]) registration_no_affiliation.save() @@ -509,8 +511,7 @@ def test_add_user_is_read_only(self, app, registration_no_affiliation, url_insti registration_no_affiliation.reload() assert institution not in registration_no_affiliation.affiliated_institutions.all() - def test_add_user_is_admin_but_not_affiliated(self, app, url_institution_registrations, institution): - user = AuthUserFactory() + def test_add_user_is_admin_but_not_affiliated(self, user, app, url_institution_registrations, institution): registration = RegistrationFactory(creator=user) res = app.post_json_api( url_institution_registrations, @@ -547,8 +548,7 @@ def test_delete_user_is_admin(self, app, url_institution_registrations, registra assert res.status_code == 204 assert institution not in registration_pending.affiliated_institutions.all() - def test_delete_user_is_read_write(self, app, registration_pending, url_institution_registrations, institution): - user = AuthUserFactory() + def test_delete_user_is_read_write(self, app, user, registration_pending, url_institution_registrations, institution): registration_pending.add_contributor(user) registration_pending.save() @@ -562,8 +562,7 @@ def test_delete_user_is_read_write(self, app, registration_pending, url_institut assert res.status_code == 204 assert institution not in registration_pending.affiliated_institutions.all() - def test_delete_user_is_read_only(self, registration_pending, app, url_institution_registrations, institution): - user = AuthUserFactory() + def test_delete_user_is_read_only(self, user, registration_pending, app, url_institution_registrations, institution): registration_pending.add_contributor(user, permissions='read') registration_pending.save() @@ -578,8 +577,7 @@ def test_delete_user_is_read_only(self, registration_pending, app, url_instituti assert res.status_code == 403 assert institution in registration_pending.affiliated_institutions.all() - def test_delete_user_is_admin_but_not_affiliated_with_inst(self, institution, app, url_institution_registrations): - user = AuthUserFactory() + def test_delete_user_is_admin_but_not_affiliated_with_inst(self, user, institution, app, url_institution_registrations): registration = RegistrationFactory(creator=user) registration.affiliated_institutions.add(institution) registration.save() @@ -593,4 +591,5 @@ def test_delete_user_is_admin_but_not_affiliated_with_inst(self, institution, ap assert res.status_code == 204 registration.reload() - assert institution not in registration.affiliated_institutions.all() \ No newline at end of file + assert institution not in registration.affiliated_institutions.all() + \ No newline at end of file From 0d3792478c14701da470c9e607365dd9167d2966 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 14 Sep 2017 15:21:33 -0400 Subject: [PATCH 028/108] Remove same line variable declaration --- .../institutions/views/test_institution_relationship_nodes.py | 1 - website/static/js/institutionProjectSettings.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api_tests/institutions/views/test_institution_relationship_nodes.py b/api_tests/institutions/views/test_institution_relationship_nodes.py index 8b058dc3528..33d46952cee 100644 --- a/api_tests/institutions/views/test_institution_relationship_nodes.py +++ b/api_tests/institutions/views/test_institution_relationship_nodes.py @@ -592,4 +592,3 @@ def test_delete_user_is_admin_but_not_affiliated_with_inst(self, user, instituti assert res.status_code == 204 registration.reload() assert institution not in registration.affiliated_institutions.all() - \ No newline at end of file diff --git a/website/static/js/institutionProjectSettings.js b/website/static/js/institutionProjectSettings.js index 031fb328b34..5540ad91784 100644 --- a/website/static/js/institutionProjectSettings.js +++ b/website/static/js/institutionProjectSettings.js @@ -155,7 +155,8 @@ var ViewModel = function(data) { closeButton: false, message: '

    Updating affiliation... this may take a minute.

    ', }); - var index, url, nodesToModify; + var url = ''; + var nodesToModify = []; if (self.isRegistration()) { url = data.apiV2Prefix + 'institutions/' + item.id + '/relationships/registrations/'; nodesToModify = [{'type': 'registrations', 'id': self.nodeId()}]; @@ -183,6 +184,7 @@ var ViewModel = function(data) { fields: {xhrFields: {withCredentials: true}} } ).done(function () { + var index; if (self.isAddInstitution()) { index = self.availableInstitutionsIds().indexOf(item.id); var added = self.availableInstitutions.splice(index, 1)[0]; From 4931cc59f61c6f4afbed398ef2359da5b05a86a6 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Fri, 15 Sep 2017 14:50:42 -0400 Subject: [PATCH 029/108] Consolidate tests --- .../test_institution_relationship_nodes.py | 141 +++++++++--------- 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/api_tests/institutions/views/test_institution_relationship_nodes.py b/api_tests/institutions/views/test_institution_relationship_nodes.py index 33d46952cee..655997d8bbe 100644 --- a/api_tests/institutions/views/test_institution_relationship_nodes.py +++ b/api_tests/institutions/views/test_institution_relationship_nodes.py @@ -70,7 +70,7 @@ def url_institution_nodes(self, institution): return '/{}institutions/{}/relationships/nodes/'.format(API_BASE, institution._id) - def test_auth_get_nodes(self, app, institution, user, node_one, node_public, node_private, url_institution_nodes): + def test_auth_get_nodes(self, app, user, node_one, node_public, node_private, url_institution_nodes): #test_get_nodes_no_auth res = app.get(url_institution_nodes) @@ -364,6 +364,13 @@ def admin(self, institution): def user(self, institution): return AuthUserFactory() + @pytest.fixture() + def affiliated_user(self, institution): + user = AuthUserFactory() + user.affiliated_institutions.add(institution) + user.save() + return user + @pytest.fixture() def registration_no_owner(self): return RegistrationFactory(is_public=True) @@ -414,36 +421,81 @@ def test_auth_get_registrations(self, app, admin, registration_no_owner, registr assert registration_public._id in registration_ids assert registration_no_owner._id not in registration_ids - def test_add_on_nonexistent_registration(self, app, admin, registration_no_affiliation, url_institution_registrations): - #test registration does not exist + def test_add_incorrect_permissions(self, app, admin, user, affiliated_user, registration_no_affiliation, url_institution_registrations, institution): + # No authentication + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + expect_errors=True, + ) + assert res.status_code == 401 + + # User has no permission + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + expect_errors=True, + auth = AuthUserFactory().auth + ) + assert res.status_code == 403 + + # User has read permission + registration_no_affiliation.add_contributor(affiliated_user, permissions=[permissions.READ]) + registration_no_affiliation.save() + + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration_no_affiliation._id), + auth=user.auth, + expect_errors=True + ) + assert res.status_code == 403 + + # User is admin but not affiliated + registration = RegistrationFactory(creator=user) + res = app.post_json_api( + url_institution_registrations, + make_registration_payload(registration._id), + expect_errors=True, + auth=user.auth + ) + assert res.status_code == 403 + registration.reload() + assert institution not in registration.affiliated_institutions.all() + + # Registration does not exist res = app.post_json_api( url_institution_registrations, make_registration_payload('notIdatAll'), expect_errors=True, auth=admin.auth ) - assert res.status_code == 404 + # Attempt to use endpoint on Node res = app.post_json_api( url_institution_registrations, - {'data': [{'type': 'nodes', 'id': registration_no_affiliation._id}]}, + {'data': [{'type': 'nodes', 'id': NodeFactory(creator=admin)._id}]}, expect_errors=True, auth=admin.auth ) - assert res.status_code == 409 - def test_add_no_permissions(self, app, registration_no_affiliation, url_institution_registrations, institution): + registration_no_affiliation.reload() + assert institution not in registration_no_affiliation.affiliated_institutions.all() + + def test_add_some_with_permissions_others_without(self, admin, registration_no_affiliation, registration_no_owner, app, url_institution_registrations, institution): res = app.post_json_api( url_institution_registrations, - make_registration_payload(registration_no_affiliation._id), + make_registration_payload(registration_no_owner._id, registration_no_affiliation._id), expect_errors=True, - auth = AuthUserFactory().auth + auth=admin.auth ) assert res.status_code == 403 + registration_no_owner.reload() registration_no_affiliation.reload() + assert institution not in registration_no_owner.affiliated_institutions.all() assert institution not in registration_no_affiliation.affiliated_institutions.all() def test_add_user_is_admin(self, admin, app, registration_no_affiliation, url_institution_registrations, institution): @@ -470,14 +522,13 @@ def test_add_withdrawn_registration(self, app, url_institution_registrations, ad registration_no_affiliation.reload() assert institution in registration_no_affiliation.affiliated_institutions.all() - def test_add_user_is_read_write(self, app, user, registration_no_affiliation, url_institution_registrations, institution): - user.affiliated_institutions.add(institution) - registration_no_affiliation.add_contributor(user) + def test_add_user_is_read_write(self, app, affiliated_user, registration_no_affiliation, url_institution_registrations, institution): + registration_no_affiliation.add_contributor(affiliated_user) registration_no_affiliation.save() res = app.post_json_api( url_institution_registrations, make_registration_payload(registration_no_affiliation._id), - auth=user.auth + auth=affiliated_user.auth ) assert res.status_code == 201 @@ -495,49 +546,6 @@ def test_add_already_added(self, admin, app, registration_pending, url_instituti registration_pending.reload() assert institution in registration_pending.affiliated_institutions.all() - def test_add_user_is_read_only(self, app, user, registration_no_affiliation, url_institution_registrations, institution): - user.affiliated_institutions.add(institution) - registration_no_affiliation.add_contributor(user, permissions=[permissions.READ]) - registration_no_affiliation.save() - - res = app.post_json_api( - url_institution_registrations, - make_registration_payload(registration_no_affiliation._id), - auth=user.auth, - expect_errors=True - ) - - assert res.status_code == 403 - registration_no_affiliation.reload() - assert institution not in registration_no_affiliation.affiliated_institutions.all() - - def test_add_user_is_admin_but_not_affiliated(self, user, app, url_institution_registrations, institution): - registration = RegistrationFactory(creator=user) - res = app.post_json_api( - url_institution_registrations, - make_registration_payload(registration._id), - expect_errors=True, - auth=user.auth - ) - - assert res.status_code == 403 - registration.reload() - assert institution not in registration.affiliated_institutions.all() - - def test_add_some_with_permissions_others_without(self, admin, registration_no_affiliation, registration_no_owner, app, url_institution_registrations, institution): - res = app.post_json_api( - url_institution_registrations, - make_registration_payload(registration_no_owner._id, registration_no_affiliation._id), - expect_errors=True, - auth=admin.auth - ) - - assert res.status_code == 403 - registration_no_owner.reload() - registration_no_affiliation.reload() - assert institution not in registration_no_owner.affiliated_institutions.all() - assert institution not in registration_no_affiliation.affiliated_institutions.all() - def test_delete_user_is_admin(self, app, url_institution_registrations, registration_pending, admin, institution): res = app.delete_json_api( url_institution_registrations, @@ -548,35 +556,20 @@ def test_delete_user_is_admin(self, app, url_institution_registrations, registra assert res.status_code == 204 assert institution not in registration_pending.affiliated_institutions.all() - def test_delete_user_is_read_write(self, app, user, registration_pending, url_institution_registrations, institution): - registration_pending.add_contributor(user) + def test_delete_user_is_read_write(self, app, affiliated_user, registration_pending, url_institution_registrations, institution): + registration_pending.add_contributor(affiliated_user) registration_pending.save() res = app.delete_json_api( url_institution_registrations, make_registration_payload(registration_pending._id), - auth=user.auth + auth=affiliated_user.auth ) registration_pending.reload() assert res.status_code == 204 assert institution not in registration_pending.affiliated_institutions.all() - def test_delete_user_is_read_only(self, user, registration_pending, app, url_institution_registrations, institution): - registration_pending.add_contributor(user, permissions='read') - registration_pending.save() - - res = app.delete_json_api( - url_institution_registrations, - make_registration_payload(registration_pending._id), - auth=user.auth, - expect_errors=True - ) - registration_pending.reload() - - assert res.status_code == 403 - assert institution in registration_pending.affiliated_institutions.all() - def test_delete_user_is_admin_but_not_affiliated_with_inst(self, user, institution, app, url_institution_registrations): registration = RegistrationFactory(creator=user) registration.affiliated_institutions.add(institution) From ff095e5211fb12f6e82e613dbc64daceef655fbb Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Fri, 15 Sep 2017 16:25:07 -0400 Subject: [PATCH 030/108] Fix core scopes --- api/institutions/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/institutions/views.py b/api/institutions/views.py index 4230a46a357..bf33afa0bfa 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -269,8 +269,8 @@ class InstitutionRegistrationsRelationship(JSONAPIBaseView, generics.RetrieveDes base_permissions.TokenHasScope, UserIsAffiliated ) - required_read_scopes = [CoreScopes.NULL] - required_write_scopes = [CoreScopes.NULL] + required_read_scopes = [CoreScopes.NODE_REGISTRATIONS_READ, CoreScopes.INSTITUTION_READ] + required_write_scopes = [CoreScopes.NODE_REGISTRATIONS_WRITE] serializer_class = InstitutionRegistrationsRelationshipSerializer parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON, ) @@ -353,8 +353,8 @@ class InstitutionNodesRelationship(JSONAPIBaseView, generics.RetrieveDestroyAPIV base_permissions.TokenHasScope, UserIsAffiliated ) - required_read_scopes = [CoreScopes.NULL] - required_write_scopes = [CoreScopes.NULL] + required_read_scopes = [CoreScopes.NODE_BASE_READ, CoreScopes.INSTITUTION_READ] + required_write_scopes = [CoreScopes.NODE_BASE_WRITE] serializer_class = InstitutionNodesRelationshipSerializer parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON, ) From 2c6682dfd8d21ca9c0cf0047222edf4cf606200e Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 30 Aug 2017 09:39:13 -0400 Subject: [PATCH 031/108] preliminary commit before the blocker --- website/static/js/nodesDelete.js | 296 ++++++++++++++++++ website/static/js/nodesDeleteTreebeard.js | 119 +++++++ .../static/js/pages/project-settings-page.js | 5 +- website/static/js/projectSettings.js | 1 + website/templates/project/nodes_delete.mako | 120 +++++++ website/templates/project/settings.mako | 3 +- 6 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 website/static/js/nodesDelete.js create mode 100644 website/static/js/nodesDeleteTreebeard.js create mode 100644 website/templates/project/nodes_delete.mako diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js new file mode 100644 index 00000000000..0cc5e3c6da5 --- /dev/null +++ b/website/static/js/nodesDelete.js @@ -0,0 +1,296 @@ +/** + * Controller for deleting a node and it childs (if they exists) + */ +'use strict'; + +var $ = require('jquery'); +var $3 = window.$3; +var ko = require('knockout'); +var Raven = require('raven-js'); +var $osf = require('./osfHelpers'); +var osfHelpers = require('js/osfHelpers'); +var m = require('mithril'); +var NodesDeleteTreebeard = require('js/nodesDeleteTreebeard'); + +function _flattenNodeTree(nodeTree) { + var ret = []; + var stack = [nodeTree]; + while (stack.length) { + var node = stack.pop(); + ret.push(node); + stack = stack.concat(node.children); + } + return ret; +} + +/** + * take treebeard tree structure of nodes and get a dictionary of parent node and all its + * children + */ +function getNodesOriginal(nodeTree, nodesOriginal) { + var flatNodes = _flattenNodeTree(nodeTree); + $.each(flatNodes, function(_, nodeMeta) { + nodesOriginal[nodeMeta.node.id] = { + public: nodeMeta.node.is_public, + id: nodeMeta.node.id, + title: nodeMeta.node.title, + isAdmin: nodeMeta.node.is_admin, + changed: false + }; + }); + nodesOriginal[nodeTree.node.id].isRoot = true; + return nodesOriginal; +} + +/** + * patches all the nodes in a changed state + * uses API v2 bulk requests + */ +function patchNodesDelete(nodes) { + var nodesV2Url = window.contextVars.apiV2Prefix + 'nodes/'; + var nodesPatch = $.map(nodes, function (node) { + return { + 'type': 'nodes', + 'id': node.id, + 'attributes': { + 'public': node.public + } + }; + }); + //s3 is a very recent version of jQuery that fixes a known bug when used in internet explorer + return $3.ajax({ + url: nodesV2Url, + type: 'DELETE', + dataType: 'json', + contentType: 'application/vnd.api+json; ext=bulk', + crossOrigin: true, + xhrFields: {withCredentials: true}, + processData: false, + data: JSON.stringify({ + data: nodesPatch.reverse() + }) + }); +} + +/** + * view model which corresponds to nodes_delete.mako (#nodesDelete) + * + * @type {NodesPrivacyViewModel} + */ +var NodesDeleteViewModel = function(node, onSetDelete) { + var self = this; + self.WARNING = 'warning'; + self.SELECT = 'select'; + self.CONFIRM = 'confirm'; + + self.onSetDelete = onSetDelete; + + self.parentIsEmbargoed = node.is_embargoed; + self.parentIsPublic = node.is_public; + self.parentNodeType = node.node_type; + self.isPreprint = node.is_preprint; + self.treebeardUrl = window.contextVars.node.urls.api + 'tree/'; + self.nodesOriginal = {}; + self.nodesChanged = ko.observable(); + //state of current nodes + self.nodesState = ko.observableArray(); + self.nodesState.subscribe(function(newValue) { + var nodesChanged = 0; + for (var key in newValue) { + if (newValue[key].public !== self.nodesOriginal[key].public) { + newValue[key].changed = true; + nodesChanged++; + } + else { + newValue[key].changed = false; + } + } + self.nodesChanged(nodesChanged > 0); + m.redraw(true); + }); + //original node state on page load + self.nodesChangedPublic = ko.observableArray([]); + self.nodesChangedPrivate = ko.observableArray([]); + self.hasChildren = ko.observable(false); + $('#nodesDelete').on('hidden.bs.modal', function () { + self.clear(); + }); + + self.page = ko.observable(self.WARNING); + + self.pageTitle = ko.computed(function() { + if (self.page() === self.WARNING && self.parentIsEmbargoed) { + return "This is a message"; + } + + return { + warning: self.parentIsPublic ? + 'Make ' + self.parentNodeType + ' private' : + 'Warning', + select: 'Change privacy settings', + confirm: 'Projects and components affected' + }[self.page()]; + }); + + self.message = ko.computed(function() { + if (self.page() === self.WARNING && self.parentIsEmbargoed) { + return "This is a message" + } + + if (self.page() === self.WARNING && self.isPreprint) { + return "messages" + } + + return { + warning: "messages", + select: "messages", + confirm: "messages" + }[self.page()]; + }); +}; + +/** + * get node tree for treebeard from API V1 + */ +NodesDeleteViewModel.prototype.fetchNodeTree = function() { + var self = this; + + return $.ajax({ + url: self.treebeardUrl, + type: 'GET', + dataType: 'json' + }).done(function(response) { + self.nodesOriginal = getNodesOriginal(response[0], self.nodesOriginal); + var size = 0; + $.each(Object.keys(self.nodesOriginal), function(_, key) { + if (self.nodesOriginal.hasOwnProperty(key)) { + size++; + } + }); + self.hasChildren(size > 1); + var nodesState = $.extend(true, {}, self.nodesOriginal); + var nodeParent = response[0].node.id; + //change node state and response to reflect button push by user on project page (make public | make private) + nodesState[nodeParent].public = response[0].node.is_public = !self.parentIsPublic; + nodesState[nodeParent].changed = true; + self.nodesState(nodesState); + }).fail(function(xhr, status, error) { + $osf.growl('Error', 'Unable to retrieve project settings'); + Raven.captureMessage('Could not GET project settings.', { + extra: { + url: self.treebeardUrl, status: status, error: error + } + }); + }); +}; + +NodesDeleteViewModel.prototype.selectProjects = function() { + this.page(this.SELECT); +}; + +NodesDeleteViewModel.prototype.confirmWarning = function() { + var nodesState = ko.toJS(this.nodesState); + for (var node in nodesState) { + if (nodesState[node].changed) { + if (nodesState[node].public) { + this.nodesChangedPublic().push(nodesState[node].title); + } + else { + this.nodesChangedPrivate().push(nodesState[node].title); + } + } + } + this.page(this.CONFIRM); +}; + +NodesDeleteViewModel.prototype.confirmChanges = function() { + var self = this; + + var nodesState = ko.toJS(this.nodesState()); + nodesState = Object.keys(nodesState).map(function(key) { + return nodesState[key]; + }); + var nodesChanged = nodesState.filter(function(node) { + return node.changed; + }); + //The API's bulk limit is 100 nodes. We catch the exception in nodes_privacy.mako. + if (nodesChanged.length <= 100) { + $osf.block('Deleting Project'); + patchNodesDelete(nodesChanged).then(function () { + self.onSetDelete(nodesChanged); + self.nodesChangedPublic([]); + self.nodesChangedPrivate([]); + self.page(self.WARNING); + window.location.reload(); + }).fail(function (xhr) { + $osf.unblock(); + var errorMessage = 'Unable to update project privacy'; + if (xhr.responseJSON && xhr.responseJSON.errors) { + errorMessage = xhr.responseJSON.errors[0].detail; + } + $osf.growl('Problem changing privacy', errorMessage); + Raven.captureMessage('Could not PATCH project settings.'); + self.clear(); + $('#nodesDelete').modal('hide'); + }).always(function() { + $osf.unblock(); + }); + } +}; + +NodesDeleteViewModel.prototype.clear = function() { + this.nodesChangedPublic([]); + this.nodesChangedPrivate([]); + this.page(this.WARNING); +}; + +NodesDeleteViewModel.prototype.back = function() { + this.nodesChangedPublic([]); + this.nodesChangedPrivate([]); + this.page(this.SELECT); +}; + +NodesDeleteViewModel.prototype.makeEmbargoPublic = function() { + var self = this; + + var nodesChanged = $.map(self.nodesOriginal, function(node) { + if (node.isRoot) { + node.public = true; + return node; + } + return null; + }).filter(Boolean); + $osf.block('Submitting request to end embargo early ...'); + patchNodesDelete(nodesChanged).then(function (res) { + $osf.unblock(); + $('.modal').modal('hide'); + self.onSetPrivacy(nodesChanged, true); + $osf.growl( + 'Email sent', + 'The administrator(s) can approve or cancel the action within 48 hours. If 48 hours pass without any action taken, then the registration will become public.', + 'success' + ); + }); +}; + +function NodesDelete(selector, node, onSetDelete) { + var self = this; + + self.selector = selector; + self.$element = $(self.selector); + self.viewModel = new NodesDeleteViewModel(node, onSetDelete); + self.viewModel.fetchNodeTree().done(function(response) { + new NodesDeleteTreebeard('nodesDeleteTreebeard', response, self.viewModel.nodesState, self.viewModel.nodesOriginal); + }); + self.init(); +} + +NodesDelete.prototype.init = function() { + osfHelpers.applyBindings(this.viewModel, this.selector); +}; + +module.exports = { + _NodesDeleteViewModel: NodesDeleteViewModel, + NodesDelete: NodesDelete +}; + diff --git a/website/static/js/nodesDeleteTreebeard.js b/website/static/js/nodesDeleteTreebeard.js new file mode 100644 index 00000000000..34f8009a8ab --- /dev/null +++ b/website/static/js/nodesDeleteTreebeard.js @@ -0,0 +1,119 @@ +'use strict'; + +var $ = require('jquery'); +var m = require('mithril'); +var ko = require('knockout'); +var Treebeard = require('treebeard'); +var projectSettingsTreebeardBase = require('js/projectSettingsTreebeardBase'); + +function expandOnLoad() { + var tb = this; // jshint ignore: line + for (var i = 0; i < tb.treeData.children.length; i++) { + var parent = tb.treeData.children[i]; + tb.updateFolder(null, parent); + expandChildren(tb, parent.children); + } +} + +function expandChildren(tb, children) { + var openParent = false; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var parent = children[i].parent(); + if (child.children.length > 0) { + expandChildren(tb, child.children); + } + } + if (openParent) { + openAncestors(tb, children[0]); + } +} + +function openAncestors (tb, item) { + var parent = item.parent(); + if(parent && parent.id > 0) { + tb.updateFolder(null, parent); + openAncestors(tb, parent); + } +} + +function NodesDeleteTreebeard(divID, data, nodesState, nodesOriginal) { + /** + * nodesChanged and nodesState are knockout variables. nodesChanged will keep track of the nodes that have + * changed state. nodeState is all the nodes in their current state. + * */ + var tbOptions = $.extend({}, projectSettingsTreebeardBase.defaults, { + divID: divID, + filesData: data, + naturalScrollLimit : 0, + rowHeight : 35, + hScroll : 0, + columnTitles : function() { + return [ + { + title: 'checkBox', + width: '4%', + sortType : 'text', + sort : true + }, + { + title: 'project', + width : '96%', + sortType : 'text', + sort: true + } + ]; + }, + onload : function () { + var tb = this; + expandOnLoad.call(tb); + }, + resolveRows: function nodesDeleteResolveRows(item){ + var tb = this; + var columns = []; + var id = item.data.node.id; + var nodesStateLocal = ko.toJS(nodesState()); + //this lets treebeard know when changes come from the knockout side (select all or select none) + item.data.node.is_public = nodesStateLocal[id].public; + columns.push( + { + data : 'action', + sortInclude : false, + filter : false, + custom : function () { + return m('input[type=checkbox]', { + disabled : !item.data.node.is_admin, + onclick : function() { + item.data.node.is_public = !item.data.node.is_public; + item.open = true; + nodesStateLocal[id].public = item.data.node.is_public; + if (nodesStateLocal[id].public !== nodesOriginal[id].local) { + nodesStateLocal[id].changed = true; + } + else { + nodesStateLocal[id].changed = false; + } + nodesState(nodesStateLocal); + tb.updateFolder(null, item); + }, + checked: nodesState()[id].public + }); + } + }, + { + data: 'project', // Data field name + folderIcons: true, + filter: true, + sortInclude: false, + hideColumnTitles: false, + custom: function () { + return m('span', item.data.node.title); + } + } + ); + return columns; + } + }); + var grid = new Treebeard(tbOptions); +} +module.exports = NodesDeleteTreebeard; diff --git a/website/static/js/pages/project-settings-page.js b/website/static/js/pages/project-settings-page.js index cec30011629..b02a14d477e 100644 --- a/website/static/js/pages/project-settings-page.js +++ b/website/static/js/pages/project-settings-page.js @@ -6,11 +6,14 @@ var Raven = require('raven-js'); var ko = require('knockout'); var ProjectSettings = require('js/projectSettings.js'); +var NodesDelete = require('js/nodesDelete.js'); var InstitutionProjectSettings = require('js/institutionProjectSettings.js'); var $osf = require('js/osfHelpers'); require('css/addonsettings.css'); +var template = require('raw!templates/registration-modal.html'); + var ctx = window.contextVars; @@ -84,7 +87,7 @@ $(document).ready(function() { $('#deleteNode').on('click', function() { if(ctx.node.childExists){ - $osf.growl('Error', 'Any child components must be deleted prior to deleting this project.','danger', 30000); + new NodesDelete.NodesDelete('#nodesDelete', ctx.node) }else{ ProjectSettings.getConfirmationCode(ctx.node.nodeType, ctx.node.isPreprint); } diff --git a/website/static/js/projectSettings.js b/website/static/js/projectSettings.js index fd8574df3a4..c14d049f083 100644 --- a/website/static/js/projectSettings.js +++ b/website/static/js/projectSettings.js @@ -7,6 +7,7 @@ var $osf = require('js/osfHelpers'); var oop = require('js/oop'); var ChangeMessageMixin = require('js/changeMessage'); var language = require('js/osfLanguage').projectSettings; +var NodesDelete = require('js/nodesDelete').NodesDelete; var ProjectSettings = oop.extend( ChangeMessageMixin, diff --git a/website/templates/project/nodes_delete.mako b/website/templates/project/nodes_delete.mako new file mode 100644 index 00000000000..c8f4ec48c25 --- /dev/null +++ b/website/templates/project/nodes_delete.mako @@ -0,0 +1,120 @@ + diff --git a/website/templates/project/settings.mako b/website/templates/project/settings.mako index d2b3358aa1e..d1ebcfd8a2f 100644 --- a/website/templates/project/settings.mako +++ b/website/templates/project/settings.mako @@ -1,4 +1,5 @@ <%inherit file="project/project_base.mako"/> +<%include file="project/nodes_delete.mako"/> <%def name="title()">${node['title']} Settings - + % endif From 7cda2e6ac447f005f242ca9379de669c51e9d3b1 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 31 Aug 2017 11:36:17 -0400 Subject: [PATCH 032/108] supposedly fixed bulk deletion --- api/base/generic_bulk_views.py | 6 +++++- website/static/js/nodesDelete.js | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/base/generic_bulk_views.py b/api/base/generic_bulk_views.py index 4a892bcdf65..fae012b6983 100644 --- a/api/base/generic_bulk_views.py +++ b/api/base/generic_bulk_views.py @@ -76,10 +76,14 @@ def get_requested_resources(self, request, request_data): Retrieves resources in request body """ model_cls = request.parser_context['view'].model_class + requested_ids = [data['id'] for data in request_data] column_name = 'guids___id' if issubclass(model_cls, GuidMixin) else '_id' resource_object_list = model_cls.find(Q(column_name, 'in', requested_ids)) - + if column_name == 'guids___id': + resource_object_list = [resource_object_list.get(guids___id=id) for id in requested_ids] + else: + resource_object_list = [resource_object_list.get(_id=id) for id in requested_ids] for resource in resource_object_list: if getattr(resource, 'is_deleted', None): raise Gone diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 0cc5e3c6da5..f8cefb42e4b 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -57,6 +57,7 @@ function patchNodesDelete(nodes) { } }; }); + //s3 is a very recent version of jQuery that fixes a known bug when used in internet explorer return $3.ajax({ url: nodesV2Url, @@ -67,7 +68,7 @@ function patchNodesDelete(nodes) { xhrFields: {withCredentials: true}, processData: false, data: JSON.stringify({ - data: nodesPatch.reverse() + data: nodesPatch }) }); } @@ -216,7 +217,7 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { //The API's bulk limit is 100 nodes. We catch the exception in nodes_privacy.mako. if (nodesChanged.length <= 100) { $osf.block('Deleting Project'); - patchNodesDelete(nodesChanged).then(function () { + patchNodesDelete(nodesChanged.reverse()).then(function () { self.onSetDelete(nodesChanged); self.nodesChangedPublic([]); self.nodesChangedPrivate([]); From 9ac5a43515cc1c2d4593a90a0d3a7554c9f0a803 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 6 Sep 2017 10:38:16 -0400 Subject: [PATCH 033/108] front-end files --- website/static/js/nodesDelete.js | 148 ++++++++---------- website/static/js/nodesDeleteTreebeard.js | 13 +- .../static/js/pages/project-settings-page.js | 12 +- website/templates/project/nodes_delete.mako | 66 +++----- 4 files changed, 91 insertions(+), 148 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index f8cefb42e4b..4e437354cb1 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -31,7 +31,6 @@ function getNodesOriginal(nodeTree, nodesOriginal) { var flatNodes = _flattenNodeTree(nodeTree); $.each(flatNodes, function(_, nodeMeta) { nodesOriginal[nodeMeta.node.id] = { - public: nodeMeta.node.is_public, id: nodeMeta.node.id, title: nodeMeta.node.title, isAdmin: nodeMeta.node.is_admin, @@ -78,74 +77,71 @@ function patchNodesDelete(nodes) { * * @type {NodesPrivacyViewModel} */ -var NodesDeleteViewModel = function(node, onSetDelete) { +var NodesDeleteViewModel = function(node) { var self = this; - self.WARNING = 'warning'; self.SELECT = 'select'; self.CONFIRM = 'confirm'; - self.onSetDelete = onSetDelete; - + self.confirmationString = ''; self.parentIsEmbargoed = node.is_embargoed; self.parentIsPublic = node.is_public; self.parentNodeType = node.node_type; self.isPreprint = node.is_preprint; self.treebeardUrl = window.contextVars.node.urls.api + 'tree/'; self.nodesOriginal = {}; - self.nodesChanged = ko.observable(); + self.nodesDeleted = ko.observable(); + self.nodesChanged = ko.observableArray([]); //state of current nodes self.nodesState = ko.observableArray(); self.nodesState.subscribe(function(newValue) { - var nodesChanged = 0; + var nodesDeleted = 0; for (var key in newValue) { - if (newValue[key].public !== self.nodesOriginal[key].public) { + if (newValue[key].changed !== self.nodesOriginal[key].changed){ newValue[key].changed = true; - nodesChanged++; + nodesDeleted++; } else { newValue[key].changed = false; } } - self.nodesChanged(nodesChanged > 0); + self.nodesDeleted(nodesDeleted > 0 && nodesDeleted == Object.keys(self.nodesOriginal).length); m.redraw(true); }); //original node state on page load - self.nodesChangedPublic = ko.observableArray([]); - self.nodesChangedPrivate = ko.observableArray([]); self.hasChildren = ko.observable(false); $('#nodesDelete').on('hidden.bs.modal', function () { self.clear(); }); - self.page = ko.observable(self.WARNING); + self.page = ko.observable(self.SELECT); self.pageTitle = ko.computed(function() { if (self.page() === self.WARNING && self.parentIsEmbargoed) { - return "This is a message"; + return "This is a an embargo?"; } return { - warning: self.parentIsPublic ? - 'Make ' + self.parentNodeType + ' private' : - 'Warning', - select: 'Change privacy settings', - confirm: 'Projects and components affected' + select: 'Delete Project', + confirm: 'Delete Project and Components' }[self.page()]; }); self.message = ko.computed(function() { if (self.page() === self.WARNING && self.parentIsEmbargoed) { - return "This is a message" + return "This is an embargo?" } if (self.page() === self.WARNING && self.isPreprint) { - return "messages" + return "You are attempting to delete a preprint." + } + + if (self.page() === self.CONFIRM) { + return "The following project and components will be deleted" } return { - warning: "messages", - select: "messages", - confirm: "messages" + select: "It looks like your project has components within it. To delete this project, you must also delete all child components", + confirm: "The following project and components will be deleted." }[self.page()]; }); }; @@ -171,9 +167,6 @@ NodesDeleteViewModel.prototype.fetchNodeTree = function() { self.hasChildren(size > 1); var nodesState = $.extend(true, {}, self.nodesOriginal); var nodeParent = response[0].node.id; - //change node state and response to reflect button push by user on project page (make public | make private) - nodesState[nodeParent].public = response[0].node.is_public = !self.parentIsPublic; - nodesState[nodeParent].changed = true; self.nodesState(nodesState); }).fail(function(xhr, status, error) { $osf.growl('Error', 'Unable to retrieve project settings'); @@ -193,17 +186,25 @@ NodesDeleteViewModel.prototype.confirmWarning = function() { var nodesState = ko.toJS(this.nodesState); for (var node in nodesState) { if (nodesState[node].changed) { - if (nodesState[node].public) { - this.nodesChangedPublic().push(nodesState[node].title); - } - else { - this.nodesChangedPrivate().push(nodesState[node].title); - } + this.nodesChanged().push(nodesState[node].title) } } + this.confirmationString = $osf.getConfirmationString(); this.page(this.CONFIRM); }; +NodesDeleteViewModel.prototype.selectAll = function() { + var nodesState = ko.toJS(this.nodesState()); + for (var node in nodesState) { + if (nodesState[node].isAdmin) { + nodesState[node].changed = true; + } + } + this.nodesState(nodesState); + m.redraw(true); +}; + + NodesDeleteViewModel.prototype.confirmChanges = function() { var self = this; @@ -214,72 +215,49 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { var nodesChanged = nodesState.filter(function(node) { return node.changed; }); - //The API's bulk limit is 100 nodes. We catch the exception in nodes_privacy.mako. - if (nodesChanged.length <= 100) { - $osf.block('Deleting Project'); - patchNodesDelete(nodesChanged.reverse()).then(function () { - self.onSetDelete(nodesChanged); - self.nodesChangedPublic([]); - self.nodesChangedPrivate([]); - self.page(self.WARNING); - window.location.reload(); - }).fail(function (xhr) { - $osf.unblock(); - var errorMessage = 'Unable to update project privacy'; - if (xhr.responseJSON && xhr.responseJSON.errors) { - errorMessage = xhr.responseJSON.errors[0].detail; - } - $osf.growl('Problem changing privacy', errorMessage); - Raven.captureMessage('Could not PATCH project settings.'); - self.clear(); - $('#nodesDelete').modal('hide'); - }).always(function() { - $osf.unblock(); - }); + + if ($('#bbConfirmText').val() === this.confirmationString) { + if (nodesChanged.length <= 100) { + $osf.block('Deleting Project'); + patchNodesDelete(nodesChanged.reverse()).then(function () { + self.page(self.WARNING); + window.location.reload(); + }).fail(function (xhr) { + $osf.unblock(); + var errorMessage = 'Unable to delete project'; + if (xhr.responseJSON && xhr.responseJSON.errors) { + errorMessage = xhr.responseJSON.errors[0].detail; + } + $osf.growl('Problem delete project', errorMessage); + Raven.captureMessage('Could not batch delete projects.'); + self.clear(); + $('#nodesDelete').modal('hide'); + }).always(function() { + $osf.unblock(); + }); + } + } + else { + $osf.growl('Verification failed', 'Strings did not match'); } }; NodesDeleteViewModel.prototype.clear = function() { - this.nodesChangedPublic([]); - this.nodesChangedPrivate([]); - this.page(this.WARNING); + this.nodesChanged([]); + this.page(this.SELECT); }; NodesDeleteViewModel.prototype.back = function() { - this.nodesChangedPublic([]); - this.nodesChangedPrivate([]); + this.nodesChanged([]); this.page(this.SELECT); }; -NodesDeleteViewModel.prototype.makeEmbargoPublic = function() { - var self = this; - - var nodesChanged = $.map(self.nodesOriginal, function(node) { - if (node.isRoot) { - node.public = true; - return node; - } - return null; - }).filter(Boolean); - $osf.block('Submitting request to end embargo early ...'); - patchNodesDelete(nodesChanged).then(function (res) { - $osf.unblock(); - $('.modal').modal('hide'); - self.onSetPrivacy(nodesChanged, true); - $osf.growl( - 'Email sent', - 'The administrator(s) can approve or cancel the action within 48 hours. If 48 hours pass without any action taken, then the registration will become public.', - 'success' - ); - }); -}; - -function NodesDelete(selector, node, onSetDelete) { +function NodesDelete(selector, node) { var self = this; self.selector = selector; self.$element = $(self.selector); - self.viewModel = new NodesDeleteViewModel(node, onSetDelete); + self.viewModel = new NodesDeleteViewModel(node); self.viewModel.fetchNodeTree().done(function(response) { new NodesDeleteTreebeard('nodesDeleteTreebeard', response, self.viewModel.nodesState, self.viewModel.nodesOriginal); }); diff --git a/website/static/js/nodesDeleteTreebeard.js b/website/static/js/nodesDeleteTreebeard.js index 34f8009a8ab..242a9806f1a 100644 --- a/website/static/js/nodesDeleteTreebeard.js +++ b/website/static/js/nodesDeleteTreebeard.js @@ -74,7 +74,7 @@ function NodesDeleteTreebeard(divID, data, nodesState, nodesOriginal) { var id = item.data.node.id; var nodesStateLocal = ko.toJS(nodesState()); //this lets treebeard know when changes come from the knockout side (select all or select none) - item.data.node.is_public = nodesStateLocal[id].public; + item.data.node.changed = nodesStateLocal[id].changed; columns.push( { data : 'action', @@ -84,19 +84,12 @@ function NodesDeleteTreebeard(divID, data, nodesState, nodesOriginal) { return m('input[type=checkbox]', { disabled : !item.data.node.is_admin, onclick : function() { - item.data.node.is_public = !item.data.node.is_public; item.open = true; - nodesStateLocal[id].public = item.data.node.is_public; - if (nodesStateLocal[id].public !== nodesOriginal[id].local) { - nodesStateLocal[id].changed = true; - } - else { - nodesStateLocal[id].changed = false; - } + nodesStateLocal[id].changed = !nodesStateLocal[id].changed; nodesState(nodesStateLocal); tb.updateFolder(null, item); }, - checked: nodesState()[id].public + checked: nodesState()[id].changed }); } }, diff --git a/website/static/js/pages/project-settings-page.js b/website/static/js/pages/project-settings-page.js index b02a14d477e..116ef2a7f82 100644 --- a/website/static/js/pages/project-settings-page.js +++ b/website/static/js/pages/project-settings-page.js @@ -85,13 +85,13 @@ $(document).ready(function() { ko.applyBindings(projectSettingsVM, $('#projectSettings')[0]); } - $('#deleteNode').on('click', function() { - if(ctx.node.childExists){ - new NodesDelete.NodesDelete('#nodesDelete', ctx.node) - }else{ + if(ctx.node.childExists){ + new NodesDelete.NodesDelete('#nodesDelete', ctx.node) + }else{ + $('#deleteNode').on('click', function() { ProjectSettings.getConfirmationCode(ctx.node.nodeType, ctx.node.isPreprint); - } - }); + }); + } // TODO: Knockout-ify me $('#commentSettings').on('submit', function() { diff --git a/website/templates/project/nodes_delete.mako b/website/templates/project/nodes_delete.mako index c8f4ec48c25..963786d6480 100644 --- a/website/templates/project/nodes_delete.mako +++ b/website/templates/project/nodes_delete.mako @@ -6,14 +6,7 @@ - +
    + Select:  + All components +
    @@ -44,30 +41,15 @@
    -
    - -
    -
    +
    +
    +
    -

    +

    -
      -
    • -

      -
    • -
    -
    -
    -
    -
    -
    -
    -

    -
    -
    -
      +
      • @@ -75,15 +57,15 @@
    +

    + Please note that deleting your project will erase all your project data and this process is IRREVERSIBLE. +

    +

    + Type the following to continue: +

    +
    - -
    - -
    -
    - -
    @@ -95,22 +77,12 @@ Cancel - - - Confirm - - - Continue - Confirm - - - - Continue + Continue - - Confirm + + Delete
    From eebaba53093df6448eaaae7d8567419c4d211904 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 6 Sep 2017 13:18:11 -0400 Subject: [PATCH 034/108] minor fixes --- website/static/js/nodesDelete.js | 39 ++++++------------- .../static/js/pages/project-settings-page.js | 2 +- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 4e437354cb1..9ff6afe8a8f 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -42,18 +42,15 @@ function getNodesOriginal(nodeTree, nodesOriginal) { } /** - * patches all the nodes in a changed state + * Deletes all nodes in changed state * uses API v2 bulk requests */ -function patchNodesDelete(nodes) { +function batchNodesDelete(nodes) { var nodesV2Url = window.contextVars.apiV2Prefix + 'nodes/'; - var nodesPatch = $.map(nodes, function (node) { + var nodesBatch = $.map(nodes, function (node) { return { 'type': 'nodes', 'id': node.id, - 'attributes': { - 'public': node.public - } }; }); @@ -67,7 +64,7 @@ function patchNodesDelete(nodes) { xhrFields: {withCredentials: true}, processData: false, data: JSON.stringify({ - data: nodesPatch + data: nodesBatch }) }); } @@ -75,7 +72,7 @@ function patchNodesDelete(nodes) { /** * view model which corresponds to nodes_delete.mako (#nodesDelete) * - * @type {NodesPrivacyViewModel} + * @type {NodesDeleteViewModel} */ var NodesDeleteViewModel = function(node) { var self = this; @@ -104,7 +101,7 @@ var NodesDeleteViewModel = function(node) { newValue[key].changed = false; } } - self.nodesDeleted(nodesDeleted > 0 && nodesDeleted == Object.keys(self.nodesOriginal).length); + self.nodesDeleted(nodesDeleted > 0 && nodesDeleted === Object.keys(self.nodesOriginal).length); m.redraw(true); }); //original node state on page load @@ -116,10 +113,6 @@ var NodesDeleteViewModel = function(node) { self.page = ko.observable(self.SELECT); self.pageTitle = ko.computed(function() { - if (self.page() === self.WARNING && self.parentIsEmbargoed) { - return "This is a an embargo?"; - } - return { select: 'Delete Project', confirm: 'Delete Project and Components' @@ -127,21 +120,13 @@ var NodesDeleteViewModel = function(node) { }); self.message = ko.computed(function() { - if (self.page() === self.WARNING && self.parentIsEmbargoed) { - return "This is an embargo?" - } - - if (self.page() === self.WARNING && self.isPreprint) { - return "You are attempting to delete a preprint." - } - if (self.page() === self.CONFIRM) { - return "The following project and components will be deleted" + return 'The following project and components will be deleted'; } return { - select: "It looks like your project has components within it. To delete this project, you must also delete all child components", - confirm: "The following project and components will be deleted." + select: 'It looks like your project has components within it. To delete this project, you must also delete all child components', + confirm: 'The following project and components will be deleted.' }[self.page()]; }); }; @@ -186,7 +171,7 @@ NodesDeleteViewModel.prototype.confirmWarning = function() { var nodesState = ko.toJS(this.nodesState); for (var node in nodesState) { if (nodesState[node].changed) { - this.nodesChanged().push(nodesState[node].title) + this.nodesChanged().push(nodesState[node].title); } } this.confirmationString = $osf.getConfirmationString(); @@ -219,7 +204,7 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { if ($('#bbConfirmText').val() === this.confirmationString) { if (nodesChanged.length <= 100) { $osf.block('Deleting Project'); - patchNodesDelete(nodesChanged.reverse()).then(function () { + batchNodesDelete(nodesChanged.reverse()).then(function () { self.page(self.WARNING); window.location.reload(); }).fail(function (xhr) { @@ -228,7 +213,7 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { if (xhr.responseJSON && xhr.responseJSON.errors) { errorMessage = xhr.responseJSON.errors[0].detail; } - $osf.growl('Problem delete project', errorMessage); + $osf.growl('Problem deleting project', errorMessage); Raven.captureMessage('Could not batch delete projects.'); self.clear(); $('#nodesDelete').modal('hide'); diff --git a/website/static/js/pages/project-settings-page.js b/website/static/js/pages/project-settings-page.js index 116ef2a7f82..27d34007001 100644 --- a/website/static/js/pages/project-settings-page.js +++ b/website/static/js/pages/project-settings-page.js @@ -86,7 +86,7 @@ $(document).ready(function() { } if(ctx.node.childExists){ - new NodesDelete.NodesDelete('#nodesDelete', ctx.node) + new NodesDelete.NodesDelete('#nodesDelete', ctx.node); }else{ $('#deleteNode').on('click', function() { ProjectSettings.getConfirmationCode(ctx.node.nodeType, ctx.node.isPreprint); From d4de029b61f7ad84e658c801ac55b47f3c8fc291 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 6 Sep 2017 17:22:46 -0400 Subject: [PATCH 035/108] rearrange some lines to handle situation where nodes are not found --- api/base/generic_bulk_views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/base/generic_bulk_views.py b/api/base/generic_bulk_views.py index fae012b6983..47e8b780d91 100644 --- a/api/base/generic_bulk_views.py +++ b/api/base/generic_bulk_views.py @@ -80,10 +80,7 @@ def get_requested_resources(self, request, request_data): requested_ids = [data['id'] for data in request_data] column_name = 'guids___id' if issubclass(model_cls, GuidMixin) else '_id' resource_object_list = model_cls.find(Q(column_name, 'in', requested_ids)) - if column_name == 'guids___id': - resource_object_list = [resource_object_list.get(guids___id=id) for id in requested_ids] - else: - resource_object_list = [resource_object_list.get(_id=id) for id in requested_ids] + for resource in resource_object_list: if getattr(resource, 'is_deleted', None): raise Gone @@ -91,6 +88,11 @@ def get_requested_resources(self, request, request_data): if len(resource_object_list) != len(request_data): raise ValidationError({'non_field_errors': 'Could not find all objects to delete.'}) + if column_name == 'guids___id': + resource_object_list = [resource_object_list.get(guids___id=id) for id in requested_ids] + else: + resource_object_list = [resource_object_list.get(_id=id) for id in requested_ids] + return resource_object_list def allow_bulk_destroy_resources(self, user, resource_list): From c865865a69609b95e9431a19df0b3564209e7533 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 7 Sep 2017 09:28:40 -0400 Subject: [PATCH 036/108] minor fix to remove lines not needed --- website/static/js/pages/project-settings-page.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/static/js/pages/project-settings-page.js b/website/static/js/pages/project-settings-page.js index 27d34007001..be83ac28a8a 100644 --- a/website/static/js/pages/project-settings-page.js +++ b/website/static/js/pages/project-settings-page.js @@ -12,8 +12,6 @@ var InstitutionProjectSettings = require('js/institutionProjectSettings.js'); var $osf = require('js/osfHelpers'); require('css/addonsettings.css'); -var template = require('raw!templates/registration-modal.html'); - var ctx = window.contextVars; From 6ea7533321728043992aa6e2de7caa9a8639b81b Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 14 Sep 2017 15:10:47 -0400 Subject: [PATCH 037/108] fix confirm delete issue --- website/static/js/nodesDelete.js | 2 +- website/templates/project/nodes_delete.mako | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 9ff6afe8a8f..23e517ba6be 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -201,7 +201,7 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { return node.changed; }); - if ($('#bbConfirmText').val() === this.confirmationString) { + if ($('#bbConfirmTextDelete').val() === this.confirmationString) { if (nodesChanged.length <= 100) { $osf.block('Deleting Project'); batchNodesDelete(nodesChanged.reverse()).then(function () { diff --git a/website/templates/project/nodes_delete.mako b/website/templates/project/nodes_delete.mako index 963786d6480..bb9142b8598 100644 --- a/website/templates/project/nodes_delete.mako +++ b/website/templates/project/nodes_delete.mako @@ -63,7 +63,7 @@

    Type the following to continue:

    - +
    From 82483cf2531d6ade7ce2b99cfeafc250f891e401 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 18 Sep 2017 09:18:24 -0400 Subject: [PATCH 038/108] add page redirection --- website/static/js/nodesDelete.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 23e517ba6be..2f1cfb5a7e9 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -4,7 +4,6 @@ 'use strict'; var $ = require('jquery'); -var $3 = window.$3; var ko = require('knockout'); var Raven = require('raven-js'); var $osf = require('./osfHelpers'); @@ -55,7 +54,7 @@ function batchNodesDelete(nodes) { }); //s3 is a very recent version of jQuery that fixes a known bug when used in internet explorer - return $3.ajax({ + return $.ajax({ url: nodesV2Url, type: 'DELETE', dataType: 'json', @@ -65,7 +64,10 @@ function batchNodesDelete(nodes) { processData: false, data: JSON.stringify({ data: nodesBatch - }) + }), + success: function(){ + window.location.href = '/dashboard/'; + } }); } @@ -82,8 +84,6 @@ var NodesDeleteViewModel = function(node) { self.confirmationString = ''; self.parentIsEmbargoed = node.is_embargoed; self.parentIsPublic = node.is_public; - self.parentNodeType = node.node_type; - self.isPreprint = node.is_preprint; self.treebeardUrl = window.contextVars.node.urls.api + 'tree/'; self.nodesOriginal = {}; self.nodesDeleted = ko.observable(); @@ -206,7 +206,6 @@ NodesDeleteViewModel.prototype.confirmChanges = function() { $osf.block('Deleting Project'); batchNodesDelete(nodesChanged.reverse()).then(function () { self.page(self.WARNING); - window.location.reload(); }).fail(function (xhr) { $osf.unblock(); var errorMessage = 'Unable to delete project'; From 793e54d2c40accc32ecfed3748d071819fd3e883 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 18 Sep 2017 10:41:44 -0400 Subject: [PATCH 039/108] add tooltip --- website/static/js/nodesDeleteTreebeard.js | 29 +++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/website/static/js/nodesDeleteTreebeard.js b/website/static/js/nodesDeleteTreebeard.js index 242a9806f1a..5a4bf74d7e5 100644 --- a/website/static/js/nodesDeleteTreebeard.js +++ b/website/static/js/nodesDeleteTreebeard.js @@ -69,28 +69,37 @@ function NodesDeleteTreebeard(divID, data, nodesState, nodesOriginal) { expandOnLoad.call(tb); }, resolveRows: function nodesDeleteResolveRows(item){ + var tooltips = function(){ + $('[data-toggle="tooltip"]').tooltip(); + }; var tb = this; var columns = []; var id = item.data.node.id; var nodesStateLocal = ko.toJS(nodesState()); //this lets treebeard know when changes come from the knockout side (select all or select none) item.data.node.changed = nodesStateLocal[id].changed; + //A wrapper div for tooltips must exist because tooltips does not work on a disabled element + var tooltipWrapper = item.data.node.is_admin ? 'div' : 'div[data-toggle="tooltip"][title="You must have admin permissions on this component to be able to delete it."][data-placement="right"]'; columns.push( { data : 'action', sortInclude : false, filter : false, custom : function () { - return m('input[type=checkbox]', { - disabled : !item.data.node.is_admin, - onclick : function() { - item.open = true; - nodesStateLocal[id].changed = !nodesStateLocal[id].changed; - nodesState(nodesStateLocal); - tb.updateFolder(null, item); - }, - checked: nodesState()[id].changed - }); + return m(tooltipWrapper, {config: tooltips()}, + [ + m('input[type=checkbox]', { + disabled : !item.data.node.is_admin, + onclick : function() { + item.open = true; + nodesStateLocal[id].changed = !nodesStateLocal[id].changed; + nodesState(nodesStateLocal); + tb.updateFolder(null, item); + }, + checked: nodesState()[id].changed, + }) + ] + ); } }, { From 3139bea4583a81de6ea1a97f3448b3a6ba325a0f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 18 Sep 2017 10:47:05 -0400 Subject: [PATCH 040/108] remove helper text in settings page --- website/templates/project/settings.mako | 5 ----- 1 file changed, 5 deletions(-) diff --git a/website/templates/project/settings.mako b/website/templates/project/settings.mako index d1ebcfd8a2f..02c347ae8b7 100644 --- a/website/templates/project/settings.mako +++ b/website/templates/project/settings.mako @@ -97,11 +97,6 @@ % if 'admin' in user['permissions']:
    -
    - A project cannot be deleted if it has any components within it. - To delete a parent project, you must first delete all child components - by visiting their settings pages. -
    % endif From b371f30f2f3d9a5ad8f43aede4602bb7e169cf29 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 18 Sep 2017 16:06:11 -0400 Subject: [PATCH 041/108] add bootbox --- website/static/js/nodesDelete.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 2f1cfb5a7e9..b3067617288 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -9,6 +9,7 @@ var Raven = require('raven-js'); var $osf = require('./osfHelpers'); var osfHelpers = require('js/osfHelpers'); var m = require('mithril'); +var bootbox = require('bootbox'); var NodesDeleteTreebeard = require('js/nodesDeleteTreebeard'); function _flattenNodeTree(nodeTree) { @@ -66,7 +67,12 @@ function batchNodesDelete(nodes) { data: nodesBatch }), success: function(){ - window.location.href = '/dashboard/'; + bootbox.alert({ + message: 'Project has been successfully deleted.', + callback: function(confirmed) { + window.location.href = '/dashboard/'; + } + }); } }); } @@ -82,8 +88,6 @@ var NodesDeleteViewModel = function(node) { self.CONFIRM = 'confirm'; self.confirmationString = ''; - self.parentIsEmbargoed = node.is_embargoed; - self.parentIsPublic = node.is_public; self.treebeardUrl = window.contextVars.node.urls.api + 'tree/'; self.nodesOriginal = {}; self.nodesDeleted = ko.observable(); From 7f030827f5618cbf36b31ac01393033a525c805f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 09:49:01 -0400 Subject: [PATCH 042/108] added a test --- api_tests/nodes/views/test_node_list.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/api_tests/nodes/views/test_node_list.py b/api_tests/nodes/views/test_node_list.py index 326ac6cda8a..ecd3fe434aa 100644 --- a/api_tests/nodes/views/test_node_list.py +++ b/api_tests/nodes/views/test_node_list.py @@ -2037,6 +2037,14 @@ def public_project_one(self, user_one): def public_project_two(self, user_one): return ProjectFactory(title='Project Two', description='One Three', is_public=True, creator=user_one) + @pytest.fixture() + def public_project_parent(self, user_one): + return ProjectFactory(title='Project with Component', description='Project with component', is_public=True, creator=user_one) + + @pytest.fixture() + def public_component(self, user_one, public_project_parent): + return NodeFactory(parent=public_project_parent, creator=user_one) + @pytest.fixture() def user_one_private_project(self, user_one): return ProjectFactory(title='User One Private Project', is_public=False, creator=user_one) @@ -2245,6 +2253,15 @@ def test_bulk_delete_invalid_payload_one_not_found(self, app, user_one, public_p res = app.get(public_project_one_url, auth=user_one.auth) assert res.status_code == 200 + def test_bulk_delete_project_with_component(self, app, user_one, public_project_parent, public_component, url): + new_payload = {'data': [{'id': public_project_parent._id, 'type': 'nodes'}, {'id': public_component._id, 'type': 'nodes'}]} + res = app.delete_json_api(url, new_payload, auth=user_one.auth, expect_errors=True, bulk=True) + assert res.status_code == 400 + + new_payload = {'data': [{'id': public_component._id, 'type': 'nodes'}, {'id': public_project_parent._id, 'type': 'nodes'}]} + res = app.delete_json_api(url, new_payload, auth=user_one.auth, expect_errors=True, bulk=True) + assert res.status_code == 204 + @pytest.mark.django_db class TestNodeBulkDeleteSkipUneditable: From 45068a742188240af9b76dd04d83cdfc8f48f139 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 11:50:27 -0400 Subject: [PATCH 043/108] add image resize plugin --- webpack.common.config.js | 3 +++ website/static/js/markdown.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/webpack.common.config.js b/webpack.common.config.js index 780f63603e0..4f38d8be958 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -222,5 +222,8 @@ module.exports = { //Dirty hack because mime-type's json file is "special" {test: /db.json/, loader: 'json-loader'} ] + }, + node: { + fs: "empty" } }; diff --git a/website/static/js/markdown.js b/website/static/js/markdown.js index 957a756a640..985a1abaeb1 100644 --- a/website/static/js/markdown.js +++ b/website/static/js/markdown.js @@ -38,6 +38,7 @@ var markdown = new MarkdownIt('commonmark', { .use(require('markdown-it-video')) .use(require('@centerforopenscience/markdown-it-toc')) .use(require('markdown-it-sanitizer')) + .use(require('markdown-it-imsize')) .use(insDel) .enable('table') .use(bootstrapTable) @@ -47,6 +48,7 @@ var markdown = new MarkdownIt('commonmark', { // Fast markdown renderer for active editing to prevent slow loading/rendering tasks var markdownQuick = new MarkdownIt(('commonmark'), { }) .use(require('markdown-it-sanitizer')) + .use(require('markdown-it-imsize')) .disable('link') .disable('image') .use(insDel) @@ -57,6 +59,7 @@ var markdownQuick = new MarkdownIt(('commonmark'), { }) // Markdown renderer for older wikis rendered before switch date var markdownOld = new MarkdownIt(('commonmark'), { }) .use(require('markdown-it-sanitizer')) + .use(require('markdown-it-imsize')) .use(insDel) .enable('table') .use(bootstrapTable) From b3f9d313eeb92230cdd9ca6ac3eb362dd6f5e425 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 13:06:29 -0400 Subject: [PATCH 044/108] add dependency to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index fc73a088f26..a4b6c9d92a3 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "markdown-it": "~4.1.0", "markdown-it-ins-del": "^0.1.1", "markdown-it-sanitizer": "~0.3.0", + "markdown-it-imsize": "^2.0.1", "@centerforopenscience/markdown-it-toc": "~1.1.1", "markdown-it-video": "~0.4.0", "mime-types": "~2.0.12", From 178646c2ec048b8b39a1a68f118e3f182102403d Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 13:54:33 -0400 Subject: [PATCH 045/108] add test config --- website/static/js/tests/tests.webpack.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/static/js/tests/tests.webpack.js b/website/static/js/tests/tests.webpack.js index 54ec600f145..63d68657102 100644 --- a/website/static/js/tests/tests.webpack.js +++ b/website/static/js/tests/tests.webpack.js @@ -12,3 +12,9 @@ context.keys().forEach(context); // Include all files in the addons directory that end with .test.js var addonContext = require.context('../../../../addons/', true, /.*test\.js$/); //make sure you have your directory and regex test set correctly! addonContext.keys().forEach(addonContext); + +module.exports = { + node: { + fs: "empty" + } +}; From c0f4ab5a18e7b768c090247a21fe4d89fbebeb00 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 14:26:14 -0400 Subject: [PATCH 046/108] remove param from request in test --- api_tests/nodes/views/test_node_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_tests/nodes/views/test_node_list.py b/api_tests/nodes/views/test_node_list.py index ecd3fe434aa..37d1d0928aa 100644 --- a/api_tests/nodes/views/test_node_list.py +++ b/api_tests/nodes/views/test_node_list.py @@ -2259,7 +2259,7 @@ def test_bulk_delete_project_with_component(self, app, user_one, public_project_ assert res.status_code == 400 new_payload = {'data': [{'id': public_component._id, 'type': 'nodes'}, {'id': public_project_parent._id, 'type': 'nodes'}]} - res = app.delete_json_api(url, new_payload, auth=user_one.auth, expect_errors=True, bulk=True) + res = app.delete_json_api(url, new_payload, auth=user_one.auth, bulk=True) assert res.status_code == 204 From 9ba3e1e40791d853ed1b846bcefd9a5810e2554b Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 14:39:01 -0400 Subject: [PATCH 047/108] fix quotes --- webpack.common.config.js | 2 +- website/static/js/tests/tests.webpack.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.common.config.js b/webpack.common.config.js index 4f38d8be958..0460b0a83d4 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -224,6 +224,6 @@ module.exports = { ] }, node: { - fs: "empty" + fs: 'empty' } }; diff --git a/website/static/js/tests/tests.webpack.js b/website/static/js/tests/tests.webpack.js index 63d68657102..c21cc5009fe 100644 --- a/website/static/js/tests/tests.webpack.js +++ b/website/static/js/tests/tests.webpack.js @@ -15,6 +15,6 @@ addonContext.keys().forEach(addonContext); module.exports = { node: { - fs: "empty" + fs: 'empty' } }; From 36e384124854d97949e636801007a3ce2d469e9d Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 16:23:14 -0400 Subject: [PATCH 048/108] fix tests --- karma.common.conf.js | 3 +++ website/static/js/tests/tests.webpack.js | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/karma.common.conf.js b/karma.common.conf.js index 0d61ded724b..c093c1be1b0 100644 --- a/karma.common.conf.js +++ b/karma.common.conf.js @@ -32,6 +32,9 @@ var webpackTestConfig = { // Assume test files are ES6 {test: /\.test\.js$/, loader: 'babel-loader'}, ]) + }, + node: { + fs: 'empty' } }; diff --git a/website/static/js/tests/tests.webpack.js b/website/static/js/tests/tests.webpack.js index c21cc5009fe..c430dd12c4a 100644 --- a/website/static/js/tests/tests.webpack.js +++ b/website/static/js/tests/tests.webpack.js @@ -13,8 +13,3 @@ context.keys().forEach(context); var addonContext = require.context('../../../../addons/', true, /.*test\.js$/); //make sure you have your directory and regex test set correctly! addonContext.keys().forEach(addonContext); -module.exports = { - node: { - fs: 'empty' - } -}; From a78099919dce628655d4beef8246f50b5ab55441 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 25 Sep 2017 17:37:00 -0400 Subject: [PATCH 049/108] Remove line --- website/static/js/tests/tests.webpack.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/static/js/tests/tests.webpack.js b/website/static/js/tests/tests.webpack.js index c430dd12c4a..54ec600f145 100644 --- a/website/static/js/tests/tests.webpack.js +++ b/website/static/js/tests/tests.webpack.js @@ -12,4 +12,3 @@ context.keys().forEach(context); // Include all files in the addons directory that end with .test.js var addonContext = require.context('../../../../addons/', true, /.*test\.js$/); //make sure you have your directory and regex test set correctly! addonContext.keys().forEach(addonContext); - From e3de353111f61ebde46381dedc122a8f81789f72 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 27 Sep 2017 12:45:18 -0400 Subject: [PATCH 050/108] Update package.json to maintain alphabetical order --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4b6c9d92a3..e57e7ce45aa 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,9 @@ "lodash.includes": "^4.3.0", "lodash.set": "^4.3.2", "markdown-it": "~4.1.0", + "markdown-it-imsize": "^2.0.1", "markdown-it-ins-del": "^0.1.1", "markdown-it-sanitizer": "~0.3.0", - "markdown-it-imsize": "^2.0.1", "@centerforopenscience/markdown-it-toc": "~1.1.1", "markdown-it-video": "~0.4.0", "mime-types": "~2.0.12", From f5693ccf0af24bc199ad473250b13a13a249f21a Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Mon, 2 Oct 2017 16:36:50 -0400 Subject: [PATCH 051/108] Add custom taxonomy import/export --- admin/preprint_providers/views.py | 9 ++++----- admin_tests/preprint_providers/test_views.py | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/admin/preprint_providers/views.py b/admin/preprint_providers/views.py index ad97906d49b..983647c7795 100644 --- a/admin/preprint_providers/views.py +++ b/admin/preprint_providers/views.py @@ -18,8 +18,7 @@ # When preprint_providers exclusively use Subject relations for creation, set this to False SHOW_TAXONOMIES_IN_PREPRINT_PROVIDER_CREATE = True -#TODO: Add subjects back in when custom taxonomies are fully integrated -FIELDS_TO_NOT_IMPORT_EXPORT = ['access_token', 'share_source', 'subjects_acceptable', 'subjects'] +FIELDS_TO_NOT_IMPORT_EXPORT = ['access_token', 'share_source'] class PreprintProviderList(PermissionRequiredMixin, ListView): @@ -170,7 +169,7 @@ def get(self, request, *args, **kwargs): cleaned_fields = {key: value for key, value in cleaned_data['fields'].iteritems() if key not in FIELDS_TO_NOT_IMPORT_EXPORT} cleaned_fields['licenses_acceptable'] = [node_license.license_id for node_license in preprint_provider.licenses_acceptable.all()] cleaned_fields['default_license'] = preprint_provider.default_license.license_id if preprint_provider.default_license else '' - # cleaned_fields['subjects'] = self.serialize_subjects(preprint_provider) + cleaned_fields['subjects'] = self.serialize_subjects(preprint_provider) cleaned_data['fields'] = cleaned_fields filename = '{}_export.json'.format(preprint_provider.name) response = HttpResponse(json.dumps(cleaned_data), content_type='text/json') @@ -178,7 +177,7 @@ def get(self, request, *args, **kwargs): return response def serialize_subjects(self, provider): - if provider._id != 'osf': + if provider._id != 'osf' and provider.subjects.count(): result = {} result['include'] = [] result['exclude'] = [] @@ -249,7 +248,7 @@ def get_page_provider(self): def add_subjects(self, provider, subject_data): from osf.management.commands.populate_custom_taxonomies import migrate - migrate(provider._id, subject_data) + migrate(provider=provider._id, data=subject_data) def create_or_update_provider(self, provider_data): provider = self.get_page_provider() diff --git a/admin_tests/preprint_providers/test_views.py b/admin_tests/preprint_providers/test_views.py index 70fdd511e51..c522403056a 100644 --- a/admin_tests/preprint_providers/test_views.py +++ b/admin_tests/preprint_providers/test_views.py @@ -250,9 +250,9 @@ def test_export_to_import_new_provider(self): nt.assert_equal(res.status_code, 302) nt.assert_equal(new_provider._id, 'new_id') - # nt.assert_equal(new_provider.subjects.all().count(), 1) + nt.assert_equal(new_provider.subjects.all().count(), 1) nt.assert_equal(new_provider.licenses_acceptable.all().count(), 1) - # nt.assert_equal(new_provider.subjects.all()[0].text, self.subject.text) + nt.assert_equal(new_provider.subjects.all()[0].text, self.subject.text) nt.assert_equal(new_provider.licenses_acceptable.all()[0].license_id, 'NONE') def test_update_provider_existing_subjects(self): @@ -281,9 +281,9 @@ def test_update_provider_existing_subjects(self): nt.assert_equal(res.status_code, 302) nt.assert_equal(new_provider_id, self.preprint_provider.id) - # nt.assert_equal(self.preprint_provider.subjects.all().count(), 1) + nt.assert_equal(self.preprint_provider.subjects.all().count(), 1) nt.assert_equal(self.preprint_provider.licenses_acceptable.all().count(), 1) - # nt.assert_equal(self.preprint_provider.subjects.all()[0].text, self.subject.text) + nt.assert_equal(self.preprint_provider.subjects.all()[0].text, self.subject.text) nt.assert_equal(self.preprint_provider.licenses_acceptable.all()[0].license_id, 'CCBY') From 583d6803ff9c17efd6e6cd0c0a5417044385ed03 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Sun, 15 Oct 2017 02:12:17 -0400 Subject: [PATCH 052/108] Add capability for using SSL with RabbitMQ --- .dockerignore | 1 + .gitignore | 1 + docker-compose.override.yml | 17 +++++++++++++++++ docker-compose.yml | 4 ++++ framework/celery_tasks/__init__.py | 2 ++ website/settings/defaults.py | 1 + website/settings/local-dist.py | 10 ++++++++++ 7 files changed, 36 insertions(+) diff --git a/.dockerignore b/.dockerignore index a25fac9aca5..d3711e80f6f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ node_modules *.pyc **/*.pyc docker* +ssl/ diff --git a/.gitignore b/.gitignore index 5b2dcd5a08b..be81e641324 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,4 @@ scripts/image_maniplation/test_rounded_corners.html docker-compose.override.yml .unison* .docker-sync/ +ssl/ \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml index f18f026836f..9a3603d213e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -82,6 +82,23 @@ services: # volumes: # - emberosf-sync:/code:nocopy +# #################### +# # RabbitMQ SSL +# # Enable this, place the certs in ./ssl, and uncomment the BROKER_USE_SSL dictionary in local.py +# # Uncomment lines under worker in docker-compose +# ##################### +# rabbitmq: +# ports: +# - 5671:5671 +# environment: +# RABBITMQ_SSL_CERTFILE: /etc/ssl/server_certificate.pem +# RABBITMQ_SSL_KEYFILE: /etc/ssl/server_key.pem +# RABBITMQ_SSL_CACERTFILE: /etc/ssl/ca_certificate.pem +# volumes: +# - ./ssl/celery-server.cert.pem:/etc/ssl/server_certificate.pem:ro +# - ./ssl/celery-server.key.pem:/etc/ssl/server_key.pem:ro +# - ./ssl/ca-chain.cert.pem:/etc/ssl/ca_certificate.pem:ro + volumes: osf-sync: external: true diff --git a/docker-compose.yml b/docker-compose.yml index 9ca2b9d0b45..16996ac8e1e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -329,12 +329,16 @@ services: environment: C_FORCE_ROOT: 1 DJANGO_SETTINGS_MODULE: api.base.settings +# BROKER_URL: amqp://guest:guest@192.168.168.167:5671/ env_file: - .docker-compose.env volumes: - osf_requirements_vol:/usr/local/lib/python2.7 - osf_bower_components_vol:/code/website/static/vendor/bower_components - osf_node_modules_vol:/code/node_modules +# - ./ssl/ca-chain.cert.pem:/etc/ssl/certs/ca-chain.cert.pem:ro +# - ./ssl/celery-client.cert.pem:/etc/ssl/certs/worker.pem:ro +# - ./ssl/celery-client.key.pem:/etc/ssl/private/worker.key:ro stdin_open: true admin: diff --git a/framework/celery_tasks/__init__.py b/framework/celery_tasks/__init__.py index e7d1736bfd1..f389437e68a 100644 --- a/framework/celery_tasks/__init__.py +++ b/framework/celery_tasks/__init__.py @@ -17,6 +17,8 @@ client = Client(settings.SENTRY_DSN, release=settings.VERSION, tags={'App': 'celery'}) register_signal(client) +if settings.BROKER_USE_SSL: + app.setup_security() @app.task def error_handler(task_id, task_name): diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 9d69eae4b3e..d28ebddf45c 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -414,6 +414,7 @@ def parent_dir(path): RABBITMQ_VHOST = os.environ.get('RABBITMQ_VHOST', '/') BROKER_URL = os.environ.get('BROKER_URL', 'amqp://{}:{}@{}:{}/{}'.format(RABBITMQ_USERNAME, RABBITMQ_PASSWORD, RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_VHOST)) +BROKER_USE_SSL = False # Default RabbitMQ backend CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', BROKER_URL) diff --git a/website/settings/local-dist.py b/website/settings/local-dist.py index a24c1d5ebca..f144c74071e 100644 --- a/website/settings/local-dist.py +++ b/website/settings/local-dist.py @@ -75,6 +75,16 @@ ## Default RabbitMQ broker BROKER_URL = 'amqp://' +# Celery with SSL +# import ssl +# +# BROKER_USE_SSL = { +# 'keyfile': '/etc/ssl/private/worker.key', +# 'certfile': '/etc/ssl/certs/worker.pem', +# 'ca_certs': '/etc/ssl/certs/ca-chain.cert.pem', +# 'cert_reqs': ssl.CERT_REQUIRED, +# } + # Default RabbitMQ backend CELERY_RESULT_BACKEND = 'amqp://' From 22d6096a6f4f99cafb3b60c5ca010d984392b151 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Wed, 18 Oct 2017 16:51:45 -0400 Subject: [PATCH 053/108] Use notify() and update mixins --- reviews/models/mixins.py | 43 +++++++++++---------------------- website/notifications/emails.py | 6 +++-- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index 6a5ad1ddd15..c003d6a4044 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -18,12 +18,10 @@ from website import settings -from osf.models import NotificationDigest from osf.models import OSFUser from website.mails import mails -from website.notifications.emails import get_user_subscriptions, get_node_lineage -from website.notifications import utils +from website.notifications.emails import notify from website.reviews import signals as reviews_signals @@ -217,13 +215,15 @@ def notify_accept_reject(self, ev): context['template'] = 'reviews_submission_status' context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value - reviews_signals.reviews_email.send(context=context) + context['email_sender'] = self.reviewable.node.creator + reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) def notify_edit_comment(self, ev): context = self.get_context() context['template'] = 'reviews_update_comment' + context['email_sender'] = self.reviewable.node.creator if not self.reviewable.provider.reviews_comments_private and self.action.comment: - reviews_signals.reviews_email.send(context=context) + reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) def get_context(self): return { @@ -238,30 +238,15 @@ def get_context(self): # Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect -def reviews_notification(self, context): - timestamp = timezone.now() - event_type = utils.find_subscription_type('global_reviews') - template = context['template'] + '.html.mako' - for user_id in context['email_recipients']: - user = OSFUser.load(user_id) - subscriptions = get_user_subscriptions(user, event_type) - for notification_type in subscriptions: - check_user_subscribe = subscriptions[notification_type] and user_id in subscriptions[notification_type] and notification_type != 'none' # check if user is subscribed to this type of notifications - if check_user_subscribe: - node_lineage_ids = get_node_lineage(context.get('reviewable').node) if context.get('reviewable').node else [] - context['user'] = user - context['no_future_emails'] = notification_type == 'none' - send_type = notification_type if notification_type != 'none' else 'email_transactional' - message = mails.render_message(template, **context) - digest = NotificationDigest( - user=user, - timestamp=timestamp, - send_type=send_type, - event='global_reviews', - message=message, - node_lineage=node_lineage_ids - ) - digest.save() +def reviews_notification(self, context, node): + user = context['email_sender'] + auth = Auth(user) + time_now = timezone.now() + email_recipients = context['email_recipients'] + email_recipients.remove(user._id) # remove email sender + for user_id in email_recipients: + context['target_user'] = OSFUser.load(user_id) + notify(event='global_reviews', user=auth.user, node=node, timestamp=time_now, **context) # Handle email notifications for a new submission. @reviews_signals.reviews_email_submit.connect diff --git a/website/notifications/emails.py b/website/notifications/emails.py index 7530fddb6a5..2c3ee81e077 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -43,7 +43,8 @@ def notify(event, user, node, timestamp, **context): # If target, they get a reply email and are removed from the general email if target_user and target_user._id in subscriptions[notification_type]: subscriptions[notification_type].remove(target_user._id) - store_emails([target_user._id], notification_type, 'comment_replies', user, node, timestamp, **context) + event_name = 'global_reviews' if event_type == 'global_reviews' else 'comment_replies' + store_emails([target_user._id], notification_type, event_name, user, node, timestamp, **context) sent_users.append(target_user._id) if subscriptions[notification_type]: @@ -87,7 +88,8 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, if notification_type == 'none': return - template = event + '.html.mako' + # Reviews has three email templates for the global_reviews event. + template = context['template'] + '.html.mako' if event == 'global_reviews' else event + '.html.mako' # user whose action triggered email sending context['user'] = user node_lineage_ids = get_node_lineage(node) if node else [] From 643b6b19b960bdfe80ca09231a3d15a7fd20cb52 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Wed, 18 Oct 2017 17:58:46 -0400 Subject: [PATCH 054/108] Update test --- reviews/models/mixins.py | 4 ++-- tests/test_notifications.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index c003d6a4044..73a57213d70 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -21,7 +21,7 @@ from osf.models import OSFUser from website.mails import mails -from website.notifications.emails import notify +from website.notifications import emails from website.reviews import signals as reviews_signals @@ -246,7 +246,7 @@ def reviews_notification(self, context, node): email_recipients.remove(user._id) # remove email sender for user_id in email_recipients: context['target_user'] = OSFUser.load(user_id) - notify(event='global_reviews', user=auth.user, node=node, timestamp=time_now, **context) + emails.notify(event='global_reviews', user=auth.user, node=node, timestamp=time_now, **context) # Handle email notifications for a new submission. @reviews_signals.reviews_email_submit.connect diff --git a/tests/test_notifications.py b/tests/test_notifications.py index e9a4b0b3739..972f9252b8f 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1808,11 +1808,13 @@ def setUp(self): self.provider = factories.PreprintProviderFactory(_id='engrxiv') self.preprint = factories.PreprintFactory(provider=self.provider) self.user = factories.UserFactory() + self.sender = factories.UserFactory() self.context_info = { - 'email_recipients': [self.user._id], + 'email_recipients': [self.sender._id, self.user._id], + 'email_sender': self.sender, 'template': 'test', 'domain': 'osf.io', - 'referrer': self.user, + 'referrer': self.sender, 'reviewable': self.preprint, 'workflow': 'pre-moderation', 'provider_contact_email': 'contact@osf.io', @@ -1846,9 +1848,7 @@ def test_reviews_submit_notification(self, mock_send_email): mixins.reviews_submit_notification(self, context=self.context_info) assert_true(mock_send_email.called) - @mock.patch('website.mails.mails.render_message') - def test_reviews_notification(self, mock_render): - mixins.reviews_notification(self, context=self.context_info) - assert_true(mock_render.called) - template = self.context_info['template'] + '.html.mako' - mock_render.assert_called_with(template, **self.context_info) + @mock.patch('website.notifications.emails.notify') + def test_reviews_notification(self, mock_notify): + mixins.reviews_notification(self, context=self.context_info, node=self.preprint.node) + assert_true(mock_notify.called) From e647177ef1698e9ab4c57027d7846a1749038b5f Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Thu, 19 Oct 2017 16:46:44 -0400 Subject: [PATCH 055/108] Fix import --- reviews/models/mixins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index 73a57213d70..8946e11af37 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -21,6 +21,8 @@ from osf.models import OSFUser from website.mails import mails +from website.notifications.emails import get_user_subscriptions +from website.notifications import utils from website.notifications import emails from website.reviews import signals as reviews_signals From f7f39e7a6038f34119f3d9f4ddace198fa9b51f6 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Thu, 19 Oct 2017 17:28:10 -0400 Subject: [PATCH 056/108] Apply requested changes --- reviews/models/mixins.py | 5 ++--- .../emails/reviews_resubmission_confirmation.html.mako | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index df9b23c7a5a..355dcdb6e2d 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -215,21 +215,19 @@ def notify_submit(self, ev): def notify_resubmit(self, ev): context = self.get_context() context['template'] = 'reviews_resubmission_confirmation' - reviews_signals.reviews_email.send(context=context) + reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) def notify_accept_reject(self, ev): context = self.get_context() context['template'] = 'reviews_submission_status' context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value - context['email_sender'] = self.reviewable.node.creator context['was_pending'] = self.action.from_state == workflow.States.PENDING.value reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) def notify_edit_comment(self, ev): context = self.get_context() context['template'] = 'reviews_update_comment' - context['email_sender'] = self.reviewable.node.creator if not self.reviewable.provider.reviews_comments_private and self.action.comment: reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) @@ -237,6 +235,7 @@ def get_context(self): return { 'domain': settings.DOMAIN, 'email_recipients': [contributor._id for contributor in self.reviewable.node.contributors], + 'email_sender': self.reviewable.node.creator, 'reviewable': self.reviewable, 'workflow': self.reviewable.provider.reviews_workflow, 'provider_url': self.reviewable.provider.domain or '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), diff --git a/website/templates/emails/reviews_resubmission_confirmation.html.mako b/website/templates/emails/reviews_resubmission_confirmation.html.mako index 3e179889e8b..2c644860a2c 100644 --- a/website/templates/emails/reviews_resubmission_confirmation.html.mako +++ b/website/templates/emails/reviews_resubmission_confirmation.html.mako @@ -14,7 +14,8 @@ You will ${'not receive ' if no_future_emails else 'be automatically subscribed to '}future notification emails for this ${reviewable.provider.preprint_word}. Each ${reviewable.provider.preprint_word} is associated with a project on the Open Science Framework for managing the ${reviewable.provider.preprint_word}. - To change your email notification preferences, visit your project user settings. + To change your email notification preferences, visit your + user settings.

    If you have been erroneously associated with "${reviewable.node.title}", then you may visit the From 62c96667787492c40192b1ccee69aa86e4e74c7e Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 20 Oct 2017 15:00:32 -0400 Subject: [PATCH 057/108] Use separate method notify_global_event --- reviews/models/mixins.py | 24 +++++++------- website/notifications/emails.py | 33 ++++++++++--------- ...ns.html.mako => global_mentions.html.mako} | 0 ...ions.txt.mako => global_mentions.txt.mako} | 0 .../reviews_submission_status.html.mako | 2 +- .../emails/reviews_update_comment.html.mako | 2 +- 6 files changed, 32 insertions(+), 29 deletions(-) rename website/templates/emails/{mentions.html.mako => global_mentions.html.mako} (100%) rename website/templates/emails/{mentions.txt.mako => global_mentions.txt.mako} (100%) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index 355dcdb6e2d..381ebe36fee 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -215,7 +215,7 @@ def notify_submit(self, ev): def notify_resubmit(self, ev): context = self.get_context() context['template'] = 'reviews_resubmission_confirmation' - reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) def notify_accept_reject(self, ev): context = self.get_context() @@ -223,19 +223,18 @@ def notify_accept_reject(self, ev): context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value context['was_pending'] = self.action.from_state == workflow.States.PENDING.value - reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) def notify_edit_comment(self, ev): context = self.get_context() context['template'] = 'reviews_update_comment' if not self.reviewable.provider.reviews_comments_private and self.action.comment: - reviews_signals.reviews_email.send(context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) def get_context(self): return { 'domain': settings.DOMAIN, 'email_recipients': [contributor._id for contributor in self.reviewable.node.contributors], - 'email_sender': self.reviewable.node.creator, 'reviewable': self.reviewable, 'workflow': self.reviewable.provider.reviews_workflow, 'provider_url': self.reviewable.provider.domain or '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), @@ -245,15 +244,18 @@ def get_context(self): # Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect -def reviews_notification(self, context, node): - user = context['email_sender'] - auth = Auth(user) +def reviews_notification(self, ev, context, node): + user = ev.kwargs.get('user') time_now = timezone.now() email_recipients = context['email_recipients'] - email_recipients.remove(user._id) # remove email sender - for user_id in email_recipients: - context['target_user'] = OSFUser.load(user_id) - emails.notify(event='global_reviews', user=auth.user, node=node, timestamp=time_now, **context) + emails.notify_global_event( + event='global_reviews', + sender_user=user, + node=node, + timestamp=time_now, + target_users=email_recipients, + **context + ) # Handle email notifications for a new submission. @reviews_signals.reviews_email_submit.connect diff --git a/website/notifications/emails.py b/website/notifications/emails.py index 2c3ee81e077..a6510dc21b2 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -43,8 +43,7 @@ def notify(event, user, node, timestamp, **context): # If target, they get a reply email and are removed from the general email if target_user and target_user._id in subscriptions[notification_type]: subscriptions[notification_type].remove(target_user._id) - event_name = 'global_reviews' if event_type == 'global_reviews' else 'comment_replies' - store_emails([target_user._id], notification_type, event_name, user, node, timestamp, **context) + store_emails([target_user._id], notification_type, 'comment_replies', user, node, timestamp, **context) sent_users.append(target_user._id) if subscriptions[notification_type]: @@ -53,22 +52,23 @@ def notify(event, user, node, timestamp, **context): return sent_users def notify_mentions(event, user, node, timestamp, **context): + new_mentions = context.get('new_mentions', []) + sent_users = notify_global_event(event, user, node, timestamp, new_mentions, **context) + return sent_users + +def notify_global_event(event, sender_user, node, timestamp, target_users, **context): event_type = utils.find_subscription_type(event) sent_users = [] - new_mentions = context.get('new_mentions', []) - for m in new_mentions: - mentioned_user = OSFUser.load(m) - subscriptions = get_user_subscriptions(mentioned_user, event_type) + + for target_id in target_users: + target_user = OSFUser.load(target_id) + subscriptions = get_user_subscriptions(target_user, event_type) for notification_type in subscriptions: - if ( - notification_type != 'none' and - subscriptions[notification_type] and - m in subscriptions[notification_type] - ): - store_emails([m], notification_type, 'mentions', user, node, - timestamp, **context) - sent_users.extend([m]) - return sent_users + if (notification_type != 'none' and subscriptions[notification_type] and target_id in subscriptions[notification_type]): + store_emails([target_id], notification_type, event, sender_user, node, timestamp, **context) + sent_users.extend([target_id]) + + return sent_users, target_users def store_emails(recipient_ids, notification_type, event, user, node, timestamp, **context): @@ -88,7 +88,7 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, if notification_type == 'none': return - # Reviews has three email templates for the global_reviews event. + # Reviews has multiple email templates for the global_reviews event. template = context['template'] + '.html.mako' if event == 'global_reviews' else event + '.html.mako' # user whose action triggered email sending context['user'] = user @@ -99,6 +99,7 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, continue recipient = OSFUser.load(recipient_id) context['localized_timestamp'] = localize_timestamp(timestamp, recipient) + context['recipient'] = recipient message = mails.render_message(template, **context) digest = NotificationDigest( diff --git a/website/templates/emails/mentions.html.mako b/website/templates/emails/global_mentions.html.mako similarity index 100% rename from website/templates/emails/mentions.html.mako rename to website/templates/emails/global_mentions.html.mako diff --git a/website/templates/emails/mentions.txt.mako b/website/templates/emails/global_mentions.txt.mako similarity index 100% rename from website/templates/emails/mentions.txt.mako rename to website/templates/emails/global_mentions.txt.mako diff --git a/website/templates/emails/reviews_submission_status.html.mako b/website/templates/emails/reviews_submission_status.html.mako index ef83997f850..d21c89b345e 100644 --- a/website/templates/emails/reviews_submission_status.html.mako +++ b/website/templates/emails/reviews_submission_status.html.mako @@ -1,6 +1,6 @@ ## -*- coding: utf-8 -*-

    -

    Hello ${user.fullname},

    +

    Hello ${recipient.fullname},

    % if workflow == 'pre-moderation': Your submission "${reviewable.node.title}", submitted to ${reviewable.provider.name} has diff --git a/website/templates/emails/reviews_update_comment.html.mako b/website/templates/emails/reviews_update_comment.html.mako index 7d3c405037b..0eac0aedaf2 100644 --- a/website/templates/emails/reviews_update_comment.html.mako +++ b/website/templates/emails/reviews_update_comment.html.mako @@ -1,6 +1,6 @@ ## -*- coding: utf-8 -*-

    -

    Hello ${user.fullname},

    +

    Hello ${recipient.fullname},

    Your ${reviewable.provider.preprint_word} "${reviewable.node.title}" has an updated comment by the moderator. To view the comment, go to your ${reviewable.provider.preprint_word}. From bad1b0c9ed725f2ba0dd27c919d2a345af48fd91 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 20 Oct 2017 15:33:39 -0400 Subject: [PATCH 058/108] Update --- reviews/models/mixins.py | 11 +++++------ tests/test_notifications.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index 381ebe36fee..baa0d4e5d25 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -215,7 +215,7 @@ def notify_submit(self, ev): def notify_resubmit(self, ev): context = self.get_context() context['template'] = 'reviews_resubmission_confirmation' - reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) def notify_accept_reject(self, ev): context = self.get_context() @@ -223,13 +223,13 @@ def notify_accept_reject(self, ev): context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value context['was_pending'] = self.action.from_state == workflow.States.PENDING.value - reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) def notify_edit_comment(self, ev): context = self.get_context() context['template'] = 'reviews_update_comment' if not self.reviewable.provider.reviews_comments_private and self.action.comment: - reviews_signals.reviews_email.send(ev=ev, context=context, node=self.reviewable.node) + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) def get_context(self): return { @@ -244,13 +244,12 @@ def get_context(self): # Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect -def reviews_notification(self, ev, context, node): - user = ev.kwargs.get('user') +def reviews_notification(self, creator, context, node): time_now = timezone.now() email_recipients = context['email_recipients'] emails.notify_global_event( event='global_reviews', - sender_user=user, + sender_user=creator, node=node, timestamp=time_now, target_users=email_recipients, diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 972f9252b8f..38cebe57a74 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1848,7 +1848,7 @@ def test_reviews_submit_notification(self, mock_send_email): mixins.reviews_submit_notification(self, context=self.context_info) assert_true(mock_send_email.called) - @mock.patch('website.notifications.emails.notify') + @mock.patch('website.notifications.emails.notify_global_event') def test_reviews_notification(self, mock_notify): - mixins.reviews_notification(self, context=self.context_info, node=self.preprint.node) + mixins.reviews_notification(self, creator=self.sender, context=self.context_info, node=self.preprint.node) assert_true(mock_notify.called) From 4ead0c3deb0692e3b11dfd39cd9623e9f76ea8bd Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 20 Oct 2017 16:50:01 -0400 Subject: [PATCH 059/108] remove extra return --- website/notifications/emails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index a6510dc21b2..d44b776ec8b 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -68,7 +68,7 @@ def notify_global_event(event, sender_user, node, timestamp, target_users, **con store_emails([target_id], notification_type, event, sender_user, node, timestamp, **context) sent_users.extend([target_id]) - return sent_users, target_users + return sent_users def store_emails(recipient_ids, notification_type, event, user, node, timestamp, **context): From 79c46c16d6ca5ea27f8cf697fcf7919710bd90a9 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 20 Oct 2017 16:59:08 -0400 Subject: [PATCH 060/108] update test --- tests/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 38cebe57a74..2420553c084 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1476,7 +1476,7 @@ def test_notify_mentions_does_send_to_mentioned_users(self, mock_store): time_now = timezone.now() emails.notify_mentions('global_mentions', user=user, node=node, timestamp=time_now, new_mentions=[user._id]) assert_true(mock_store.called) - mock_store.assert_called_with([node.creator._id], 'email_transactional', 'mentions', user, + mock_store.assert_called_with([node.creator._id], 'email_transactional', 'global_mentions', user, node, time_now, new_mentions=[node.creator._id]) @mock.patch('website.notifications.emails.store_emails') From d9c9d29b162d8e64aa831f6853382fab4b0b0a30 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Mon, 23 Oct 2017 09:56:45 -0400 Subject: [PATCH 061/108] Use format --- website/notifications/emails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index d44b776ec8b..aee94564c27 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -89,7 +89,7 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, return # Reviews has multiple email templates for the global_reviews event. - template = context['template'] + '.html.mako' if event == 'global_reviews' else event + '.html.mako' + template = '{template}.html.mako'.format(template=context['template']) if event == 'global_reviews' else '{event}.html.mako'.format(event=event) # user whose action triggered email sending context['user'] = user node_lineage_ids = get_node_lineage(node) if node else [] From 1bbae26e041e16cdc78d9237f18a6e9addf36f22 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 09:59:29 -0400 Subject: [PATCH 062/108] Add original_data_published field --- api/nodes/views.py | 1 + api/preprint_providers/views.py | 1 + api/preprints/serializers.py | 6 ++++++ api/preprints/views.py | 2 ++ ...preprintservice_original_date_published.py | 21 +++++++++++++++++++ osf/models/preprint_service.py | 1 + 6 files changed, 32 insertions(+) create mode 100644 osf/migrations/0065_preprintservice_original_date_published.py diff --git a/api/nodes/views.py b/api/nodes/views.py index 7c061c0238f..d7e462a10e8 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -3396,6 +3396,7 @@ class NodePreprintsList(JSONAPIBaseView, generics.ListAPIView, NodeMixin, Prepri date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published + original_date_published iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects list of lists of dictionaries ids of Subject in the BePress taxonomy. Dictrionary, containing the subject text and subject ID diff --git a/api/preprint_providers/views.py b/api/preprint_providers/views.py index 9d1b6197180..b6c36f62398 100644 --- a/api/preprint_providers/views.py +++ b/api/preprint_providers/views.py @@ -216,6 +216,7 @@ class PreprintProviderPreprintList(JSONAPIBaseView, generics.ListAPIView, Prepri date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published + original_date_published iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects array of tuples of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 74d3c04a72d..699d0a86a57 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -61,6 +61,7 @@ class PreprintSerializer(JSONAPISerializer): 'date_created', 'date_modified', 'date_published', + 'original_date_published', 'provider', 'is_published', 'subjects', @@ -73,6 +74,7 @@ class PreprintSerializer(JSONAPISerializer): date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) date_published = DateByVersion(read_only=True) + original_date_published = DateByVersion(required=False) doi = ser.CharField(source='article_doi', required=False, allow_null=True) is_published = ser.BooleanField(required=False) is_preprint_orphan = ser.BooleanField(read_only=True) @@ -231,6 +233,10 @@ def update(self, preprint, validated_data): self.set_field(preprint.set_preprint_license, license_details, auth) save_preprint = True + if 'original_date_published' in validated_data: + preprint.original_date_published = validated_data['original_date_published'] + save_preprint = True + if published is not None: if not preprint.primary_file: raise exceptions.ValidationError(detail='A valid primary_file must be set before publishing a preprint.') diff --git a/api/preprints/views.py b/api/preprints/views.py index b752a3888cd..4964d0971be 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -73,6 +73,7 @@ class PreprintList(JSONAPIBaseView, generics.ListCreateAPIView, PreprintFilterMi date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published + original_date_published iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects list of lists of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID @@ -195,6 +196,7 @@ class PreprintDetail(JSONAPIBaseView, generics.RetrieveUpdateDestroyAPIView, Pre date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published + original_date_published iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects array of tuples of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID diff --git a/osf/migrations/0065_preprintservice_original_date_published.py b/osf/migrations/0065_preprintservice_original_date_published.py new file mode 100644 index 00000000000..827c4b16de6 --- /dev/null +++ b/osf/migrations/0065_preprintservice_original_date_published.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-24 13:58 +from __future__ import unicode_literals + +from django.db import migrations +import osf.utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0064_auto_20171019_0918'), + ] + + operations = [ + migrations.AddField( + model_name='preprintservice', + name='original_date_published', + field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), + ), + ] diff --git a/osf/models/preprint_service.py b/osf/models/preprint_service.py index bc54e2e43f4..180e30fe10e 100644 --- a/osf/models/preprint_service.py +++ b/osf/models/preprint_service.py @@ -36,6 +36,7 @@ class PreprintService(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMi null=True, blank=True, db_index=True) is_published = models.BooleanField(default=False, db_index=True) date_published = NonNaiveDateTimeField(null=True, blank=True) + original_date_published = NonNaiveDateTimeField(null=True, blank=True) license = models.ForeignKey('osf.NodeLicenseRecord', on_delete=models.SET_NULL, null=True, blank=True) From 3ba8503b465559bcd5ac4787ab786d1e6acc82a6 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 11:06:32 -0400 Subject: [PATCH 063/108] Make citations use original_date_published --- osf/models/node.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osf/models/node.py b/osf/models/node.py index c06d0b16a52..efa1c944847 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -656,7 +656,12 @@ def csl(self): # formats node information into CSL format for citation parsing csl['DOI'] = doi if self.logs.exists(): - csl['issued'] = datetime_to_csl(self.logs.latest().date) + citation_date = self.logs.latest().date + + if self.is_preprint and self.preprints.get_queryset()[0].original_date_published: + citation_date = self.preprints.get_queryset()[0].original_date_published + + csl['issued'] = datetime_to_csl(citation_date) return csl From 0c844d2d267d98b07d4deeb53d4e08ab66067032 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 12:18:04 -0400 Subject: [PATCH 064/108] Add citation tests --- api/preprints/serializers.py | 1 - .../views/test_preprint_citations.py | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 699d0a86a57..dceb63fe068 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -61,7 +61,6 @@ class PreprintSerializer(JSONAPISerializer): 'date_created', 'date_modified', 'date_published', - 'original_date_published', 'provider', 'is_published', 'subjects', diff --git a/api_tests/preprints/views/test_preprint_citations.py b/api_tests/preprints/views/test_preprint_citations.py index 9a859adcef3..973a2f879a5 100644 --- a/api_tests/preprints/views/test_preprint_citations.py +++ b/api_tests/preprints/views/test_preprint_citations.py @@ -4,6 +4,7 @@ from nose.tools import * # flake8: noqa from osf_tests.factories import AuthUserFactory, PreprintFactory from tests.base import ApiTestCase +from datetime import datetime class PreprintCitationsMixin(object): @@ -49,9 +50,23 @@ def test_citation_url_is_preprint_url_not_project(self): assert_equal(res.json['data']['links']['self'], display_absolute_url(self.published_preprint)) -class TestPreprintCitationsStyle(PreprintCitationsMixin, ApiTestCase): +class TestPreprintCitationContent(PreprintCitationsMixin, ApiTestCase): def setUp(self): - super(TestPreprintCitationsStyle, self).setUp() + super(TestPreprintCitationContent, self).setUp() self.published_preprint_url = '/{}preprints/{}/citation/apa/'.format(API_BASE, self.published_preprint._id) self.unpublished_preprint_url = '/{}preprints/{}/citation/apa/'.format(API_BASE, self.unpublished_preprint._id) + + def test_citation_contains_correct_date(self): + res = self.app.get(self.published_preprint_url) + assert_equal(res.status_code, 200) + expected_date = self.published_preprint.date_modified.strftime('%Y, %B %-d') + assert_true(expected_date in res.json['data']['attributes']['citation']) + + self.published_preprint.original_date_published = datetime(2017, 12, 24) + self.published_preprint.save() + + res = self.app.get(self.published_preprint_url) + assert_equal(res.status_code, 200) + expected_date = self.published_preprint.original_date_published.strftime('%Y, %B %-d') + assert_true(expected_date in res.json['data']['attributes']['citation']) From e450b6af091aedb96da05d24a7ecbd07f576f9df Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 13:32:39 -0400 Subject: [PATCH 065/108] Change from original_date_published to original_publication_date --- api/nodes/views.py | 2 +- api/preprint_providers/views.py | 2 +- api/preprints/serializers.py | 6 +++--- api/preprints/views.py | 4 ++-- api_tests/preprints/views/test_preprint_citations.py | 4 ++-- ...py => 0065_preprintservice_original_publication_date.py} | 4 ++-- osf/models/node.py | 4 ++-- osf/models/preprint_service.py | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) rename osf/migrations/{0065_preprintservice_original_date_published.py => 0065_preprintservice_original_publication_date.py} (81%) diff --git a/api/nodes/views.py b/api/nodes/views.py index d7e462a10e8..32dfa688de2 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -3396,7 +3396,7 @@ class NodePreprintsList(JSONAPIBaseView, generics.ListAPIView, NodeMixin, Prepri date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published - original_date_published iso8601 timestamp user-entered date of publication from external posting + original_publication_date iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects list of lists of dictionaries ids of Subject in the BePress taxonomy. Dictrionary, containing the subject text and subject ID diff --git a/api/preprint_providers/views.py b/api/preprint_providers/views.py index b6c36f62398..107d2f3912c 100644 --- a/api/preprint_providers/views.py +++ b/api/preprint_providers/views.py @@ -216,7 +216,7 @@ class PreprintProviderPreprintList(JSONAPIBaseView, generics.ListAPIView, Prepri date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published - original_date_published iso8601 timestamp user-entered date of publication from external posting + original_publication_date iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects array of tuples of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index dceb63fe068..6fdf93aa6ff 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -73,7 +73,7 @@ class PreprintSerializer(JSONAPISerializer): date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) date_published = DateByVersion(read_only=True) - original_date_published = DateByVersion(required=False) + original_publication_date = DateByVersion(required=False) doi = ser.CharField(source='article_doi', required=False, allow_null=True) is_published = ser.BooleanField(required=False) is_preprint_orphan = ser.BooleanField(read_only=True) @@ -232,8 +232,8 @@ def update(self, preprint, validated_data): self.set_field(preprint.set_preprint_license, license_details, auth) save_preprint = True - if 'original_date_published' in validated_data: - preprint.original_date_published = validated_data['original_date_published'] + if 'original_publication_date' in validated_data: + preprint.original_publication_date = validated_data['original_publication_date'] save_preprint = True if published is not None: diff --git a/api/preprints/views.py b/api/preprints/views.py index 4964d0971be..ee1a858f08e 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -73,7 +73,7 @@ class PreprintList(JSONAPIBaseView, generics.ListCreateAPIView, PreprintFilterMi date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published - original_date_published iso8601 timestamp user-entered date of publication from external posting + original_publication_date iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects list of lists of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID @@ -196,7 +196,7 @@ class PreprintDetail(JSONAPIBaseView, generics.RetrieveUpdateDestroyAPIView, Pre date_created iso8601 timestamp timestamp that the preprint was created date_modified iso8601 timestamp timestamp that the preprint was last modified date_published iso8601 timestamp timestamp when the preprint was published - original_date_published iso8601 timestamp user-entered date of publication from external posting + original_publication_date iso8601 timestamp user-entered date of publication from external posting is_published boolean whether or not this preprint is published is_preprint_orphan boolean whether or not this preprint is orphaned subjects array of tuples of dictionaries ids of Subject in the BePress taxonomy. Dictionary, containing the subject text and subject ID diff --git a/api_tests/preprints/views/test_preprint_citations.py b/api_tests/preprints/views/test_preprint_citations.py index 973a2f879a5..9516ea73091 100644 --- a/api_tests/preprints/views/test_preprint_citations.py +++ b/api_tests/preprints/views/test_preprint_citations.py @@ -63,10 +63,10 @@ def test_citation_contains_correct_date(self): expected_date = self.published_preprint.date_modified.strftime('%Y, %B %-d') assert_true(expected_date in res.json['data']['attributes']['citation']) - self.published_preprint.original_date_published = datetime(2017, 12, 24) + self.published_preprint.original_publication_date = datetime(2017, 12, 24) self.published_preprint.save() res = self.app.get(self.published_preprint_url) assert_equal(res.status_code, 200) - expected_date = self.published_preprint.original_date_published.strftime('%Y, %B %-d') + expected_date = self.published_preprint.original_publication_date.strftime('%Y, %B %-d') assert_true(expected_date in res.json['data']['attributes']['citation']) diff --git a/osf/migrations/0065_preprintservice_original_date_published.py b/osf/migrations/0065_preprintservice_original_publication_date.py similarity index 81% rename from osf/migrations/0065_preprintservice_original_date_published.py rename to osf/migrations/0065_preprintservice_original_publication_date.py index 827c4b16de6..fa5d4328f82 100644 --- a/osf/migrations/0065_preprintservice_original_date_published.py +++ b/osf/migrations/0065_preprintservice_original_publication_date.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-10-24 13:58 +# Generated by Django 1.11.4 on 2017-10-24 17:29 from __future__ import unicode_literals from django.db import migrations @@ -15,7 +15,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='preprintservice', - name='original_date_published', + name='original_publication_date', field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), ), ] diff --git a/osf/models/node.py b/osf/models/node.py index efa1c944847..0e38a6c824f 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -658,8 +658,8 @@ def csl(self): # formats node information into CSL format for citation parsing if self.logs.exists(): citation_date = self.logs.latest().date - if self.is_preprint and self.preprints.get_queryset()[0].original_date_published: - citation_date = self.preprints.get_queryset()[0].original_date_published + if self.is_preprint and self.preprints.get_queryset()[0].original_publication_date: + citation_date = self.preprints.get_queryset()[0].original_publication_date csl['issued'] = datetime_to_csl(citation_date) diff --git a/osf/models/preprint_service.py b/osf/models/preprint_service.py index 180e30fe10e..03093a7551d 100644 --- a/osf/models/preprint_service.py +++ b/osf/models/preprint_service.py @@ -36,7 +36,7 @@ class PreprintService(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMi null=True, blank=True, db_index=True) is_published = models.BooleanField(default=False, db_index=True) date_published = NonNaiveDateTimeField(null=True, blank=True) - original_date_published = NonNaiveDateTimeField(null=True, blank=True) + original_publication_date = NonNaiveDateTimeField(null=True, blank=True) license = models.ForeignKey('osf.NodeLicenseRecord', on_delete=models.SET_NULL, null=True, blank=True) From ee56b5d4d96e1ba1a05a4c0852b5b6e08116a47e Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 16:13:12 -0400 Subject: [PATCH 066/108] Don't use is_preprint --- osf/models/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osf/models/node.py b/osf/models/node.py index 0e38a6c824f..8f09bdbcec5 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -658,7 +658,7 @@ def csl(self): # formats node information into CSL format for citation parsing if self.logs.exists(): citation_date = self.logs.latest().date - if self.is_preprint and self.preprints.get_queryset()[0].original_publication_date: + if self.preprints.exists() and self.preprints.first().original_publication_date: citation_date = self.preprints.get_queryset()[0].original_publication_date csl['issued'] = datetime_to_csl(citation_date) From 03d1817df288a6a6ec04ad39b602e446f88e2412 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 24 Oct 2017 16:18:17 -0400 Subject: [PATCH 067/108] Add filtering by original_publication_date --- api/preprints/serializers.py | 1 + api_tests/preprints/filters/test_filters.py | 29 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 6fdf93aa6ff..dfc387f235e 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -61,6 +61,7 @@ class PreprintSerializer(JSONAPISerializer): 'date_created', 'date_modified', 'date_published', + 'original_publication_date', 'provider', 'is_published', 'subjects', diff --git a/api_tests/preprints/filters/test_filters.py b/api_tests/preprints/filters/test_filters.py index 6cb582ec8c6..85457847a44 100644 --- a/api_tests/preprints/filters/test_filters.py +++ b/api_tests/preprints/filters/test_filters.py @@ -55,13 +55,17 @@ def subject_two(self): @pytest.fixture() def preprint_one(self, user, project_one, provider_one, subject_one): - return PreprintFactory(creator=user, project=project_one, provider=provider_one, subjects=[[subject_one._id]]) + preprint_one = PreprintFactory(creator=user, project=project_one, provider=provider_one, subjects=[[subject_one._id]]) + preprint_one.original_publication_date = '2013-12-25 10:09:08.070605+00:00' + preprint_one.save() + return preprint_one @pytest.fixture() def preprint_two(self, user, project_two, provider_two, subject_two): preprint_two = PreprintFactory(creator=user, project=project_two, filename='howto_reason.txt', provider=provider_two, subjects=[[subject_two._id]]) preprint_two.date_created = '2013-12-11 10:09:08.070605+00:00' preprint_two.date_published = '2013-12-11 10:09:08.070605+00:00' + preprint_two.original_publication_date = '2013-12-11 10:09:08.070605+00:00' preprint_two.save() return preprint_two @@ -70,6 +74,7 @@ def preprint_three(self, user, project_three, provider_three, subject_one, subje preprint_three = PreprintFactory(creator=user, project=project_three, filename='darn_reason.txt', provider=provider_three, subjects=[[subject_one._id], [subject_two._id]]) preprint_three.date_created = '2013-12-11 10:09:08.070605+00:00' preprint_three.date_published = '2013-12-11 10:09:08.070605+00:00' + preprint_three.original_publication_date = '2013-12-11 10:09:08.070605+00:00' preprint_three.is_published = False preprint_three.save() return preprint_three @@ -95,6 +100,10 @@ def date_modified_url(self, url): def date_published_url(self, url): return '{}filter[date_published]='.format(url) + @pytest.fixture() + def original_publication_date_url(self, url): + return '{}filter[original_publication_date]='.format(url) + @pytest.fixture() def is_published_url(self, url): return '{}filter[is_published]='.format(url) @@ -179,6 +188,24 @@ def test_date_published_filter_equals_returns_multiple(self, app, user, preprint actual = set([preprint['id'] for preprint in res.json['data']]) assert expected == actual + def test_original_publication_date_filter_equals_returns_none(self, app, user, original_publication_date_url): + expected = [] + res = app.get('{}{}'.format(original_publication_date_url, '2015-11-15 10:09:08.070605+00:00'), auth=user.auth) + actual = [preprint['id'] for preprint in res.json['data']] + assert expected == actual + + def test_original_publication_date_filter_equals_returns_one(self, app, user, preprint_one, original_publication_date_url): + expected = [preprint_one._id] + res = app.get('{}{}'.format(original_publication_date_url, preprint_one.original_publication_date), auth=user.auth) + actual = [preprint['id'] for preprint in res.json['data']] + assert expected == actual + + def test_original_publication_date_filter_equals_returns_multiple(self, app, user, preprint_two, preprint_three, original_publication_date_url): + expected = set([preprint_two._id, preprint_three._id]) + res = app.get('{}{}'.format(original_publication_date_url, preprint_two.original_publication_date), auth=user.auth) + actual = set([preprint['id'] for preprint in res.json['data']]) + assert expected == actual + def test_is_published_false_filter_equals_returns_one(self, app, user, preprint_three, is_published_url): expected = [preprint_three._id] res = app.get('{}{}'.format(is_published_url, 'false'), auth=user.auth) From cc78c814760b9b0c42bc3df85132ea75ab497abe Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Tue, 24 Oct 2017 17:57:11 -0400 Subject: [PATCH 068/108] Apply requested changes --- reviews/models/mixins.py | 49 ++++++++++++++++++--------------- tests/test_notifications.py | 9 +++--- website/notifications/emails.py | 36 ++++++++++++++++-------- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index baa0d4e5d25..de0e2639968 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -18,8 +18,6 @@ from website import settings -from osf.models import OSFUser - from website.mails import mails from website.notifications.emails import get_user_subscriptions from website.notifications import utils @@ -210,31 +208,40 @@ def notify_submit(self, ev): auth=auth, save=False, ) - reviews_signals.reviews_email_submit.send(context=context) + recipients = [contributor for contributor in self.reviewable.node.contributors] + reviews_signals.reviews_email_submit.send(context=context, recipients=recipients) def notify_resubmit(self, ev): context = self.get_context() - context['template'] = 'reviews_resubmission_confirmation' - reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) + time_now = self.action.date_created if self.action is not None else timezone.now() + recipients = [contributor for contributor in self.reviewable.node.contributors] + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, + node=self.reviewable.node, template='reviews_resubmission_confirmation', + recipients=recipients, time_now=time_now) def notify_accept_reject(self, ev): context = self.get_context() - context['template'] = 'reviews_submission_status' + time_now = self.action.date_created if self.action is not None else timezone.now() context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value context['was_pending'] = self.action.from_state == workflow.States.PENDING.value - reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) + recipients = [contributor for contributor in self.reviewable.node.contributors] + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, + node=self.reviewable.node, template='reviews_submission_status', + recipients=recipients, time_now=time_now) def notify_edit_comment(self, ev): context = self.get_context() - context['template'] = 'reviews_update_comment' + time_now = self.action.date_created if self.action is not None else timezone.now() if not self.reviewable.provider.reviews_comments_private and self.action.comment: - reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node) + recipients = [contributor for contributor in self.reviewable.node.contributors] + reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, + node=self.reviewable.node, template='reviews_update_comment', + recipients=recipients, time_now=time_now) def get_context(self): return { 'domain': settings.DOMAIN, - 'email_recipients': [contributor._id for contributor in self.reviewable.node.contributors], 'reviewable': self.reviewable, 'workflow': self.reviewable.provider.reviews_workflow, 'provider_url': self.reviewable.provider.domain or '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), @@ -244,32 +251,30 @@ def get_context(self): # Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect -def reviews_notification(self, creator, context, node): - time_now = timezone.now() - email_recipients = context['email_recipients'] +def reviews_notification(self, creator, recipients, time_now, template, context, node): emails.notify_global_event( event='global_reviews', sender_user=creator, node=node, timestamp=time_now, - target_users=email_recipients, - **context + recipients=recipients, + template=template, + context=context ) # Handle email notifications for a new submission. @reviews_signals.reviews_email_submit.connect -def reviews_submit_notification(self, context): +def reviews_submit_notification(self, recipients, context): event_type = utils.find_subscription_type('global_reviews') - for user_id in context['email_recipients']: - user = OSFUser.load(user_id) - user_subscriptions = get_user_subscriptions(user, event_type) + for recipient in recipients: + user_subscriptions = get_user_subscriptions(recipient, event_type) context['no_future_emails'] = user_subscriptions['none'] - context['is_creator'] = user == context['reviewable'].node.creator + context['is_creator'] = recipient == context['reviewable'].node.creator context['provider_name'] = context['reviewable'].provider.name mails.send_mail( - user.username, + recipient.username, mails.REVIEWS_SUBMISSION_CONFIRMATION, mimetype='html', - user=user, + user=recipient, **context ) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 2420553c084..327bbce2acb 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1810,11 +1810,8 @@ def setUp(self): self.user = factories.UserFactory() self.sender = factories.UserFactory() self.context_info = { - 'email_recipients': [self.sender._id, self.user._id], 'email_sender': self.sender, - 'template': 'test', 'domain': 'osf.io', - 'referrer': self.sender, 'reviewable': self.preprint, 'workflow': 'pre-moderation', 'provider_contact_email': 'contact@osf.io', @@ -1845,10 +1842,12 @@ def test_reviews_base_notification(self): @mock.patch('website.mails.mails.send_mail') def test_reviews_submit_notification(self, mock_send_email): - mixins.reviews_submit_notification(self, context=self.context_info) + mixins.reviews_submit_notification(self, context=self.context_info, recipients=[self.sender, self.user]) assert_true(mock_send_email.called) @mock.patch('website.notifications.emails.notify_global_event') def test_reviews_notification(self, mock_notify): - mixins.reviews_notification(self, creator=self.sender, context=self.context_info, node=self.preprint.node) + mixins.reviews_notification(self, creator=self.sender, context=self.context_info, + node=self.preprint.node, recipients=[self.sender, self.user], + time_now=timezone.now(), template='test.html.mako') assert_true(mock_notify.called) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index aee94564c27..fef80a9fd26 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -52,26 +52,39 @@ def notify(event, user, node, timestamp, **context): return sent_users def notify_mentions(event, user, node, timestamp, **context): - new_mentions = context.get('new_mentions', []) - sent_users = notify_global_event(event, user, node, timestamp, new_mentions, **context) + recipient_ids = context.get('new_mentions', []) + sent_users = notify_global_event(event, user, node, timestamp, recipient_ids, context=context) return sent_users -def notify_global_event(event, sender_user, node, timestamp, target_users, **context): +def notify_global_event(event, sender_user, node, timestamp, recipients, template=None, context=None): event_type = utils.find_subscription_type(event) sent_users = [] - for target_id in target_users: - target_user = OSFUser.load(target_id) - subscriptions = get_user_subscriptions(target_user, event_type) + # Initialize the subscriptions dict + users_subscriptions = {} + for key in constants.NOTIFICATION_TYPES: + users_subscriptions[key] = [] + + # Group recipients IDs per each notification type + # e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'none': [], 'email_digest': []} + for recipient in recipients: + subscriptions = get_user_subscriptions(recipient, event_type) for notification_type in subscriptions: - if (notification_type != 'none' and subscriptions[notification_type] and target_id in subscriptions[notification_type]): - store_emails([target_id], notification_type, event, sender_user, node, timestamp, **context) - sent_users.extend([target_id]) + if (notification_type != 'none' and subscriptions[notification_type] and recipient._id in subscriptions[notification_type]): + users_subscriptions[notification_type].append(recipient._id) + if (sender_user._id != recipient): + sent_users.append(recipient._id) + + # For each notification type store the list of users + for type in users_subscriptions: + # Check if list is empty + if users_subscriptions[type]: + store_emails(users_subscriptions[type], type, event, sender_user, node, timestamp, template, **context) return sent_users -def store_emails(recipient_ids, notification_type, event, user, node, timestamp, **context): +def store_emails(recipient_ids, notification_type, event, user, node, timestamp, template=None, **context): """Store notification emails Emails are sent via celery beat as digests @@ -89,7 +102,8 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, return # Reviews has multiple email templates for the global_reviews event. - template = '{template}.html.mako'.format(template=context['template']) if event == 'global_reviews' else '{event}.html.mako'.format(event=event) + template = '{template}.html.mako'.format(template=template) if template else '{event}.html.mako'.format(event=event) + # user whose action triggered email sending context['user'] = user node_lineage_ids = get_node_lineage(node) if node else [] From 8de4147e2c9bfe26ec2986ab1a1aad4479fa2316 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Tue, 24 Oct 2017 18:14:36 -0400 Subject: [PATCH 069/108] Standardize docker-compose commands --- docker-compose.yml | 55 ++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 72441082f0d..3fbda07eaa1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,12 +57,12 @@ services: postgres: image: postgres:9.6 - command: |- - /bin/bash -c " - echo \"$$POSTGRES_INITDB\" > /docker-entrypoint-initdb.d/commands.sh && + command: + - /bin/bash + - -c + - echo "$$POSTGRES_INITDB" > /docker-entrypoint-initdb.d/commands.sh && chmod +x /docker-entrypoint-initdb.d/commands.sh && /docker-entrypoint.sh postgres - " ports: - 5432:5432 environment: @@ -111,13 +111,13 @@ services: mfr_requirements: image: quay.io/centerforopenscience/mfr:develop - command: |- - /bin/bash -c " - invoke install --develop && + command: + - /bin/bash + - -c + - invoke install --develop && (python -m compileall /usr/local/lib/python3.5 || true) && rm -Rf /python3.5/* && cp -Rf -p /usr/local/lib/python3.5 / - " restart: 'no' volumes: - mfr_requirements_vol:/python3.5 @@ -142,13 +142,13 @@ services: wb_requirements: image: quay.io/centerforopenscience/waterbutler:develop - command: |- - /bin/bash -c " - invoke install --develop && + command: + - /bin/bash + - -c + - invoke install --develop && (python -m compileall /usr/local/lib/python3.5 || true) && rm -Rf /python3.5/* && cp -Rf -p /usr/local/lib/python3.5 / - " restart: 'no' volumes: - wb_requirements_vol:/python3.5 @@ -200,14 +200,18 @@ services: - postgres stdin_open: true - ############# # Preprints # ############# preprints: image: quay.io/centerforopenscience/ember-preprints:develop-development-local - command: /bin/bash -c "yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4200 --live-reload-port 41953" + command: + - /bin/bash + - -c + - yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && + ./node_modules/.bin/bower install --allow-root --config.interactive=false && + ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4200 --live-reload-port 41953 restart: unless-stopped depends_on: - api @@ -230,7 +234,12 @@ services: registries: image: quay.io/centerforopenscience/ember-osf-registries:develop-development-local - command: /bin/bash -c "yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4300" + command: + - /bin/bash + - -c + - yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && + ./node_modules/.bin/bower install --allow-root --config.interactive=false && + ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4300 restart: unless-stopped depends_on: - api @@ -250,8 +259,12 @@ services: ############## reviews: - image: quay.io/centerforopenscience/ember-osf-reviews:develop-development-local - command: /bin/bash -c "yarn --pure-lockfile --no-progress --no-emoji && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4400" + image: quay.io/centerforopenscience/osf-reviews:develop + command: + - /bin/bash + - -c + - yarn --pure-lockfile --no-progress --no-emoji && + ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4400 restart: unless-stopped depends_on: - api @@ -274,13 +287,13 @@ services: requirements: image: quay.io/centerforopenscience/osf:develop - command: |- - /bin/bash -c " - invoke requirements --quick && + command: + - /bin/bash + - -c + - invoke requirements --quick && (python -m compileall /usr/local/lib/python2.7 || true) && rm -Rf /python2.7/* && cp -Rf -p /usr/local/lib/python2.7 / - " restart: 'no' environment: DJANGO_SETTINGS_MODULE: api.base.settings From 6fb54130e86b5d108e646714d07f9aee997ec7e3 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Tue, 24 Oct 2017 18:30:12 -0400 Subject: [PATCH 070/108] Die, toku, die --- docker-compose.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3fbda07eaa1..d54a81b5108 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ version: '3' volumes: postgres_data_vol: external: false - tokumx_data_vol: + mongo_data_vol: external: false elasticsearch_data_vol: external: false @@ -74,15 +74,13 @@ services: - "${POSTGRES_DATA_VOL:-postgres_data_vol}:/var/lib/postgresql/data/" stdin_open: true - tokumx: - image: quay.io/centerforopenscience/tokumx:latest + mongo: + image: mongo:3.4 command: mongod --ipv6 ports: - 27017:27017 - environment: - TOKU_HUGE_PAGES_OK: 1 volumes: - - tokumx_data_vol:/data/db + - mongo_data_vol:/data/db stdin_open: true rabbitmq: @@ -332,7 +330,7 @@ services: ports: - 7007:7007 depends_on: - - tokumx + - mongo env_file: - .docker-compose.sharejs.env volumes: From 1b5288fc55f7550716611b0e15dca608bee0cc48 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Wed, 25 Oct 2017 00:25:05 -0400 Subject: [PATCH 071/108] Add Mongo SSL support for the wiki --- .docker-compose.sharejs.env | 2 +- addons/wiki/settings/defaults.py | 4 ++-- addons/wiki/utils.py | 3 ++- docker-compose.yml | 13 ++++++++++++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.docker-compose.sharejs.env b/.docker-compose.sharejs.env index cbfa27f62d2..5d16845a8fb 100644 --- a/.docker-compose.sharejs.env +++ b/.docker-compose.sharejs.env @@ -1,7 +1,7 @@ SHAREJS_SERVER_HOST=0.0.0.0 SHAREJS_SERVER_PORT=7007 SHAREJS_DB_NAME=sharejs -SHAREJS_DB_URL=mongodb://192.168.168.167:27017/sharejs +SHAREJS_DB_URL=mongodb://192.168.168.167:27017/sharejs?ssl=true #SHAREJS_CORS_ALLOW_ORIGIN= #SHAREJS_SENTRY_DSN= diff --git a/addons/wiki/settings/defaults.py b/addons/wiki/settings/defaults.py index 325e5a1f1c9..5b3f5b33235 100644 --- a/addons/wiki/settings/defaults.py +++ b/addons/wiki/settings/defaults.py @@ -1,5 +1,5 @@ import datetime - +import os import pytz from website import settings @@ -9,7 +9,7 @@ SHAREJS_URL = '{}:{}'.format(SHAREJS_HOST, SHAREJS_PORT) SHAREJS_DB_NAME = 'sharejs' -SHAREJS_DB_URL = 'mongodb://{}:{}/{}'.format(settings.DB_HOST, settings.DB_PORT, SHAREJS_DB_NAME) +SHAREJS_DB_URL = os.environ.get('SHAREJS_DB_URL', 'mongodb://{}:{}/{}'.format(settings.DB_HOST, settings.DB_PORT, SHAREJS_DB_NAME)) # TODO: Change to release date for wiki change WIKI_CHANGE_DATE = datetime.datetime.utcfromtimestamp(1423760098).replace(tzinfo=pytz.utc) diff --git a/addons/wiki/utils.py b/addons/wiki/utils.py index ce96b35acf9..b387fa4ee73 100644 --- a/addons/wiki/utils.py +++ b/addons/wiki/utils.py @@ -3,6 +3,7 @@ import urllib import uuid +import ssl from pymongo import MongoClient import requests @@ -101,7 +102,7 @@ def migrate_uuid(node, wname): def share_db(): """Generate db client for sharejs db""" - client = MongoClient(wiki_settings.SHAREJS_DB_URL) + client = MongoClient(wiki_settings.SHAREJS_DB_URL, ssl_cert_reqs=ssl.CERT_NONE) return client[wiki_settings.SHAREJS_DB_NAME] diff --git a/docker-compose.yml b/docker-compose.yml index d54a81b5108..cfb04500d6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,7 +76,16 @@ services: mongo: image: mongo:3.4 - command: mongod --ipv6 + command: + - /bin/bash + - -c + - openssl req -newkey rsa:2048 -new -x509 -days 9999 -nodes + -subj "/C=US/ST=Virginia/L=Charlottesville/O=COS/OU=Test/CN=192.168.168.167" + -out /etc/ssl/mongodb.crt -keyout /etc/ssl/mongodb.key && + cat /etc/ssl/mongodb.key /etc/ssl/mongodb.crt > /etc/ssl/mongodb.pem && + chown -R mongodb:mongodb /etc/ssl /data/db && + chmod -R 0600 /etc/ssl/* && + gosu mongodb mongod --sslMode requireSSL --sslDisabledProtocols=TLS1_0,TLS1_1 --sslPEMKeyFile /etc/ssl/mongodb.pem ports: - 27017:27017 volumes: @@ -331,6 +340,7 @@ services: - 7007:7007 depends_on: - mongo + - web env_file: - .docker-compose.sharejs.env volumes: @@ -429,6 +439,7 @@ services: DJANGO_SETTINGS_MODULE: api.base.settings env_file: - .docker-compose.env + - .docker-compose.sharejs.env volumes: - osf_requirements_vol:/usr/local/lib/python2.7 - osf_bower_components_vol:/code/website/static/vendor/bower_components From 9993ccf7fa4906a61c0294049818426efef9122c Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Wed, 25 Oct 2017 10:48:49 -0400 Subject: [PATCH 072/108] Update test and notify_mention --- tests/test_notifications.py | 2 +- website/notifications/emails.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 327bbce2acb..29deb701f2d 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1477,7 +1477,7 @@ def test_notify_mentions_does_send_to_mentioned_users(self, mock_store): emails.notify_mentions('global_mentions', user=user, node=node, timestamp=time_now, new_mentions=[user._id]) assert_true(mock_store.called) mock_store.assert_called_with([node.creator._id], 'email_transactional', 'global_mentions', user, - node, time_now, new_mentions=[node.creator._id]) + node, time_now, None, new_mentions=[node.creator._id]) @mock.patch('website.notifications.emails.store_emails') def test_notify_sends_comment_reply_event_if_comment_is_direct_reply(self, mock_store): diff --git a/website/notifications/emails.py b/website/notifications/emails.py index fef80a9fd26..0de8f30c5c7 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -53,7 +53,8 @@ def notify(event, user, node, timestamp, **context): def notify_mentions(event, user, node, timestamp, **context): recipient_ids = context.get('new_mentions', []) - sent_users = notify_global_event(event, user, node, timestamp, recipient_ids, context=context) + recipients = OSFUser.objects.filter(guids___id__in=recipient_ids) + sent_users = notify_global_event(event, user, node, timestamp, recipients, context=context) return sent_users def notify_global_event(event, sender_user, node, timestamp, recipients, template=None, context=None): From 9227ab537e360014a11e534d54bc8ce3b7aaff42 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Thu, 26 Oct 2017 15:20:54 -0400 Subject: [PATCH 073/108] Apply requested changes --- reviews/models/mixins.py | 8 ++++---- website/notifications/emails.py | 14 ++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index de0e2639968..618884f3353 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -208,13 +208,13 @@ def notify_submit(self, ev): auth=auth, save=False, ) - recipients = [contributor for contributor in self.reviewable.node.contributors] + recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email_submit.send(context=context, recipients=recipients) def notify_resubmit(self, ev): context = self.get_context() time_now = self.action.date_created if self.action is not None else timezone.now() - recipients = [contributor for contributor in self.reviewable.node.contributors] + recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node, template='reviews_resubmission_confirmation', recipients=recipients, time_now=time_now) @@ -225,7 +225,7 @@ def notify_accept_reject(self, ev): context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value context['was_pending'] = self.action.from_state == workflow.States.PENDING.value - recipients = [contributor for contributor in self.reviewable.node.contributors] + recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node, template='reviews_submission_status', recipients=recipients, time_now=time_now) @@ -234,7 +234,7 @@ def notify_edit_comment(self, ev): context = self.get_context() time_now = self.action.date_created if self.action is not None else timezone.now() if not self.reviewable.provider.reviews_comments_private and self.action.comment: - recipients = [contributor for contributor in self.reviewable.node.contributors] + recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, node=self.reviewable.node, template='reviews_update_comment', recipients=recipients, time_now=time_now) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index 0de8f30c5c7..43eedfc820c 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -62,9 +62,7 @@ def notify_global_event(event, sender_user, node, timestamp, recipients, templat sent_users = [] # Initialize the subscriptions dict - users_subscriptions = {} - for key in constants.NOTIFICATION_TYPES: - users_subscriptions[key] = [] + users_subscriptions = {key: [] for key in constants.NOTIFICATION_TYPES} # Group recipients IDs per each notification type # e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'none': [], 'email_digest': []} @@ -77,10 +75,10 @@ def notify_global_event(event, sender_user, node, timestamp, recipients, templat sent_users.append(recipient._id) # For each notification type store the list of users - for type in users_subscriptions: + for notify_type in users_subscriptions: # Check if list is empty - if users_subscriptions[type]: - store_emails(users_subscriptions[type], type, event, sender_user, node, timestamp, template, **context) + if users_subscriptions[notify_type]: + store_emails(users_subscriptions[notify_type], notify_type, event, sender_user, node, timestamp, template, **context) return sent_users @@ -102,8 +100,8 @@ def store_emails(recipient_ids, notification_type, event, user, node, timestamp, if notification_type == 'none': return - # Reviews has multiple email templates for the global_reviews event. - template = '{template}.html.mako'.format(template=template) if template else '{event}.html.mako'.format(event=event) + # If `template` is not specified, default to using a template with name `event` + template = '{template}.html.mako'.format(template=template or event) # user whose action triggered email sending context['user'] = user From 1086c75603503a198a0f8a27034308431f299752 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Wed, 25 Oct 2017 10:37:56 -0400 Subject: [PATCH 074/108] Move checking original_publication_date to its rightful home, preprint_csl --- api/citations/utils.py | 4 ++++ api_tests/preprints/views/test_preprint_citations.py | 2 +- osf/models/node.py | 7 +------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/citations/utils.py b/api/citations/utils.py index bbb67c2d37a..86b224674e9 100644 --- a/api/citations/utils.py +++ b/api/citations/utils.py @@ -7,6 +7,7 @@ from citeproc.source.json import CiteProcJSON from osf.models import PreprintService +from website.citations.utils import datetime_to_csl from website.settings import CITATION_STYLES_PATH, BASE_PATH, CUSTOM_CITATIONS @@ -28,6 +29,9 @@ def preprint_csl(preprint, node): csl['publisher'] = preprint.provider.name csl['URL'] = display_absolute_url(preprint) + if preprint.original_publication_date: + csl['issued'] = datetime_to_csl(preprint.original_publication_date) + if csl.get('DOI'): csl.pop('DOI') diff --git a/api_tests/preprints/views/test_preprint_citations.py b/api_tests/preprints/views/test_preprint_citations.py index 9516ea73091..4ee682aabf7 100644 --- a/api_tests/preprints/views/test_preprint_citations.py +++ b/api_tests/preprints/views/test_preprint_citations.py @@ -60,7 +60,7 @@ def setUp(self): def test_citation_contains_correct_date(self): res = self.app.get(self.published_preprint_url) assert_equal(res.status_code, 200) - expected_date = self.published_preprint.date_modified.strftime('%Y, %B %-d') + expected_date = self.published_preprint.node.logs.latest().date.strftime('%Y, %B %-d') assert_true(expected_date in res.json['data']['attributes']['citation']) self.published_preprint.original_publication_date = datetime(2017, 12, 24) diff --git a/osf/models/node.py b/osf/models/node.py index 8f09bdbcec5..c06d0b16a52 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -656,12 +656,7 @@ def csl(self): # formats node information into CSL format for citation parsing csl['DOI'] = doi if self.logs.exists(): - citation_date = self.logs.latest().date - - if self.preprints.exists() and self.preprints.first().original_publication_date: - citation_date = self.preprints.get_queryset()[0].original_publication_date - - csl['issued'] = datetime_to_csl(citation_date) + csl['issued'] = datetime_to_csl(self.logs.latest().date) return csl From 9d7fc38af604d60fdff126651fa860322057180c Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Thu, 26 Oct 2017 21:05:46 -0400 Subject: [PATCH 075/108] Update notify_global_event --- website/notifications/emails.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index 43eedfc820c..bf718e15bda 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -60,25 +60,24 @@ def notify_mentions(event, user, node, timestamp, **context): def notify_global_event(event, sender_user, node, timestamp, recipients, template=None, context=None): event_type = utils.find_subscription_type(event) sent_users = [] + users_subscriptions = [] - # Initialize the subscriptions dict - users_subscriptions = {key: [] for key in constants.NOTIFICATION_TYPES} - - # Group recipients IDs per each notification type - # e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'none': [], 'email_digest': []} + # Create a list of users' subscription dictionaries for recipient in recipients: - subscriptions = get_user_subscriptions(recipient, event_type) - for notification_type in subscriptions: - if (notification_type != 'none' and subscriptions[notification_type] and recipient._id in subscriptions[notification_type]): - users_subscriptions[notification_type].append(recipient._id) - if (sender_user._id != recipient): - sent_users.append(recipient._id) + users_subscriptions.append(get_user_subscriptions(recipient, event_type)) + + # Merge the dictionaries using dict comprehension + # Output e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'none': [], 'email_digest': [u'39dbp']} + merged_user_subscriptions = {k: [d[k][0] for d in users_subscriptions if d[k]] for k in users_subscriptions[0]} # For each notification type store the list of users - for notify_type in users_subscriptions: - # Check if list is empty - if users_subscriptions[notify_type]: - store_emails(users_subscriptions[notify_type], notify_type, event, sender_user, node, timestamp, template, **context) + for notify_type in merged_user_subscriptions: + # Group of users assigned this notification type + user_group_type = merged_user_subscriptions[notify_type] + # Check if user group is empty + if user_group_type: + sent_users += user_group_type + store_emails(merged_user_subscriptions[notify_type], notify_type, event, sender_user, node, timestamp, template, **context) return sent_users From 2b4fcc7ca410f144880ae49cd9ba3363041f3a77 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Thu, 26 Oct 2017 22:52:32 -0400 Subject: [PATCH 076/108] Update notify_global_event --- website/notifications/emails.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index bf718e15bda..4d91abf2a43 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -66,9 +66,9 @@ def notify_global_event(event, sender_user, node, timestamp, recipients, templat for recipient in recipients: users_subscriptions.append(get_user_subscriptions(recipient, event_type)) - # Merge the dictionaries using dict comprehension - # Output e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'none': [], 'email_digest': [u'39dbp']} - merged_user_subscriptions = {k: [d[k][0] for d in users_subscriptions if d[k]] for k in users_subscriptions[0]} + # Merge the dictionaries using dict comprehension and exclude 'none' notification type. + # Output e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'email_digest': [u'39dbp']} + merged_user_subscriptions = {k: [d[k][0] for d in users_subscriptions if d[k]] for k in users_subscriptions[0] if k != 'none'} # For each notification type store the list of users for notify_type in merged_user_subscriptions: From 7ac0f41d15feccb5cb4efe2ef898a8bf3646d781 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Thu, 14 Sep 2017 16:01:31 -0400 Subject: [PATCH 077/108] Add preprint confirmation emails --- osf/models/preprint_service.py | 5 ++++ tests/test_preprints.py | 27 ++++++++++++++++- tests/test_views.py | 1 + website/mails/mails.py | 8 +++++ website/project/views/contributor.py | 19 ++++++++---- .../preprint_confirmation_branded.txt.mako | 30 +++++++++++++++++++ .../preprint_confirmation_default.txt.mako | 27 +++++++++++++++++ 7 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 website/templates/emails/preprint_confirmation_branded.txt.mako create mode 100644 website/templates/emails/preprint_confirmation_default.txt.mako diff --git a/osf/models/preprint_service.py b/osf/models/preprint_service.py index bc54e2e43f4..3d63ce464ab 100644 --- a/osf/models/preprint_service.py +++ b/osf/models/preprint_service.py @@ -14,6 +14,7 @@ from osf.utils.fields import NonNaiveDateTimeField from website.preprints.tasks import on_preprint_updated, get_and_set_preprint_identifiers from website.project.licenses import set_license +from website.project import signals as project_signals from website.util import api_v2_url from website.util.permissions import ADMIN from website import settings @@ -210,6 +211,10 @@ def set_published(self, published, auth, save=False): self.node.save() self.save() + # Send creator confirmation email + project_signals.contributor_added.send(self.node, contributor=auth.user, auth=auth, + email_template='preprint_confirmation') + def set_preprint_license(self, license_detail, auth, save=False): license_record, license_changed = set_license(self, license_detail, auth, node_type='preprint') diff --git a/tests/test_preprints.py b/tests/test_preprints.py index d8aba75a90f..fa1ff1c6210 100644 --- a/tests/test_preprints.py +++ b/tests/test_preprints.py @@ -21,9 +21,10 @@ from tests.utils import assert_logs from tests.base import OsfTestCase from website import settings +from website.project.signals import contributor_added from website.identifiers.utils import get_doi_and_metadata_for_object from website.preprints.tasks import format_preprint, update_preprint_share, on_preprint_updated -from website.project.views.contributor import find_preprint_provider +from website.project.views.contributor import find_preprint_provider, notify_added_contributor from website.util import permissions from website.util.share import format_user @@ -727,3 +728,27 @@ def test_no_call_async_update_on_400_failure(self, requests, mock_async, mock_ma update_preprint_share(self.preprint) assert not mock_async.called assert mock_mail.called + +class TestPreprintConfirmationEmails(OsfTestCase): + def setUp(self): + super(TestPreprintConfirmationEmails, self).setUp() + self.user = AuthUserFactory() + self.write_contrib = AuthUserFactory() + self.project = ProjectFactory(creator=self.user) + self.project.add_contributor(self.write_contrib, permissions=[permissions.WRITE]) + self.preprint = PreprintFactory(project=self.project, is_published=False) + contributor_added.connect(notify_added_contributor) + + @mock.patch('website.mails.send_mail') + def test_creator_gets_email(self, send_mail): + self.preprint.set_published(True, auth=Auth(self.user), save=True) + assert_true(send_mail.called) + + @mock.patch('website.mails.send_mail') + def test_creator_preprint_not_saved(self, send_mail): + self.preprint.set_published(True, auth=Auth(self.user), save=False) + assert_false(send_mail.called) + + def tearDown(self): + super(TestPreprintConfirmationEmails, self).tearDown() + contributor_added.disconnect(notify_added_contributor) diff --git a/tests/test_views.py b/tests/test_views.py index 17e6642551c..12fd30051fa 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1822,6 +1822,7 @@ def test_email_sent_when_reg_user_is_added(self, send_mail): referrer_name=self.auth.user.fullname, all_global_subscriptions_none=False, branded_service=None, + preprint=None ) assert_almost_equal(contributor.contributor_added_email_records[project._id]['last_sent'], int(time.time()), delta=1) diff --git a/website/mails/mails.py b/website/mails/mails.py index 488ca5585fc..6ff691cabc7 100644 --- a/website/mails/mails.py +++ b/website/mails/mails.py @@ -211,6 +211,14 @@ def get_english_article(word): 'contributor_added_preprint_node_from_osf', subject='You have been added as a contributor to an OSF project.' ) +PREPRINT_CONFIRMATION = lambda template, provider: Mail( + 'preprint_confirmation_{}'.format(template), + subject="You've shared {} {} on {}".format( + get_english_article(provider.preprint_word), + provider.preprint_word, + 'OSF Preprints' if provider._id == 'osf' else provider.name + ) +) FORWARD_INVITE = Mail('forward_invite', subject='Please forward to ${fullname}') FORWARD_INVITE_REGISTERED = Mail('forward_invite_registered', subject='Please forward to ${fullname}') diff --git a/website/project/views/contributor.py b/website/project/views/contributor.py index 269ae60e872..23b4a14e376 100644 --- a/website/project/views/contributor.py +++ b/website/project/views/contributor.py @@ -525,11 +525,14 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ or node.parent_node and not node.parent_node.is_contributor(contributor)): preprint_provider = None - if email_template == 'preprint': - email_template, preprint_provider = find_preprint_provider(node) - if not email_template or not preprint_provider: + if 'preprint' in email_template: + template, preprint_provider = find_preprint_provider(node) + if not template or not preprint_provider: return - email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT')(email_template, preprint_provider) + if email_template == 'preprint_confirmation': + email_template = getattr(mails, 'PREPRINT_CONFIRMATION')(template, preprint_provider) + else: + email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT')(template, preprint_provider) elif node.is_preprint: email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF'.format(email_template.upper())) else: @@ -551,7 +554,8 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ node=node, referrer_name=auth.user.fullname if auth else '', all_global_subscriptions_none=check_if_all_global_subscriptions_are_none(contributor), - branded_service=preprint_provider + branded_service=preprint_provider, + preprint=get_preprint(node) ) contributor.contributor_added_email_records[node._id]['last_sent'] = get_timestamp() @@ -560,6 +564,11 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ elif not contributor.is_registered: unreg_contributor_added.send(node, contributor=contributor, auth=auth, email_template=email_template) +def get_preprint(node): + try: + return PreprintService.objects.get(node=node) + except PreprintService.DoesNotExist: + return None def find_preprint_provider(node): """ diff --git a/website/templates/emails/preprint_confirmation_branded.txt.mako b/website/templates/emails/preprint_confirmation_branded.txt.mako new file mode 100644 index 00000000000..abcffde98d4 --- /dev/null +++ b/website/templates/emails/preprint_confirmation_branded.txt.mako @@ -0,0 +1,30 @@ +<%! + from website import settings +%> + +Hello ${user.fullname}, + +Congratulations on sharing your ${preprint.provider.preprint_word} "${node.title}" on ${preprint.provider.name}, powered by OSF Preprints: ${preprint.absolute_url} + +Now that you've shared your ${preprint.provider.preprint_word}, take advantage of more OSF features: + +*Upload supplemental, materials, data, and code to the OSF project associated with your ${preprint.provider.preprint_word}: ${node.absolute_url} +Learn how: http://help.osf.io/m/preprints/l/685323-add-supplemental-files-to-a-preprint + +*Preregister your next study and become eligible for a $1000 prize: osf.io/prereg + +*Track your impact with ${preprint.provider.preprint_word} downloads + + + +You will ${'not receive ' if all_global_subscriptions_none else 'be automatically subscribed to '}notification emails for this project. To change your email notification preferences, visit your project or your user settings: ${settings.DOMAIN + "settings/notifications/"} + +Sincerely, + +Your ${preprint.provider.name} and OSF teams + +Want more information? Visit https://osf.io/preprints/${preprint.provider.name.lower()} to learn about ${preprint.provider.name} or https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. + +Questions? Email support+${preprint.provider._id}@osf.io + + diff --git a/website/templates/emails/preprint_confirmation_default.txt.mako b/website/templates/emails/preprint_confirmation_default.txt.mako new file mode 100644 index 00000000000..d6075df1b26 --- /dev/null +++ b/website/templates/emails/preprint_confirmation_default.txt.mako @@ -0,0 +1,27 @@ +<%! + from website import settings +%> + +Hello ${user.fullname}, + +Congratulations on sharing your preprint "${node.title}" on OSF Preprints: ${preprint.absolute_url} + +Now that you've shared your preprint, take advantage of more OSF features: + +*Upload supplemental, materials, data, and code to the OSF project associated with your preprint: ${node.absolute_url} Learn how: http://help.osf.io/m/preprints/l/685323-add-supplemental-files-to-a-preprint + +*Preregister your next study and become eligible for a $1000 prize: osf.io/prereg + +*Track your impact with preprint downloads + + + +You will ${'not receive ' if all_global_subscriptions_none else 'be automatically subscribed to '} notification emails for this project. To change your email notification preferences, visit your project or your user settings: ${settings.DOMAIN + 'settings/notifications/'} + +Sincerely, + +Your OSF Team + +Want more information? Visit https://osf.io/ to learn about the Open Science Framework or https://cos.io/ for information about its supporting organization, the Center for Open Science. + +Questions? Email contact@osf.io From 544bcc97b583c629d7dd97ae8d98b6d6da50b1d7 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Fri, 22 Sep 2017 13:18:30 -0400 Subject: [PATCH 078/108] Add address info --- .../emails/preprint_confirmation_branded.txt.mako | 7 +++++++ .../emails/preprint_confirmation_default.txt.mako | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/website/templates/emails/preprint_confirmation_branded.txt.mako b/website/templates/emails/preprint_confirmation_branded.txt.mako index abcffde98d4..190b97d4f9d 100644 --- a/website/templates/emails/preprint_confirmation_branded.txt.mako +++ b/website/templates/emails/preprint_confirmation_branded.txt.mako @@ -28,3 +28,10 @@ Want more information? Visit https://osf.io/preprints/${preprint.provider.name.l Questions? Email support+${preprint.provider._id}@osf.io + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md + diff --git a/website/templates/emails/preprint_confirmation_default.txt.mako b/website/templates/emails/preprint_confirmation_default.txt.mako index d6075df1b26..357c8b2ad8e 100644 --- a/website/templates/emails/preprint_confirmation_default.txt.mako +++ b/website/templates/emails/preprint_confirmation_default.txt.mako @@ -25,3 +25,11 @@ Your OSF Team Want more information? Visit https://osf.io/ to learn about the Open Science Framework or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email contact@osf.io + + + +Center for Open Science + +210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083 + +Privacy Policy: https://github.com/CenterForOpenScience/cos.io/blob/master/PRIVACY_POLICY.md From 5e7bd0b9d69c353865236aa323d39a97e732c834 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Mon, 25 Sep 2017 15:25:30 -0400 Subject: [PATCH 079/108] Update preprint confirmation emails --- osf/models/preprint_provider.py | 5 ++++ osf/models/preprint_service.py | 24 +++++++++++---- tests/test_preprints.py | 29 ++++++++++--------- tests/test_views.py | 1 - website/mails/mails.py | 11 ++++--- website/project/views/contributor.py | 19 ++++-------- .../preprint_confirmation_branded.txt.mako | 4 +-- 7 files changed, 53 insertions(+), 40 deletions(-) diff --git a/osf/models/preprint_provider.py b/osf/models/preprint_provider.py index be2e125144a..d4cb02fbe81 100644 --- a/osf/models/preprint_provider.py +++ b/osf/models/preprint_provider.py @@ -13,6 +13,7 @@ from reviews import permissions as reviews_permissions from reviews.models import ReviewProviderMixin +from website import settings from website.util import api_v2_url @@ -94,6 +95,10 @@ def all_subjects(self): # TODO: Delet this when all PreprintProviders have a mapping return rules_to_subjects(self.subjects_acceptable) + @property + def landing_url(self): + return self.domain if self.domain else '{}preprints/{}'.format(settings.DOMAIN, self.name.lower()) + def get_absolute_url(self): return '{}preprint_providers/{}'.format(self.absolute_api_v2_url, self._id) diff --git a/osf/models/preprint_service.py b/osf/models/preprint_service.py index 3d63ce464ab..bbaff11a011 100644 --- a/osf/models/preprint_service.py +++ b/osf/models/preprint_service.py @@ -14,10 +14,9 @@ from osf.utils.fields import NonNaiveDateTimeField from website.preprints.tasks import on_preprint_updated, get_and_set_preprint_identifiers from website.project.licenses import set_license -from website.project import signals as project_signals from website.util import api_v2_url from website.util.permissions import ADMIN -from website import settings +from website import settings, mails from reviews.models.mixins import ReviewableMixin from reviews.workflow import States @@ -207,14 +206,12 @@ def set_published(self, published, auth, save=False): # This should be called after all fields for EZID metadta have been set enqueue_postcommit_task(get_and_set_preprint_identifiers, (), {'preprint': self}, celery=True) + self._send_preprint_confirmation(auth) + if save: self.node.save() self.save() - # Send creator confirmation email - project_signals.contributor_added.send(self.node, contributor=auth.user, auth=auth, - email_template='preprint_confirmation') - def set_preprint_license(self, license_detail, auth, save=False): license_record, license_changed = set_license(self, license_detail, auth, node_type='preprint') @@ -248,3 +245,18 @@ def save(self, *args, **kwargs): if (not first_save and 'is_published' in saved_fields) or self.is_published: enqueue_postcommit_task(on_preprint_updated, (self._id,), {'old_subjects': old_subjects}, celery=True) return ret + + def _send_preprint_confirmation(self, auth): + # Send creator confirmation email + if self.provider._id == 'osf': + email_template = getattr(mails, 'PREPRINT_CONFIRMATION_DEFAULT') + else: + email_template = getattr(mails, 'PREPRINT_CONFIRMATION_BRANDED')(self.provider) + + mails.send_mail( + auth.user.username, + email_template, + user=auth.user, + node=self.node, + preprint=self + ) diff --git a/tests/test_preprints.py b/tests/test_preprints.py index fa1ff1c6210..9dc4fce2269 100644 --- a/tests/test_preprints.py +++ b/tests/test_preprints.py @@ -20,11 +20,10 @@ from osf_tests.utils import MockShareResponse from tests.utils import assert_logs from tests.base import OsfTestCase -from website import settings -from website.project.signals import contributor_added +from website import settings, mails from website.identifiers.utils import get_doi_and_metadata_for_object from website.preprints.tasks import format_preprint, update_preprint_share, on_preprint_updated -from website.project.views.contributor import find_preprint_provider, notify_added_contributor +from website.project.views.contributor import find_preprint_provider from website.util import permissions from website.util.share import format_user @@ -736,19 +735,23 @@ def setUp(self): self.write_contrib = AuthUserFactory() self.project = ProjectFactory(creator=self.user) self.project.add_contributor(self.write_contrib, permissions=[permissions.WRITE]) - self.preprint = PreprintFactory(project=self.project, is_published=False) - contributor_added.connect(notify_added_contributor) + self.preprint = PreprintFactory(project=self.project, provider=PreprintProviderFactory(_id='osf'), is_published=False) + self.preprint_branded = PreprintFactory(creator=self.user, is_published=False) @mock.patch('website.mails.send_mail') def test_creator_gets_email(self, send_mail): self.preprint.set_published(True, auth=Auth(self.user), save=True) - assert_true(send_mail.called) - @mock.patch('website.mails.send_mail') - def test_creator_preprint_not_saved(self, send_mail): - self.preprint.set_published(True, auth=Auth(self.user), save=False) - assert_false(send_mail.called) + send_mail.assert_called_with( + self.user.email, + mails.PREPRINT_CONFIRMATION_DEFAULT, + user=self.user, + node=self.preprint.node, + preprint=self.preprint + ) + + assert_equals(send_mail.call_count, 1) + + self.preprint_branded.set_published(True, auth=Auth(self.user), save=True) + assert_equals(send_mail.call_count, 2) - def tearDown(self): - super(TestPreprintConfirmationEmails, self).tearDown() - contributor_added.disconnect(notify_added_contributor) diff --git a/tests/test_views.py b/tests/test_views.py index 12fd30051fa..17e6642551c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1822,7 +1822,6 @@ def test_email_sent_when_reg_user_is_added(self, send_mail): referrer_name=self.auth.user.fullname, all_global_subscriptions_none=False, branded_service=None, - preprint=None ) assert_almost_equal(contributor.contributor_added_email_records[project._id]['last_sent'], int(time.time()), delta=1) diff --git a/website/mails/mails.py b/website/mails/mails.py index 6ff691cabc7..ab3d113db37 100644 --- a/website/mails/mails.py +++ b/website/mails/mails.py @@ -211,12 +211,15 @@ def get_english_article(word): 'contributor_added_preprint_node_from_osf', subject='You have been added as a contributor to an OSF project.' ) -PREPRINT_CONFIRMATION = lambda template, provider: Mail( - 'preprint_confirmation_{}'.format(template), +PREPRINT_CONFIRMATION_DEFAULT = Mail( + 'preprint_confirmation_default', + subject="You've shared a preprint on OSF preprints" +) +PREPRINT_CONFIRMATION_BRANDED = lambda provider: Mail( + 'preprint_confirmation_branded', subject="You've shared {} {} on {}".format( get_english_article(provider.preprint_word), - provider.preprint_word, - 'OSF Preprints' if provider._id == 'osf' else provider.name + provider.preprint_word, provider.name ) ) FORWARD_INVITE = Mail('forward_invite', subject='Please forward to ${fullname}') diff --git a/website/project/views/contributor.py b/website/project/views/contributor.py index 23b4a14e376..269ae60e872 100644 --- a/website/project/views/contributor.py +++ b/website/project/views/contributor.py @@ -525,14 +525,11 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ or node.parent_node and not node.parent_node.is_contributor(contributor)): preprint_provider = None - if 'preprint' in email_template: - template, preprint_provider = find_preprint_provider(node) - if not template or not preprint_provider: + if email_template == 'preprint': + email_template, preprint_provider = find_preprint_provider(node) + if not email_template or not preprint_provider: return - if email_template == 'preprint_confirmation': - email_template = getattr(mails, 'PREPRINT_CONFIRMATION')(template, preprint_provider) - else: - email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT')(template, preprint_provider) + email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT')(email_template, preprint_provider) elif node.is_preprint: email_template = getattr(mails, 'CONTRIBUTOR_ADDED_PREPRINT_NODE_FROM_OSF'.format(email_template.upper())) else: @@ -554,8 +551,7 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ node=node, referrer_name=auth.user.fullname if auth else '', all_global_subscriptions_none=check_if_all_global_subscriptions_are_none(contributor), - branded_service=preprint_provider, - preprint=get_preprint(node) + branded_service=preprint_provider ) contributor.contributor_added_email_records[node._id]['last_sent'] = get_timestamp() @@ -564,11 +560,6 @@ def notify_added_contributor(node, contributor, auth=None, throttle=None, email_ elif not contributor.is_registered: unreg_contributor_added.send(node, contributor=contributor, auth=auth, email_template=email_template) -def get_preprint(node): - try: - return PreprintService.objects.get(node=node) - except PreprintService.DoesNotExist: - return None def find_preprint_provider(node): """ diff --git a/website/templates/emails/preprint_confirmation_branded.txt.mako b/website/templates/emails/preprint_confirmation_branded.txt.mako index 190b97d4f9d..30aef2d55da 100644 --- a/website/templates/emails/preprint_confirmation_branded.txt.mako +++ b/website/templates/emails/preprint_confirmation_branded.txt.mako @@ -4,7 +4,7 @@ Hello ${user.fullname}, -Congratulations on sharing your ${preprint.provider.preprint_word} "${node.title}" on ${preprint.provider.name}, powered by OSF Preprints: ${preprint.absolute_url} +Congratulations on sharing your ${preprint.provider.preprint_word} "${node.title}" on ${preprint.provider.name}, powered by OSF Preprints: ${preprint.absolute_url} Now that you've shared your ${preprint.provider.preprint_word}, take advantage of more OSF features: @@ -23,7 +23,7 @@ Sincerely, Your ${preprint.provider.name} and OSF teams -Want more information? Visit https://osf.io/preprints/${preprint.provider.name.lower()} to learn about ${preprint.provider.name} or https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. +Want more information? Visit ${preprint.provider.landing_url} to learn about ${preprint.provider.name} or https://osf.io/ to learn about the Open Science Framework, or https://cos.io/ for information about its supporting organization, the Center for Open Science. Questions? Email support+${preprint.provider._id}@osf.io From 9274722ffb897979cb9cdb0f413ae25809573962 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 27 Oct 2017 15:18:59 -0400 Subject: [PATCH 080/108] update notify_global_event --- website/notifications/emails.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/website/notifications/emails.py b/website/notifications/emails.py index 4d91abf2a43..f67ab0d0020 100644 --- a/website/notifications/emails.py +++ b/website/notifications/emails.py @@ -60,24 +60,13 @@ def notify_mentions(event, user, node, timestamp, **context): def notify_global_event(event, sender_user, node, timestamp, recipients, template=None, context=None): event_type = utils.find_subscription_type(event) sent_users = [] - users_subscriptions = [] - # Create a list of users' subscription dictionaries for recipient in recipients: - users_subscriptions.append(get_user_subscriptions(recipient, event_type)) - - # Merge the dictionaries using dict comprehension and exclude 'none' notification type. - # Output e.g. {'email_transactional': [u'vsu7t', u'evz43'], 'email_digest': [u'39dbp']} - merged_user_subscriptions = {k: [d[k][0] for d in users_subscriptions if d[k]] for k in users_subscriptions[0] if k != 'none'} - - # For each notification type store the list of users - for notify_type in merged_user_subscriptions: - # Group of users assigned this notification type - user_group_type = merged_user_subscriptions[notify_type] - # Check if user group is empty - if user_group_type: - sent_users += user_group_type - store_emails(merged_user_subscriptions[notify_type], notify_type, event, sender_user, node, timestamp, template, **context) + subscriptions = get_user_subscriptions(recipient, event_type) + for notification_type in subscriptions: + if (notification_type != 'none' and subscriptions[notification_type] and recipient._id in subscriptions[notification_type]): + store_emails([recipient._id], notification_type, event, sender_user, node, timestamp, template, **context) + sent_users.append(recipient._id) return sent_users From 62448a3987bddca92e33bbfc580fbf8d4efb30a1 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 27 Oct 2017 15:34:19 -0400 Subject: [PATCH 081/108] Upped reviews_notification in mixins --- reviews/models/mixins.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index 618884f3353..a2731cd4d13 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -213,45 +213,43 @@ def notify_submit(self, ev): def notify_resubmit(self, ev): context = self.get_context() - time_now = self.action.date_created if self.action is not None else timezone.now() - recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, - node=self.reviewable.node, template='reviews_resubmission_confirmation', - recipients=recipients, time_now=time_now) + template='reviews_resubmission_confirmation', + action=self.action) def notify_accept_reject(self, ev): context = self.get_context() - time_now = self.action.date_created if self.action is not None else timezone.now() context['notify_comment'] = not self.reviewable.provider.reviews_comments_private and self.action.comment context['is_rejected'] = self.action.to_state == workflow.States.REJECTED.value context['was_pending'] = self.action.from_state == workflow.States.PENDING.value - recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, - node=self.reviewable.node, template='reviews_submission_status', - recipients=recipients, time_now=time_now) + template='reviews_submission_status', + action=self.action) def notify_edit_comment(self, ev): context = self.get_context() - time_now = self.action.date_created if self.action is not None else timezone.now() if not self.reviewable.provider.reviews_comments_private and self.action.comment: - recipients = list(self.reviewable.node.contributors) reviews_signals.reviews_email.send(creator=ev.kwargs.get('user'), context=context, - node=self.reviewable.node, template='reviews_update_comment', - recipients=recipients, time_now=time_now) + template='reviews_update_comment', + action=self.action) def get_context(self): return { 'domain': settings.DOMAIN, 'reviewable': self.reviewable, 'workflow': self.reviewable.provider.reviews_workflow, - 'provider_url': self.reviewable.provider.domain or '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), + 'provider_url': self.reviewable.provider.domain or + '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), 'provider_contact_email': self.reviewable.provider.email_contact or 'contact@osf.io', 'provider_support_email': self.reviewable.provider.email_support or 'support@osf.io', } # Handle email notifications including: update comment, accept, and reject of submission. @reviews_signals.reviews_email.connect -def reviews_notification(self, creator, recipients, time_now, template, context, node): +def reviews_notification(self, creator, template, context, action): + recipients = list(action.target.node.contributors) + time_now = action.date_created if action is not None else timezone.now() + node=action.target.node emails.notify_global_event( event='global_reviews', sender_user=creator, From 7c2448ead2c752111174f8bdb2b6a905285acf63 Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 27 Oct 2017 15:47:42 -0400 Subject: [PATCH 082/108] update test --- tests/test_notifications.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 29deb701f2d..4a29f49cdbe 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -1817,6 +1817,7 @@ def setUp(self): 'provider_contact_email': 'contact@osf.io', 'provider_support_email': 'support@osf.io', } + self.action = factories.ActionFactory() factories.NotificationSubscriptionFactory( _id=self.user._id + '_' + 'global_comments', user=self.user, @@ -1847,7 +1848,5 @@ def test_reviews_submit_notification(self, mock_send_email): @mock.patch('website.notifications.emails.notify_global_event') def test_reviews_notification(self, mock_notify): - mixins.reviews_notification(self, creator=self.sender, context=self.context_info, - node=self.preprint.node, recipients=[self.sender, self.user], - time_now=timezone.now(), template='test.html.mako') + mixins.reviews_notification(self, creator=self.sender, context=self.context_info, action=self.action, template='test.html.mako') assert_true(mock_notify.called) From bfd483e246b596328e9fd33ac3588a50a92c9a2d Mon Sep 17 00:00:00 2001 From: Sherif Abdelhamid Date: Fri, 27 Oct 2017 17:03:46 -0400 Subject: [PATCH 083/108] Make flake8 happy --- reviews/models/mixins.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/reviews/models/mixins.py b/reviews/models/mixins.py index a2731cd4d13..57cb25e6a81 100644 --- a/reviews/models/mixins.py +++ b/reviews/models/mixins.py @@ -238,8 +238,7 @@ def get_context(self): 'domain': settings.DOMAIN, 'reviewable': self.reviewable, 'workflow': self.reviewable.provider.reviews_workflow, - 'provider_url': self.reviewable.provider.domain or - '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), + 'provider_url': self.reviewable.provider.domain or '{domain}preprints/{provider_id}'.format(domain=settings.DOMAIN, provider_id=self.reviewable.provider._id), 'provider_contact_email': self.reviewable.provider.email_contact or 'contact@osf.io', 'provider_support_email': self.reviewable.provider.email_support or 'support@osf.io', } @@ -249,7 +248,7 @@ def get_context(self): def reviews_notification(self, creator, template, context, action): recipients = list(action.target.node.contributors) time_now = action.date_created if action is not None else timezone.now() - node=action.target.node + node = action.target.node emails.notify_global_event( event='global_reviews', sender_user=creator, From 4cacadfe60ddd15e9bc230b19bbbf066515fa55e Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Mon, 30 Oct 2017 00:23:26 -0400 Subject: [PATCH 084/108] Populate global during Command.handle - Avoids scoping issue when test fixtures are torn down, but global is still set. - Update admin to use call_command --- admin/preprint_providers/views.py | 4 ++-- osf/management/commands/populate_custom_taxonomies.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/admin/preprint_providers/views.py b/admin/preprint_providers/views.py index 4cd6cab03fc..2a4b5133b16 100644 --- a/admin/preprint_providers/views.py +++ b/admin/preprint_providers/views.py @@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse_lazy from django.http import HttpResponse, JsonResponse from django.views.generic import ListView, DetailView, View, CreateView, DeleteView, TemplateView, UpdateView +from django.core.management import call_command from django.contrib.auth.mixins import PermissionRequiredMixin from django.forms.models import model_to_dict from django.shortcuts import redirect @@ -248,8 +249,7 @@ def get_page_provider(self): return PreprintProvider.objects.get(id=page_provider_id) def add_subjects(self, provider, subject_data): - from osf.management.commands.populate_custom_taxonomies import migrate - migrate(provider=provider._id, data=subject_data) + call_command('populate_custom_taxonomies', '--provider', provider._id, '--data', json.dumps(subject_data)) def create_or_update_provider(self, provider_data): provider = self.get_page_provider() diff --git a/osf/management/commands/populate_custom_taxonomies.py b/osf/management/commands/populate_custom_taxonomies.py index 359c68fcb6e..6138e0e63b3 100644 --- a/osf/management/commands/populate_custom_taxonomies.py +++ b/osf/management/commands/populate_custom_taxonomies.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -BEPRESS_PROVIDER = PreprintProvider.objects.filter(_id='osf').first() +BEPRESS_PROVIDER = None def validate_input(custom_provider, data, copy=False, add_missing=False): logger.info('Validating data') @@ -211,6 +211,8 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): + global BEPRESS_PROVIDER + BEPRESS_PROVIDER = PreprintProvider.objects.filter(_id='osf').first() dry_run = options.get('dry_run') provider = options['provider'] data = json.loads(options['data'] or '{}') From a8ec67f876c97c076bfc775a0799f4d284a63e77 Mon Sep 17 00:00:00 2001 From: Fabrice Mizero Date: Mon, 30 Oct 2017 16:07:28 -0400 Subject: [PATCH 085/108] Update yarn.lock --- yarn.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/yarn.lock b/yarn.lock index 1719658d5e2..d1a09be58ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2427,6 +2427,10 @@ lru-cache@~1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-1.0.6.tgz#aa50f97047422ac72543bda177a9c9d018d98452" +markdown-it-imsize@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz#cca0427905d05338a247cb9ca9d968c5cddd5170" + markdown-it-ins-del@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/markdown-it-ins-del/-/markdown-it-ins-del-0.1.1.tgz#8b421d8e788ee84a1d572eb3fcc0028ce431c9ea" From 526317e942e2256321aaea451ad0ccd60f7f61d6 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Mon, 30 Oct 2017 16:10:37 -0400 Subject: [PATCH 086/108] Use default home directory for user www-data --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e225a6b1ee2..3e775b2a237 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,14 +6,14 @@ ENV GOSU_VERSION=1.10 \ YARN_VERSION=1.1.0 # Libraries such as matplotlib require a HOME directory for cache and configuration -RUN usermod -d /home www-data \ - && chown www-data:www-data /home \ - # https://github.com/nodejs/docker-node/blob/9c25cbe93f9108fd1e506d14228afe4a3d04108f/8.2/Dockerfile - # gpg keys listed at https://github.com/nodejs/node#release-team - && set -ex \ +RUN set -ex \ + && mkdir -p /var/www \ + && chown www-data:www-data /var/www \ # GOSU && gpg --keyserver pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ && for key in \ + # https://github.com/nodejs/docker-node/blob/9c25cbe93f9108fd1e506d14228afe4a3d04108f/8.2/Dockerfile + # gpg keys listed at https://github.com/nodejs/node#release-team # Node 9554F04D7259F04124DE6B476D5A82AC7E37093B \ 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ From c02ab58d8e2d1ba88b86f6a0790795dea8a2bb94 Mon Sep 17 00:00:00 2001 From: Fabrice Mizero Date: Tue, 31 Oct 2017 11:02:45 -0400 Subject: [PATCH 087/108] Use ondataload callback --- website/static/js/nodesDeleteTreebeard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/static/js/nodesDeleteTreebeard.js b/website/static/js/nodesDeleteTreebeard.js index 5a4bf74d7e5..46f0b429aba 100644 --- a/website/static/js/nodesDeleteTreebeard.js +++ b/website/static/js/nodesDeleteTreebeard.js @@ -64,7 +64,7 @@ function NodesDeleteTreebeard(divID, data, nodesState, nodesOriginal) { } ]; }, - onload : function () { + ondataload : function () { var tb = this; expandOnLoad.call(tb); }, From b2ac084d011e787c2025d259fa1c9b98fbc25958 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Wed, 1 Nov 2017 17:28:08 -0400 Subject: [PATCH 088/108] Update docker-compose with new local ember images from quay and clean up commands [ci skip] --- docker-compose.override.yml | 39 ++++++++++++++++++++++++++++---- docker-compose.yml | 44 +++++++++++++++++++------------------ docker-sync.yml | 10 ++++----- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 096a7d1903c..e3cadbabe03 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -62,7 +62,16 @@ services: ## - emberosf-sync:/ember-osf ## depends_on: ## - emberosf -## command: /bin/bash -c "cd /ember-osf && yarn link && cd /code && yarn link @centerforopenscience/ember-osf && yarn --pure-lockfile --ignore-engines && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4200" +## command: +## - /bin/bash +## - -c +## - cd /ember-osf && +## yarn link && +## cd /code && +## yarn link @centerforopenscience/ember-osf && +## ./node_modules/.bin/bower install --allow-root --config.interactive=false && +## yarn --frozen-lockfile && +## yarn start --host 0.0.0.0 --port 4201 --live-reload-port 41954 # registries: # volumes: @@ -73,7 +82,16 @@ services: ## - emberosf-sync:/ember-osf ## depends_on: ## - emberosf -## command: /bin/bash -c "cd /ember-osf && yarn link && cd /code && yarn link @centerforopenscience/ember-osf && yarn --pure-lockfile --ignore-engines && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4300" +## command: +## - /bin/bash +## - -c +## - cd /ember-osf && +## yarn link && +## cd /code && +## yarn link @centerforopenscience/ember-osf && +## ./node_modules/.bin/bower install --allow-root --config.interactive=false && +## yarn --frozen-lockfile && +## yarn start --host 0.0.0.0 --port 4202 --live-reload-port 41955 # reviews: # volumes: @@ -84,12 +102,25 @@ services: ## - emberosf-sync:/ember-osf ## depends_on: ## - emberosf -## command: /bin/bash -c "cd /ember-osf && yarn link && cd /code && yarn link ember-osf && yarn --pure-lockfile && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false && ./node_modules/ember-cli/bin/ember serve --host 0.0.0.0 --port 4400" +## command: +## - /bin/bash +## - -c +## - cd /ember-osf && +## yarn link && +## cd /code && +## yarn link @centerforopenscience/ember-osf && +## yarn --frozen-lockfile && +## yarn start --host 0.0.0.0 --port 4203 --live-reload-port 41956 + # # Use this for ember-osf linked development (with docker-sync): # emberosf: # build: ../ember-osf -# command: /bin/bash -c "yarn --pure-lockfile --ignore-engines && ./node_modules/bower/bin/bower install --allow-root --config.interactive=false" +# command: +# - /bin/bash +# - -c +# - yarn --frozen-lockfile --ignore-engines && +# ./node_modules/.bin/bower install --allow-root --config.interactive=false # volumes: # - emberosf-sync:/code:nocopy diff --git a/docker-compose.yml b/docker-compose.yml index 647067ee534..2d49562559c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -212,13 +212,13 @@ services: ############# preprints: - image: quay.io/centerforopenscience/ember-preprints:develop-development-local + image: quay.io/centerforopenscience/osf-preprints:develop-local command: - /bin/bash - -c - - yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && + - yarn --frozen-lockfile && ./node_modules/.bin/bower install --allow-root --config.interactive=false && - ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4200 --live-reload-port 41953 + yarn start --host 0.0.0.0 --port 4201 --live-reload-port 41954 restart: unless-stopped depends_on: - api @@ -226,11 +226,11 @@ services: environment: - BACKEND=local expose: - - 4200 - - 41953 + - 4201 + - 41954 ports: - - 4200:4200 - - 41953:41953 + - 4201:4201 + - 41954:41954 volumes: - preprints_dist_vol:/code/dist stdin_open: true @@ -240,13 +240,13 @@ services: ############## registries: - image: quay.io/centerforopenscience/ember-osf-registries:develop-development-local + image: quay.io/centerforopenscience/osf-registries:develop-local command: - /bin/bash - -c - - yarn --pure-lockfile --ignore-engines --no-progress --no-emoji && + - yarn --frozen-lockfile && ./node_modules/.bin/bower install --allow-root --config.interactive=false && - ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4300 + yarn start --host 0.0.0.0 --port 4202 --live-reload-port 41955 restart: unless-stopped depends_on: - api @@ -254,24 +254,26 @@ services: environment: - BACKEND=local expose: - - 4300 + - 4202 + - 41955 ports: - - 4300:4300 + - 4202:4202 + - 41955:41955 volumes: - registries_dist_vol:/code/dist stdin_open: true - ############## + ########### # Reviews # - ############## + ########### reviews: - image: quay.io/centerforopenscience/osf-reviews:develop + image: quay.io/centerforopenscience/osf-reviews:develop-local command: - /bin/bash - -c - - yarn --pure-lockfile --no-progress --no-emoji && - ./node_modules/.bin/ember serve --host 0.0.0.0 --port 4400 + - yarn --frozen-lockfile && + yarn start --host 0.0.0.0 --port 4203 --live-reload-port 41956 restart: unless-stopped depends_on: - api @@ -279,11 +281,11 @@ services: environment: - BACKEND=local expose: - - 4400 - - 41954 + - 4203 + - 41956 ports: - - 4400:4400 - - 41954:41954 + - 4203:4203 + - 41956:41956 volumes: - reviews_dist_vol:/code/dist stdin_open: true diff --git a/docker-sync.yml b/docker-sync.yml index 3a4669c5052..2c4cd47151a 100644 --- a/docker-sync.yml +++ b/docker-sync.yml @@ -24,16 +24,16 @@ syncs: # sync_excludes: ['.DS_Store', '*.pyc', '*.tmp', '.git', '.idea'] # watch_excludes: ['.*\.DS_Store', '.*\.pyc', '.*\.tmp', '.*/\.git', '.*/\.idea'] -# preprints-sync: -# src: '../ember-preprints' +# emberosf-sync: +# src: '../ember-osf/' # sync_strategy: 'native_osx' # sync_args: [ '-prefer newer' ] # sync_excludes_type: 'Name' # sync_excludes: ['.DS_Store', '*.map', '*.pyc', '*.tmp', '.git', '.idea', 'bower_components', 'node_modules', 'tmp', 'dist'] # watch_excludes: ['.*\.DS_Store', '.*\.map', '.*\.pyc', '.*\.tmp', '.*/\.git', '.*/\.idea', '.*/bower_components', '.*/node_modules', '.*/tmp', '.*/dist'] -# emberosf-sync: -# src: '../ember-osf/' +# preprints-sync: +# src: '../ember-osf-preprints' # sync_strategy: 'native_osx' # sync_args: [ '-prefer newer' ] # sync_excludes_type: 'Name' @@ -50,7 +50,7 @@ syncs: # reviews-sync: # src: '../ember-osf-reviews' -# sync_strategy: 'unison' +# sync_strategy: 'native_osx' # sync_args: [ '-prefer newer' ] # sync_excludes_type: 'Name' # sync_excludes: ['.DS_Store', '*.map', '*.pyc', '*.tmp', '.git', '.idea', 'bower_components', 'node_modules', 'tmp', 'dist'] From a115bdaf2e7bfcf16cc06c4f1ac667f04351a46f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 2 Nov 2017 16:20:12 -0400 Subject: [PATCH 089/108] fix link project recognized as child components --- website/project/views/node.py | 2 ++ website/templates/project/project_base.mako | 2 +- website/views.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/website/project/views/node.py b/website/project/views/node.py index bfeba53c08f..a0293f86aba 100644 --- a/website/project/views/node.py +++ b/website/project/views/node.py @@ -15,6 +15,7 @@ from framework.auth.decorators import must_be_logged_in, collect_auth from framework.exceptions import HTTPError from osf.models.nodelog import NodeLog +from osf.models.node import Node from website import language @@ -700,6 +701,7 @@ def _view_project(node, auth, primary=False, 'date_modified': iso8601format(node.logs.latest().date) if node.logs.exists() else '', 'tags': list(node.tags.filter(system=False).values_list('name', flat=True)), 'children': node.nodes_active.exists(), + 'childExists': bool(Node.objects.get_children(node, active=True)), 'is_registration': is_registration, 'is_pending_registration': node.is_pending_registration if is_registration else False, 'is_retracted': node.is_retracted if is_registration else False, diff --git a/website/templates/project/project_base.mako b/website/templates/project/project_base.mako index 7fb6005faa2..f1213ff8acd 100644 --- a/website/templates/project/project_base.mako +++ b/website/templates/project/project_base.mako @@ -92,7 +92,7 @@ parentTitle: ${ parent_title | sjson, n }, parentRegisterUrl: ${parent_registration_url | sjson, n }, parentExists: ${ parent_exists | sjson, n}, - childExists: ${ node['children'] | sjson, n}, + childExists: ${ node['childExists'] | sjson, n}, registrationMetaSchemas: ${ node['registered_schemas'] | sjson, n }, registrationMetaData: ${ node['registered_meta'] | sjson, n }, contributors: ${ node['contributors'] | sjson, n } diff --git a/website/views.py b/website/views.py index 551a6e2b2cf..058bbaed465 100644 --- a/website/views.py +++ b/website/views.py @@ -22,7 +22,7 @@ from framework.auth.core import get_current_user_id from website.institutions.views import serialize_institution -from osf.models import BaseFileNode, Guid, Institution, PreprintService, AbstractNode +from osf.models import BaseFileNode, Guid, Institution, PreprintService, AbstractNode, Node from website.settings import EXTERNAL_EMBER_APPS, PROXY_EMBER_APPS, INSTITUTION_DISPLAY_NODE_THRESHOLD from website.project.model import has_anonymous_link from website.util import permissions @@ -92,7 +92,7 @@ def serialize_node_summary(node, auth, primary=True, show_path=False): 'title': node.title, 'category': node.category, 'isPreprint': bool(node.preprint_file_id), - 'childExists': node.nodes_active.exists(), + 'childExists': bool(Node.objects.get_children(node, active=True)), 'is_admin': node.has_permission(user, permissions.ADMIN), 'is_contributor': node.is_contributor(user), 'logged_in': auth.logged_in, From 1294dc7b308c1d9c83d6ae11666d0b06b0bd8f52 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 3 Nov 2017 11:29:11 -0400 Subject: [PATCH 090/108] add tests. Also make use of .exists(). Change ctx var name for naming consistency. --- tests/test_serializers.py | 25 +++++++++++++++++++++ website/project/views/node.py | 2 +- website/templates/project/project_base.mako | 2 +- website/views.py | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 3d17146d8aa..3676e24382a 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -175,6 +175,19 @@ def test_serialize_node_summary_private_fork_private_project_should_include_is_f assert_false(res['can_view']) assert_true(res['is_fork']) + def test_serialize_node_summary_child_exists(self): + user = UserFactory() + parent_node = ProjectFactory(creator=user) + linked_node = ProjectFactory(creator=user) + result = _view_project(parent_node, Auth(user)) + assert_equal(result['node']['child_exists'], False) + parent_node.add_node_link(linked_node, Auth(user), save=True) + result = _view_project(parent_node, Auth(user)) + assert_equal(result['node']['child_exists'], False) + child_component = NodeFactory(creator=user, parent=parent_node) + result = _view_project(parent_node, Auth(user)) + assert_equal(result['node']['child_exists'], True) + def test_serialize_node_search_returns_only_visible_contributors(self): node = NodeFactory() non_visible_contributor = UserFactory() @@ -213,6 +226,18 @@ def test_view_project_pending_registration_for_write_contributor_does_not_contai assert_equal(result['node']['disapproval_link'], '') pending_reg.remove() + def test_view_project_child_exists(self): + linked_node = ProjectFactory(creator=self.user) + result = _view_project(self.node, Auth(self.user)) + assert_equal(result['node']['child_exists'], False) + self.node.add_node_link(linked_node, Auth(self.user), save=True) + result = _view_project(self.node, Auth(self.user)) + assert_equal(result['node']['child_exists'], False) + child_component = NodeFactory(creator=self.user, parent=self.node) + result = _view_project(self.node, Auth(self.user)) + assert_equal(result['node']['child_exists'], True) + + class TestViewProjectEmbeds(OsfTestCase): diff --git a/website/project/views/node.py b/website/project/views/node.py index a0293f86aba..49ee6bfde91 100644 --- a/website/project/views/node.py +++ b/website/project/views/node.py @@ -701,7 +701,7 @@ def _view_project(node, auth, primary=False, 'date_modified': iso8601format(node.logs.latest().date) if node.logs.exists() else '', 'tags': list(node.tags.filter(system=False).values_list('name', flat=True)), 'children': node.nodes_active.exists(), - 'childExists': bool(Node.objects.get_children(node, active=True)), + 'child_exists': Node.objects.get_children(node, active=True).exists(), 'is_registration': is_registration, 'is_pending_registration': node.is_pending_registration if is_registration else False, 'is_retracted': node.is_retracted if is_registration else False, diff --git a/website/templates/project/project_base.mako b/website/templates/project/project_base.mako index f1213ff8acd..925e7668ec7 100644 --- a/website/templates/project/project_base.mako +++ b/website/templates/project/project_base.mako @@ -92,7 +92,7 @@ parentTitle: ${ parent_title | sjson, n }, parentRegisterUrl: ${parent_registration_url | sjson, n }, parentExists: ${ parent_exists | sjson, n}, - childExists: ${ node['childExists'] | sjson, n}, + childExists: ${ node['child_exists'] | sjson, n}, registrationMetaSchemas: ${ node['registered_schemas'] | sjson, n }, registrationMetaData: ${ node['registered_meta'] | sjson, n }, contributors: ${ node['contributors'] | sjson, n } diff --git a/website/views.py b/website/views.py index 058bbaed465..e022985560c 100644 --- a/website/views.py +++ b/website/views.py @@ -92,7 +92,7 @@ def serialize_node_summary(node, auth, primary=True, show_path=False): 'title': node.title, 'category': node.category, 'isPreprint': bool(node.preprint_file_id), - 'childExists': bool(Node.objects.get_children(node, active=True)), + 'childExists': Node.objects.get_children(node, active=True).exists(), 'is_admin': node.has_permission(user, permissions.ADMIN), 'is_contributor': node.is_contributor(user), 'logged_in': auth.logged_in, From b69da202105257ac55cb76fdd541fc9dea8c1732 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 25 Oct 2017 17:30:29 -0400 Subject: [PATCH 091/108] change docker and docker sync settings for ember --- docker-compose.override.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index e3cadbabe03..be3d81333e7 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -68,9 +68,10 @@ services: ## - cd /ember-osf && ## yarn link && ## cd /code && -## yarn link @centerforopenscience/ember-osf && +## (rm -r node_modules || true) && ## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn --frozen-lockfile && +## yarn link @centerforopenscience/ember-osf && ## yarn start --host 0.0.0.0 --port 4201 --live-reload-port 41954 # registries: @@ -88,9 +89,11 @@ services: ## - cd /ember-osf && ## yarn link && ## cd /code && -## yarn link @centerforopenscience/ember-osf && +## (rm -r node_modules || true) && +## (rm -r bower_components || true) && ## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn --frozen-lockfile && +## yarn link @centerforopenscience/ember-osf && ## yarn start --host 0.0.0.0 --port 4202 --live-reload-port 41955 # reviews: @@ -120,6 +123,8 @@ services: # - /bin/bash # - -c # - yarn --frozen-lockfile --ignore-engines && +# (rm -r node_modules || true) && +# (rm -r bower_components || true) && # ./node_modules/.bin/bower install --allow-root --config.interactive=false # volumes: # - emberosf-sync:/code:nocopy From dcfddf7fe901b92f4dfe25efdae4e46eac4fb364 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 3 Nov 2017 13:13:40 -0400 Subject: [PATCH 092/108] swich order of commands --- docker-compose.override.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index be3d81333e7..4f49c1df318 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -121,10 +121,10 @@ services: # build: ../ember-osf # command: # - /bin/bash -# - -c -# - yarn --frozen-lockfile --ignore-engines && +# - -c # (rm -r node_modules || true) && # (rm -r bower_components || true) && +# - yarn --frozen-lockfile --ignore-engines && # ./node_modules/.bin/bower install --allow-root --config.interactive=false # volumes: # - emberosf-sync:/code:nocopy From 178be86a63608a706fd575f65133bce9eeff206a Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 3 Nov 2017 13:35:45 -0400 Subject: [PATCH 093/108] Update order of commands --- docker-compose.override.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 4f49c1df318..37a082d80b2 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -69,9 +69,10 @@ services: ## yarn link && ## cd /code && ## (rm -r node_modules || true) && -## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn --frozen-lockfile && ## yarn link @centerforopenscience/ember-osf && +## (rm -r bower_components || true) && +## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn start --host 0.0.0.0 --port 4201 --live-reload-port 41954 # registries: @@ -90,10 +91,10 @@ services: ## yarn link && ## cd /code && ## (rm -r node_modules || true) && -## (rm -r bower_components || true) && -## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn --frozen-lockfile && ## yarn link @centerforopenscience/ember-osf && +## (rm -r bower_components || true) && +## ./node_modules/.bin/bower install --allow-root --config.interactive=false && ## yarn start --host 0.0.0.0 --port 4202 --live-reload-port 41955 # reviews: @@ -122,10 +123,10 @@ services: # command: # - /bin/bash # - -c -# (rm -r node_modules || true) && -# (rm -r bower_components || true) && -# - yarn --frozen-lockfile --ignore-engines && -# ./node_modules/.bin/bower install --allow-root --config.interactive=false +# - (rm -r node_modules || true) && +# yarn --frozen-lockfile --ignore-engines && +# (rm -r bower_components || true) && +# ./node_modules/.bin/bower install --allow-root --config.interactive=false # volumes: # - emberosf-sync:/code:nocopy From 4d55cc650ed3365308ad24015e93d3828e05d692 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 3 Nov 2017 13:51:49 -0400 Subject: [PATCH 094/108] changes port in local-dist.py and local-travis.py --- website/settings/local-dist.py | 6 +++--- website/settings/local-travis.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/settings/local-dist.py b/website/settings/local-dist.py index c7cc79abcac..1ce667419cb 100644 --- a/website/settings/local-dist.py +++ b/website/settings/local-dist.py @@ -33,17 +33,17 @@ EXTERNAL_EMBER_APPS = { 'preprints': { 'url': '/preprints/', - 'server': 'http://192.168.168.167:4200/', + 'server': 'http://192.168.168.167:4201/', 'path': '/preprints/' }, 'registries': { 'url': '/registries/', - 'server': 'http://192.168.168.167:4300', + 'server': 'http://192.168.168.167:4202', 'path': '/registries/' }, 'reviews': { 'url': '/reviews/', - 'server': 'http://localhost:4400', + 'server': 'http://localhost:4203', 'path': '/reviews/' } # 'meetings': { diff --git a/website/settings/local-travis.py b/website/settings/local-travis.py index 1345657760f..142b96246db 100644 --- a/website/settings/local-travis.py +++ b/website/settings/local-travis.py @@ -23,13 +23,13 @@ PREPRINT_PROVIDER_DOMAINS = { 'enabled': False, 'prefix': 'http://local.', - 'suffix': ':4200/' + 'suffix': ':4201/' } USE_EXTERNAL_EMBER = True EXTERNAL_EMBER_APPS = { 'preprints': { 'url': '/preprints/', - 'server': 'http://localhost:4200', + 'server': 'http://localhost:4201', 'path': os.environ.get('HOME') + '/preprints/' } } From f460704d04bc8f45ef997d06dff28934f3a94e6c Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 3 Nov 2017 13:53:20 -0400 Subject: [PATCH 095/108] Update provider domain. --- website/settings/local-dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/settings/local-dist.py b/website/settings/local-dist.py index 1ce667419cb..cf0318ee974 100644 --- a/website/settings/local-dist.py +++ b/website/settings/local-dist.py @@ -26,7 +26,7 @@ PREPRINT_PROVIDER_DOMAINS = { 'enabled': False, 'prefix': 'http://local.', - 'suffix': ':4200/' + 'suffix': ':4201/' } USE_EXTERNAL_EMBER = True PROXY_EMBER_APPS = False From c29c0c756dbbda8c47cd563fdb14c2771fc28f64 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 6 Nov 2017 09:31:43 -0500 Subject: [PATCH 096/108] make pagination for search results only, not added contributors (even if they are imported.) --- website/static/js/contribAdder.js | 9 +-------- website/templates/project/modal_add_contributor.mako | 6 ------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/website/static/js/contribAdder.js b/website/static/js/contribAdder.js index 9cfb0c89130..f2ffcfcb690 100644 --- a/website/static/js/contribAdder.js +++ b/website/static/js/contribAdder.js @@ -111,10 +111,6 @@ AddContributorViewModel = oop.extend(Paginator, { return self.query() && self.results().length && !self.parentImport(); }); - self.parentPagination = ko.pureComputed(function () { - return self.doneSearching() && self.parentImport(); - }); - self.noResults = ko.pureComputed(function () { return self.query() && !self.results().length && self.doneSearching(); }); @@ -295,10 +291,7 @@ AddContributorViewModel = oop.extend(Paginator, { } } self.doneSearching(true); - self.selection(pageToShow); - self.currentPage(self.pageToGet()); - self.numberOfPages(Math.ceil(contributors.length/5)); - self.addNewPaginators(true); + self.selection(contributors); } ); }, diff --git a/website/templates/project/modal_add_contributor.mako b/website/templates/project/modal_add_contributor.mako index d66ada4da49..9dba7421543 100644 --- a/website/templates/project/modal_add_contributor.mako +++ b/website/templates/project/modal_add_contributor.mako @@ -120,11 +120,6 @@

    -
    -
      -
    • -
    -

    Searching contributors...

    @@ -216,7 +211,6 @@ -
    From 40140c365ad444a9f8671c843148c1f896d4d4ba Mon Sep 17 00:00:00 2001 From: Alex Schiller Date: Mon, 6 Nov 2017 12:44:36 -0500 Subject: [PATCH 097/108] Stop script from updating deleted registrations --- scripts/fix_registration_unclaimed_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fix_registration_unclaimed_records.py b/scripts/fix_registration_unclaimed_records.py index ec1c9a3ba8d..89556c45a64 100644 --- a/scripts/fix_registration_unclaimed_records.py +++ b/scripts/fix_registration_unclaimed_records.py @@ -18,7 +18,7 @@ def main(): # If we're not running in dry mode log everything to a file script_utils.add_file_logger(logger, __file__) with transaction.atomic(): - qs = Registration.objects.filter(_contributors__is_registered=False) + qs = Registration.objects.filter(_contributors__is_registered=False, is_deleted=False) logger.info('Found {} registrations with unregistered contributors'.format(qs.count())) for registration in qs: registration_id = registration._id From 062ed0c7e8e44eebf02b10906cb750a644aa2b48 Mon Sep 17 00:00:00 2001 From: Lauren Revere Date: Tue, 7 Nov 2017 09:35:39 -0500 Subject: [PATCH 098/108] Add address --- website/templates/emails/prereg_challenge_accepted.html.mako | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/templates/emails/prereg_challenge_accepted.html.mako b/website/templates/emails/prereg_challenge_accepted.html.mako index f182700443f..52441deb1f6 100644 --- a/website/templates/emails/prereg_challenge_accepted.html.mako +++ b/website/templates/emails/prereg_challenge_accepted.html.mako @@ -36,3 +36,6 @@ Thank you for entering the Preregistration Challenge. Feel free to submit anothe Sincerely,
    The team at the Center for Open Science + +

    Center for Open Science
    210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903

    +Privacy Policy From 5b8a7e070b7443e8921567bf7935b1d94deb3660 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Tue, 7 Nov 2017 10:11:09 -0500 Subject: [PATCH 099/108] Add back bd99330f (log cloning optimization) This reverts commit c6e957e58ca3962c192997080356016134c25e9a. --- osf/models/node.py | 34 ++++++++++++++++++++++++++++------ osf/models/nodelog.py | 18 ------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/osf/models/node.py b/osf/models/node.py index 16381088885..5627c07d606 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -5,10 +5,12 @@ import urlparse import warnings +import bson from django.db.models import Q from dirtyfields import DirtyFieldsMixin from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation +from django.core.paginator import Paginator from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models, transaction, connection @@ -1651,9 +1653,7 @@ def register_node(self, schema, auth, data, parent=None): registered.affiliated_institutions.add(*self.affiliated_institutions.values_list('pk', flat=True)) # Clone each log from the original node for this registration. - logs = original.logs.all() - for log in logs: - log.clone_node_log(registered._id) + self.clone_logs(registered) registered.is_public = False # Copy unclaimed records to unregistered users for parent @@ -1908,9 +1908,7 @@ def fork_node(self, auth, title=None): ) # Clone each log from the original node for this fork. - for log in original.logs.all(): - log.clone_node_log(forked._id) - + self.clone_logs(forked) forked.refresh_from_db() # After fork callback @@ -1919,6 +1917,30 @@ def fork_node(self, auth, title=None): return forked + def clone_logs(self, node, page_size=100): + paginator = Paginator(self.logs.all(), page_size) + for page_num in paginator.page_range: + page = paginator.page(page_num) + # Instantiate NodeLogs "manually" + # because BaseModel#clone() is too slow for large projects + logs_to_create = [ + NodeLog( + _id=bson.ObjectId(), + action=log.action, + date=log.date, + params=log.params, + should_hide=log.should_hide, + foreign_user=log.foreign_user, + # Set foreign keys, not their objects + # to speed things up + node_id=node.pk, + user_id=log.user_id, + original_node_id=log.original_node_id + ) + for log in page + ] + NodeLog.objects.bulk_create(logs_to_create) + def use_as_template(self, auth, changes=None, top_level=True): """Create a new project, using an existing project as a template. diff --git a/osf/models/nodelog.py b/osf/models/nodelog.py index a7e8385a57f..b2625422847 100644 --- a/osf/models/nodelog.py +++ b/osf/models/nodelog.py @@ -171,23 +171,5 @@ def get_absolute_url(self): def absolute_url(self): return self.absolute_api_v2_url - def clone_node_log(self, node_id): - """ - When a node is forked or registered, all logs on the node need to be - cloned for the fork or registration. - - :param node_id: - :return: cloned log - """ - AbstractNode = apps.get_model('osf.AbstractNode') - original_log = self.load(self._id) - node = AbstractNode.load(node_id) - log_clone = original_log.clone() - log_clone.node = node - log_clone.original_node = original_log.original_node - log_clone.user = original_log.user - log_clone.save() - return log_clone - def _natural_key(self): return self._id From 8298d0d10b62ccd443fc23b24e157c805c106c40 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Tue, 7 Nov 2017 10:25:30 -0500 Subject: [PATCH 100/108] Fix cloning logs Paginator requires a sorted queryset OSF-8878 --- osf/models/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osf/models/node.py b/osf/models/node.py index 5627c07d606..34f08e19cd4 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -1918,7 +1918,7 @@ def fork_node(self, auth, title=None): return forked def clone_logs(self, node, page_size=100): - paginator = Paginator(self.logs.all(), page_size) + paginator = Paginator(self.logs.order_by('pk').all(), page_size) for page_num in paginator.page_range: page = paginator.page(page_num) # Instantiate NodeLogs "manually" From 65a28d27b31da9dec887339471f99c0a2ee42052 Mon Sep 17 00:00:00 2001 From: Fabrice Mizero Date: Tue, 7 Nov 2017 13:45:23 -0500 Subject: [PATCH 101/108] Name opened window --- website/static/js/fangorn.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/website/static/js/fangorn.js b/website/static/js/fangorn.js index bbe2eeeb046..9c72f5c964a 100644 --- a/website/static/js/fangorn.js +++ b/website/static/js/fangorn.js @@ -1348,7 +1348,7 @@ function gotoFileEvent (item, toUrl) { } if (COMMAND_KEYS.indexOf(tb.pressedKey) !== -1) { - window.open(fileurl, '_blank'); + window.open(fileurl, item.data.materialized); } else { window.open(fileurl, '_self'); } @@ -1376,12 +1376,6 @@ function _fangornTitleColumnHelper(tb, item, col, nameTitle, toUrl, classNameOpt attrs = { className: classNameOption, onclick: function(event) { - // Prevent gotoFileEvent from getting - // called more than once after user has clicked once - if (item.loading) { - return false; - } - item.loading = true; event.stopImmediatePropagation(); gotoFileEvent.call(tb, item, toUrl); } From ebe926f9434caa6575abb896b960e4754fac5666 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 7 Nov 2017 13:58:35 -0500 Subject: [PATCH 102/108] language and redirection changes --- website/static/js/nodesDelete.js | 70 ++++++++++++++++----- website/templates/project/nodes_delete.mako | 4 +- website/templates/project/project_base.mako | 3 + 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index b3067617288..1b0dd93357b 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -67,12 +67,21 @@ function batchNodesDelete(nodes) { data: nodesBatch }), success: function(){ - bootbox.alert({ - message: 'Project has been successfully deleted.', - callback: function(confirmed) { - window.location.href = '/dashboard/'; - } - }); + if (window.contextVars.node.nodeType === 'project') + bootbox.alert({ + message: 'Project has been successfully deleted.', + callback: function(confirmed) { + window.location.href = '/dashboard/'; + } + }); + + if (window.contextVars.node.nodeType === 'component') + bootbox.alert({ + message: 'Component has been successfully deleted.', + callback: function(confirmed) { + window.location = window.contextVars.node.parentUrl; + } + }); } }); } @@ -84,6 +93,8 @@ function batchNodesDelete(nodes) { */ var NodesDeleteViewModel = function(node) { var self = this; + self.nodeType = window.contextVars.node.nodeType; + self.parentUrl = window.contextVars.node.parentUrl; self.SELECT = 'select'; self.CONFIRM = 'confirm'; @@ -117,21 +128,50 @@ var NodesDeleteViewModel = function(node) { self.page = ko.observable(self.SELECT); self.pageTitle = ko.computed(function() { - return { - select: 'Delete Project', - confirm: 'Delete Project and Components' - }[self.page()]; + if (self.nodeType === 'project'){ + return { + select: 'Delete Project', + confirm: 'Delete Project and Components' + }[self.page()]; + } + + if (self.nodeType === 'component'){ + return { + select: 'Delete Component', + confirm: 'Delete Components' + }[self.page()]; + } + }); self.message = ko.computed(function() { if (self.page() === self.CONFIRM) { - return 'The following project and components will be deleted'; + if (self.nodeType === 'project') + return 'The following project and components will be deleted'; + + if (self.nodeType === 'component') + return 'The following components will be deleted' } - return { - select: 'It looks like your project has components within it. To delete this project, you must also delete all child components', - confirm: 'The following project and components will be deleted.' - }[self.page()]; + if (self.nodeType === 'project') + return { + select: 'It looks like your project has components within it. To delete this project, you must also delete all child components', + confirm: 'The following project and components will be deleted.' + }[self.page()]; + + if (self.nodeType === 'component') + return { + select: 'It looks like your componet has components within it. To delete this component, you must also delete all child components', + confirm: 'The following components will be deleted.' + }[self.page()]; + }); + + self.warning = ko.computed(function() { + if (self.nodeType === 'project') + return 'Please note that deleting your project will erase all your project data and this process is IRREVERSIBLE.'; + + if (self.nodeType === 'component') + return 'Please note that deleting your component will erase all your component data and this process is IRREVERSIBLE.'; }); }; diff --git a/website/templates/project/nodes_delete.mako b/website/templates/project/nodes_delete.mako index bb9142b8598..04ac796e7d2 100644 --- a/website/templates/project/nodes_delete.mako +++ b/website/templates/project/nodes_delete.mako @@ -57,9 +57,7 @@
    -

    - Please note that deleting your project will erase all your project data and this process is IRREVERSIBLE. -

    +

    Type the following to continue:

    diff --git a/website/templates/project/project_base.mako b/website/templates/project/project_base.mako index 60470ac3922..e3e46efeded 100644 --- a/website/templates/project/project_base.mako +++ b/website/templates/project/project_base.mako @@ -46,6 +46,7 @@ parent_exists = parent_node['exists'] parent_title = '' parent_registration_url = '' + parent_url = '' root_id = node['root_id'] if parent_exists: parent_title = "Private {0}".format(parent_node['category']) @@ -53,6 +54,7 @@ if parent_node['can_view'] or parent_node['is_contributor']: parent_title = parent_node['title'] parent_registration_url = parent_node['registrations_url'] + parent_url = parent_node['absolute_url'] %> // Mako variables accessible globally @@ -90,6 +92,7 @@ category: ${node['category_short'] | sjson, n }, rootId: ${ root_id | sjson, n }, parentTitle: ${ parent_title | sjson, n }, + parentUrl: ${ parent_url | sjson, n }, parentRegisterUrl: ${parent_registration_url | sjson, n }, parentExists: ${ parent_exists | sjson, n}, childExists: ${ node['child_exists'] | sjson, n}, From cc50c2ba6c9a2b70b127562ed0fab35944908b53 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 7 Nov 2017 14:23:48 -0500 Subject: [PATCH 103/108] fix tests --- website/static/js/nodesDelete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/static/js/nodesDelete.js b/website/static/js/nodesDelete.js index 1b0dd93357b..b710aac7bce 100644 --- a/website/static/js/nodesDelete.js +++ b/website/static/js/nodesDelete.js @@ -150,7 +150,7 @@ var NodesDeleteViewModel = function(node) { return 'The following project and components will be deleted'; if (self.nodeType === 'component') - return 'The following components will be deleted' + return 'The following components will be deleted'; } if (self.nodeType === 'project') From 4459caa7686542ab22247791cb9d0414ce76ae9c Mon Sep 17 00:00:00 2001 From: TomBaxter Date: Tue, 7 Nov 2017 14:36:03 -0500 Subject: [PATCH 104/108] Udate README-docker-compose.md s/tukumx/mongo/ [NO-TICKET] --- README-docker-compose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README-docker-compose.md b/README-docker-compose.md index c2f5ab02521..ac1ff45c966 100644 --- a/README-docker-compose.md +++ b/README-docker-compose.md @@ -120,7 +120,7 @@ Ubuntu: Skip install of docker-sync. instead... _NOTE: When the various requirements installations are complete these containers will exit. You should only need to run these containers after pulling code that changes python requirements or if you update the python requirements._ 2. Start Core Component Services (Detached) - - `$ docker-compose up -d elasticsearch postgres tokumx rabbitmq` + - `$ docker-compose up -d elasticsearch postgres mongo rabbitmq` 3. Remove your existing node_modules and start the assets watcher (Detached) - `$ rm -Rf ./node_modules` From d6af58f6c26697aad008e6ae25eef0b4573c242e Mon Sep 17 00:00:00 2001 From: Alex Schiller Date: Wed, 8 Nov 2017 10:17:58 -0500 Subject: [PATCH 105/108] Add linked_registration field, stop linked_node from showing not found for registrations --- api/base/serializers.py | 27 ++++++++ api/logs/serializers.py | 18 ++++- api_tests/nodes/views/test_node_logs.py | 89 ++++++++++++++++++++++++- website/static/js/components/logFeed.js | 2 +- website/static/js/logTextParser.js | 18 ++++- website/static/js/myProjects.js | 2 +- 6 files changed, 145 insertions(+), 11 deletions(-) diff --git a/api/base/serializers.py b/api/base/serializers.py index e3219d3e196..ee003407939 100644 --- a/api/base/serializers.py +++ b/api/base/serializers.py @@ -177,6 +177,33 @@ def should_be_none(self, instance): return not isinstance(self.field, RelationshipField) +class HideIfNotNodePointerLog(ConditionalField): + """ + This field will not be shown if the log is not a pointer log for a node + """ + def should_hide(self, instance): + pointer_param = instance.params.get('pointer', False) + if pointer_param: + node = AbstractNode.load(pointer_param['id']) + if node: + return node.type != 'osf.node' + return True + + +class HideIfNotRegistrationPointerLog(ConditionalField): + """ + This field will not be shown if the log is not a pointer log for a registration + """ + + def should_hide(self, instance): + pointer_param = instance.params.get('pointer', False) + if pointer_param: + node = AbstractNode.load(pointer_param['id']) + if node: + return node.type != 'osf.registration' + return True + + class HideIfProviderCommentsAnonymous(ConditionalField): """ If the action's provider has anonymous comments and the user does not have `view_actions` diff --git a/api/logs/serializers.py b/api/logs/serializers.py index 7fa69023951..7930672c72a 100644 --- a/api/logs/serializers.py +++ b/api/logs/serializers.py @@ -7,6 +7,8 @@ LinksField, is_anonymized, DateByVersion, + HideIfNotNodePointerLog, + HideIfNotRegistrationPointerLog, ) from osf.models import OSFUser, AbstractNode, PreprintService @@ -200,10 +202,20 @@ class Meta: ) # This would be a node_link, except that data isn't stored in the node log params - linked_node = RelationshipField( - related_view='nodes:node-detail', - related_view_kwargs={'node_id': ''} + linked_node = HideIfNotNodePointerLog( + RelationshipField( + related_view='nodes:node-detail', + related_view_kwargs={'node_id': ''} + ) + ) + + linked_registration = HideIfNotRegistrationPointerLog( + RelationshipField( + related_view='registrations:registration-detail', + related_view_kwargs={'node_id': ''} + ) ) + template_node = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': ''} diff --git a/api_tests/nodes/views/test_node_logs.py b/api_tests/nodes/views/test_node_logs.py index f9292bacd19..da82d53b61b 100644 --- a/api_tests/nodes/views/test_node_logs.py +++ b/api_tests/nodes/views/test_node_logs.py @@ -7,10 +7,12 @@ from api.base.settings.defaults import API_BASE from framework.auth.core import Auth -from osf.models import NodeLog +from osf.models import NodeLog, Registration, Sanction from osf_tests.factories import ( - ProjectFactory, AuthUserFactory, + ProjectFactory, + RegistrationFactory, + EmbargoFactory, ) from tests.base import assert_datetime_equal from website.util import disconnected_from_listeners @@ -31,7 +33,7 @@ def contrib(self): return AuthUserFactory() @pytest.fixture() - def creator(self): + def non_contrib(self): return AuthUserFactory() @pytest.fixture() @@ -46,6 +48,14 @@ def NodeLogFactory(self): def pointer(self, user): return ProjectFactory(creator=user) + @pytest.fixture() + def pointer_registration(self, user): + return RegistrationFactory(creator=user, is_public=True) + + @pytest.fixture() + def pointer_embargo(self, user): + return RegistrationFactory(creator=user, embargo=EmbargoFactory(user=user), is_public=False) + @pytest.fixture() def private_project(self, user): return ProjectFactory(is_public=False, creator=user) @@ -191,7 +201,80 @@ def test_pointers(self, app, user, user_auth, contrib, public_project, pointer, res = app.get(public_url, auth=contrib.auth) assert res.status_code == 200 assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + def test_registration_pointers(self, app, user, user_auth, non_contrib, public_project, pointer_registration, public_url): + public_project.add_pointer(pointer_registration, auth=user_auth, save=True) + assert public_project.logs.latest().action == 'pointer_created' + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert len(res.json['data']) == public_project.logs.count() + assert res.json['data'][API_LATEST]['attributes']['action'] == 'pointer_created' + assert res.json['data'][API_LATEST]['relationships']['linked_registration']['data']['id'] == pointer_registration._id + + # Confirm pointer contains correct data for various users + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer_registration._id + + res = app.get(public_url, auth=non_contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer_registration._id + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer_registration._id + + # Delete pointer and make sure no data shown + pointer_registration.remove_node(Auth(user)) + pointer_registration.save() + + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url, auth=non_contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + def test_embargo_pointers(self, app, user, user_auth, non_contrib, public_project, pointer_embargo, public_url): + public_project.add_pointer(pointer_embargo, auth=user_auth, save=True) + assert public_project.logs.latest().action == 'pointer_created' + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert len(res.json['data']) == public_project.logs.count() + assert res.json['data'][API_LATEST]['attributes']['action'] == 'pointer_created' + assert res.json['data'][API_LATEST]['relationships']['linked_registration']['data']['id'] == pointer_embargo._id + + # Confirm pointer contains correct data for various users + assert res.json['data'][API_LATEST]['attributes']['params']['pointer']['id'] == pointer_embargo._id + + res = app.get(public_url, auth=non_contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + # Delete pointer and make sure no data shown + pointer_embargo.remove_node(Auth(user)) + pointer_embargo.save() + + res = app.get(public_url, auth=user.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + res = app.get(public_url, auth=non_contrib.auth) + assert res.status_code == 200 + assert res.json['data'][API_LATEST]['attributes']['params']['pointer'] is None + + @pytest.mark.django_db class TestNodeLogFiltering(TestNodeLogList): diff --git a/website/static/js/components/logFeed.js b/website/static/js/components/logFeed.js index 6e28dfd0995..5c938f95b70 100644 --- a/website/static/js/components/logFeed.js +++ b/website/static/js/components/logFeed.js @@ -17,7 +17,7 @@ var _buildLogUrl = function(node, page, limitLogs) { var logPage = page || 1; var urlPrefix = (node.isRegistration || node.is_registration) ? 'registrations' : 'nodes'; var size = limitLogs ? LOG_PAGE_SIZE_LIMITED : LOG_PAGE_SIZE; - var query = { 'page[size]': size, 'page': logPage, 'embed': ['original_node', 'user', 'linked_node', 'template_node'], 'profile_image_size': PROFILE_IMAGE_SIZE}; + var query = { 'page[size]': size, 'page': logPage, 'embed': ['original_node', 'user', 'linked_node', 'linked_registration', 'template_node'], 'profile_image_size': PROFILE_IMAGE_SIZE}; var viewOnly = $osf.urlParams().view_only; if (viewOnly) { query.view_only = viewOnly; diff --git a/website/static/js/logTextParser.js b/website/static/js/logTextParser.js index 69980df3f7b..5118ab4e98b 100644 --- a/website/static/js/logTextParser.js +++ b/website/static/js/logTextParser.js @@ -300,9 +300,13 @@ var LogPieces = { pointer: { view: function (ctrl, logObject) { var linked_node = logObject.embeds.linked_node; - if (paramIsReturned(linked_node, logObject) && !linked_node.errors) { + if (linked_node && paramIsReturned(linked_node, logObject) && !linked_node.errors) { return m('a', {href: $osf.toRelativeUrl(linked_node.data.links.html, window)}, linked_node.data.attributes.title); } + var linked_registration = logObject.embeds.linked_registration; + if (linked_registration && paramIsReturned(linked_registration, logObject) && !linked_registration.errors) { + return m('a', {href: $osf.toRelativeUrl(linked_registration.data.links.html, window)}, linked_registration.data.attributes.title); + } return m('span', 'a project'); } }, @@ -310,12 +314,20 @@ var LogPieces = { pointer_category: { view: function (ctrl, logObject) { var linked_node = logObject.embeds.linked_node; - if (paramIsReturned(linked_node, logObject) && !linked_node.errors) { - var category = linked_node.data.attributes.category; + var category = ''; + if (linked_node && paramIsReturned(linked_node, logObject) && !linked_node.errors) { + category = linked_node.data.attributes.category; if (category !== '') { return m('span', linked_node.data.attributes.category); } } + var linked_registration = logObject.embeds.linked_registration; + if (linked_registration && paramIsReturned(linked_registration, logObject) && !linked_registration.errors) { + category = linked_registration.data.attributes.category; + if (category !== '') { + return m('span', linked_registration.data.attributes.category); + } + } return m('span', ''); } }, diff --git a/website/static/js/myProjects.js b/website/static/js/myProjects.js index 2d773ce98be..9d1ab2201e2 100644 --- a/website/static/js/myProjects.js +++ b/website/static/js/myProjects.js @@ -505,7 +505,7 @@ var MyProjects = { if(!item.data.attributes.retracted){ var urlPrefix = item.data.attributes.registration ? 'registrations' : 'nodes'; // TODO assess sparse field usage (some already implemented) - var url = $osf.apiV2Url(urlPrefix + '/' + id + '/logs/', { query : { 'page[size]' : 6, 'embed' : ['original_node', 'user', 'linked_node', 'template_node'], 'profile_image_size': PROFILE_IMAGE_SIZE, 'fields[users]' : sparseUserFields}}); + var url = $osf.apiV2Url(urlPrefix + '/' + id + '/logs/', { query : { 'page[size]' : 6, 'embed' : ['original_node', 'user', 'linked_node', 'linked_registration', 'template_node'], 'profile_image_size': PROFILE_IMAGE_SIZE, 'fields[users]' : sparseUserFields}}); var promise = self.getLogs(url); return promise; } From 9aa3de1e0cb4d9714f9c810639c1bd0c0358d9b5 Mon Sep 17 00:00:00 2001 From: "Barrett K. Harber" Date: Wed, 8 Nov 2017 16:37:32 -0500 Subject: [PATCH 106/108] Add ability to specify elasticsearch kwargs for SSL certs --- website/search/elastic_search.py | 3 ++- website/settings/defaults.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/website/search/elastic_search.py b/website/search/elastic_search.py index b199171d0f5..51b5e0141c4 100644 --- a/website/search/elastic_search.py +++ b/website/search/elastic_search.py @@ -76,7 +76,8 @@ def client(): CLIENT = Elasticsearch( settings.ELASTIC_URI, request_timeout=settings.ELASTIC_TIMEOUT, - retry_on_timeout=True + retry_on_timeout=True, + **settings.ELASIC_KWARGS ) logging.getLogger('elasticsearch').setLevel(logging.WARN) logging.getLogger('elasticsearch.trace').setLevel(logging.WARN) diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 4e02f8b28e4..05b71a62264 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -98,6 +98,13 @@ def parent_dir(path): ELASTIC_URI = 'localhost:9200' ELASTIC_TIMEOUT = 10 ELASTIC_INDEX = 'website' +ELASIC_KWARGS = { + # 'use_ssl': False, + # 'verify_certs': True, + # 'ca_certs': None, + # 'client_cert': None, + # 'client_key': None +} # Sessions COOKIE_NAME = 'osf' From 6474df255beb418b7b2a3d11e3024a42b82c3921 Mon Sep 17 00:00:00 2001 From: Fabrice Mizero Date: Thu, 9 Nov 2017 15:38:04 -0500 Subject: [PATCH 107/108] Revert changes and orig. hotfix --- website/static/js/fangorn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/static/js/fangorn.js b/website/static/js/fangorn.js index 9c72f5c964a..904499bc2e7 100644 --- a/website/static/js/fangorn.js +++ b/website/static/js/fangorn.js @@ -1348,7 +1348,7 @@ function gotoFileEvent (item, toUrl) { } if (COMMAND_KEYS.indexOf(tb.pressedKey) !== -1) { - window.open(fileurl, item.data.materialized); + window.open(fileurl, '_blank'); } else { window.open(fileurl, '_self'); } From 56e601a5406662fe6a034522453f31a8999aaf83 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Mon, 13 Nov 2017 20:59:17 -0500 Subject: [PATCH 108/108] Bump version and update changelog --- CHANGELOG | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 432a06bcba3..73c6d1c31ef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ Changelog ********* +0.124.0 (2017-11-13) +==================== + +- Critical bug fixes + 0.123.0 (2017-10-26) ==================== diff --git a/package.json b/package.json index d680cb737f0..efe2645d473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "0.123.0", + "version": "0.124.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science",