forked from otwcode/otwarchive
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request otwcode#2442 from ariana-paris/AO3-4359-Mass-impor…
…t-bookmarks AO3-4359 Mass import bookmarks
- Loading branch information
Showing
11 changed files
with
744 additions
and
413 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
class Api::V1::BookmarksController < Api::V1::BaseController | ||
respond_to :json | ||
|
||
def create | ||
archivist = User.find_by_login(params[:archivist]) | ||
bookmarks = params[:bookmarks] | ||
bookmarks_responses = [] | ||
@bookmarks = [] | ||
|
||
# check for top-level errors (not an archivist, no bookmarks...) | ||
status, messages = batch_errors(archivist, bookmarks) | ||
|
||
if status == :ok | ||
# Flag error and successes | ||
@some_errors = @some_success = false | ||
|
||
# Process the works, updating the flags | ||
bookmarks.each do |bookmark| | ||
bookmarks_responses << import_bookmark(archivist, bookmark) | ||
end | ||
|
||
# set final response code and message depending on the flags | ||
messages = response_message(messages) | ||
end | ||
|
||
render status: status, json: { status: status, messages: messages, bookmarks: bookmarks_responses } | ||
end | ||
|
||
private | ||
|
||
# Set messages based on success and error flags | ||
def response_message(messages) | ||
if @some_success && @some_errors | ||
messages << "At least one bookmark was not created. Please check the individual bookmark results for further information." | ||
elsif !@some_success && @some_errors | ||
messages << "None of the bookmarks were created. Please check the individual bookmark results for further information." | ||
else | ||
messages << "All bookmarks were successfully created." | ||
end | ||
messages | ||
end | ||
|
||
# Returns a hash | ||
def import_bookmark(archivist, params) | ||
bookmark_request = bookmark_request(archivist, params) | ||
bookmark_status, bookmark_messages = bookmark_errors(archivist, bookmark_request) | ||
bookmark_url = "" | ||
original_url = "" | ||
@some_errors = true | ||
if bookmark_status == :ok | ||
begin | ||
bookmark = Bookmark.new(bookmark_request) | ||
bookmarkable = bookmark.bookmarkable | ||
if bookmarkable.save && bookmark.save | ||
@bookmarks << bookmark | ||
@some_success = true | ||
@some_errors = false | ||
bookmark_status = :created | ||
bookmark_url = bookmark_url(bookmark) | ||
bookmark_messages << "Successfully created bookmark for \"" + bookmarkable.title + "\"." | ||
else | ||
bookmark_status = :unprocessable_entity | ||
bookmark_messages << bookmarkable.errors.full_messages + bookmark.errors.full_messages | ||
end | ||
rescue => exception | ||
bookmark_status = :unprocessable_entity | ||
bookmark_messages << exception.message | ||
end | ||
original_url = bookmarkable.url if bookmarkable | ||
end | ||
|
||
{ | ||
status: bookmark_status, | ||
archive_url: bookmark_url, | ||
original_id: params[:id], | ||
original_url: original_url, | ||
messages: bookmark_messages.flatten | ||
} | ||
end | ||
|
||
# Handling for requests that are incomplete | ||
def bookmark_errors(archivist, bookmark_request) | ||
status = :bad_request | ||
errors = [] | ||
|
||
# Perform basic validation which the ExternalWork model doesn't do or returns strange messages for | ||
# (title is validated correctly in the model and so isn't checked here) | ||
external_work = bookmark_request[:external] | ||
url = external_work[:url] | ||
author = external_work[:author] | ||
fandom = external_work[:fandom_string] | ||
|
||
if url.nil? | ||
# Unreachable and AO3 URLs are handled in the ExternalWork model | ||
errors << "This bookmark does not contain a URL to an external site. Please specify a valid, non-AO3 URL." | ||
end | ||
|
||
if author.nil? || author == "" | ||
errors << "This bookmark does not contain an external author name. Please specify an author." | ||
end | ||
|
||
if fandom.nil? || fandom == "" | ||
errors << "This bookmark does not contain a fandom. Please specify a fandom." | ||
end | ||
|
||
archivist_bookmarks = Bookmark.find_all_by_pseud_id(archivist.default_pseud.id) | ||
|
||
unless archivist_bookmarks.empty? | ||
archivist_bookmarks.each do |bookmark| | ||
if bookmark.bookmarkable_type == "ExternalWork" && ExternalWork.find(bookmark.bookmarkable_id).url == url | ||
errors << "There is already a bookmark for this archivist and the URL #{url}" | ||
end | ||
end | ||
end | ||
|
||
status = :ok if errors.empty? | ||
[status, errors] | ||
end | ||
|
||
# Map Json request to Bookmark request for external work | ||
def bookmark_request(archivist, params) | ||
{ | ||
pseud_id: archivist.default_pseud.id, | ||
external: { | ||
url: params[:url], | ||
author: params[:author], | ||
title: params[:title], | ||
summary: params[:summary], | ||
fandom_string: params[:fandom_string], | ||
rating_string: params[:rating_string], | ||
category_string: params[:category_string].to_s.split(","), # category is actually an array on bookmarks | ||
relationship_string: params[:relationship_string], | ||
character_string: params[:character_string] | ||
}, | ||
notes: params[:notes], | ||
tag_string: params[:tag_string], | ||
collection_names: params[:collection_names], | ||
private: params[:private].blank? ? false : params[:private], | ||
rec: params[:recommendation].blank? ? false : params[:recommendation] | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,183 +1,4 @@ | ||
class Api::V1::ImportController < Api::V1::BaseController | ||
respond_to :json | ||
|
||
# Imports multiple works with their meta from external URLs | ||
# Params: | ||
# +params+:: a JSON object containing the following: | ||
# - archivist: username of an existing archivist | ||
# - post_without_preview: false = import as drafts, true = import and post | ||
# - send_claim_emails: false = don't send emails (for testing), true = send emails | ||
# - array of works to import | ||
def create | ||
archivist = User.find_by_login(params[:archivist]) | ||
external_works = params[:works] | ||
works_responses = [] | ||
@works = [] | ||
|
||
# check for top-level errors (not an archivist, no works...) | ||
status, messages = batch_errors(archivist, external_works) | ||
|
||
if status == :ok | ||
# Flag error and successes | ||
@some_errors = @some_success = false | ||
|
||
# Process the works, updating the flags | ||
external_works.each do |external_work| | ||
works_responses << import_work(archivist, external_work.merge(params)) | ||
end | ||
|
||
# Send claim notification emails if required | ||
if params[:send_claim_emails] && !@works.empty? | ||
send_external_invites(@works, archivist) | ||
end | ||
|
||
# set final response code and message depending on the flags | ||
status, messages = response_code(messages) | ||
end | ||
render status: status, json: { status: status, messages: messages, works: works_responses } | ||
end | ||
|
||
private | ||
|
||
# Set HTTP responses based on success and error flags | ||
def response_code(messages) | ||
if @some_success && @some_errors | ||
status = :multi_status | ||
messages << "At least one work was not imported. Please check the works array for further information." | ||
elsif !@some_success && @some_errors | ||
status = :unprocessable_entity | ||
messages << "None of the works were imported. Please check the works array for further information." | ||
else | ||
status = :created | ||
messages << "All works were successfully imported." | ||
end | ||
[status, messages] | ||
end | ||
|
||
# Use the story parser to import works from the chapter URLs, | ||
# and set success or error flag depending on the outcome | ||
# Returns a hash | ||
def import_work(archivist, external_work) | ||
work_status, work_messages = work_errors(external_work) | ||
work_url = "" | ||
original_url = [] | ||
work = nil | ||
if work_status == :ok | ||
urls = external_work[:chapter_urls] | ||
original_url = urls.first | ||
storyparser = StoryParser.new | ||
options = options(archivist, external_work) | ||
begin | ||
work = storyparser.download_and_parse_chapters_into_story(urls, options) | ||
work.save | ||
if work.persisted? && work.chapters.all? { |c| c.errors.empty? } | ||
@works << work | ||
@some_success = true | ||
work_status = :created | ||
work_url = work_url(work) | ||
work_messages << "Successfully created work \"" + work.title + "\"." | ||
else | ||
@some_errors = true | ||
work_status = :unprocessable_entity | ||
work_messages << work.errors.messages.values.flatten if work | ||
|
||
# Extract work chapter errors and append the chapter number to them for readability | ||
if work.chapters | ||
chapter_errors = work.chapters.map(&:errors).each_with_index | ||
chapter_messages = chapter_errors.map do |e, i| | ||
e.messages.values.flatten.map { |s| s.prepend("Chapter #{i + 1} ") } | ||
end | ||
work_messages << chapter_messages.flatten | ||
end | ||
end | ||
rescue => exception | ||
@some_errors = true | ||
work_status = :unprocessable_entity | ||
work_messages << exception.message.to_json | ||
work_messages << work.errors if work | ||
end | ||
end | ||
|
||
{ | ||
status: work_status, | ||
url: work_url, | ||
original_url: original_url, | ||
messages: work_messages.flatten | ||
} | ||
end | ||
|
||
# Top-level error handling: returns a 403 forbidden if a valid archivist isn't supplied and a 400 | ||
# if no works are supplied. If there is neither a valid archivist nor valid works, a 400 is returned | ||
# ith both errors as a message | ||
def batch_errors(archivist, external_works) | ||
status = :bad_request | ||
errors = [] | ||
|
||
unless archivist && archivist.is_archivist? | ||
status = :forbidden | ||
errors << "The 'archivist' field must specify the name of an Archive user with archivist privileges." | ||
end | ||
|
||
if external_works.nil? || external_works.empty? | ||
errors << "No work URLs were provided." | ||
elsif external_works.size >= ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST | ||
errors << "This request contains too many works. A maximum of #{ ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST }" + | ||
"works can be imported in one go by an archivist." | ||
end | ||
status = :ok if errors.empty? | ||
[status, errors] | ||
end | ||
|
||
# Work-level error handling for requests that are incomplete or too large | ||
def work_errors(work) | ||
status = :bad_request | ||
errors = [] | ||
urls = work[:chapter_urls] | ||
if urls.nil? | ||
errors << "This work doesn't contain chapter_urls. Works can only be imported from publicly-accessible URLs." | ||
elsif urls.length >= ArchiveConfig.IMPORT_MAX_CHAPTERS | ||
errors << "This work contains too many chapter URLs. A maximum of #{ ArchiveConfig.IMPORT_MAX_CHAPTERS }" + | ||
"chapters can be imported per work." | ||
end | ||
status = :ok if errors.empty? | ||
[status, errors] | ||
end | ||
|
||
# send invitations to external authors for a given set of works | ||
def send_external_invites(works, archivist) | ||
external_authors = works.collect(&:external_authors).flatten.uniq | ||
unless external_authors.empty? | ||
external_authors.each do |external_author| | ||
external_author.find_or_invite(archivist) | ||
end | ||
end | ||
end | ||
|
||
def options(archivist, params) | ||
{ | ||
archivist: archivist, | ||
import_multiple: "chapters", | ||
importing_for_others: true, | ||
do_not_set_current_author: true, | ||
post_without_preview: params[:post_without_preview].blank? ? true : params[:post_without_preview], | ||
restricted: params[:restricted], | ||
override_tags: params[:override_tags].blank? ? true : params[:override_tags], | ||
collection_names: params[:collection_names], | ||
title: params[:title], | ||
fandom: params[:fandoms], | ||
warning: params[:warnings], | ||
character: params[:characters], | ||
rating: params[:rating], | ||
relationship: params[:relationships], | ||
category: params[:categories], | ||
freeform: params[:additional_tags], | ||
summary: params[:summary], | ||
notes: params[:notes], | ||
encoding: params[:encoding], | ||
external_author_name: params[:external_author_name], | ||
external_author_email: params[:external_author_email], | ||
external_coauthor_name: params[:external_coauthor_name], | ||
external_coauthor_email: params[:external_coauthor_email] | ||
} | ||
end | ||
end |
Oops, something went wrong.