Export comments (#1471)

* Use name attribute

* Export comments

* Update copy

* Fix time-dependent test

* Remove reactions_count from the export

* Only select needed attributes from the DB

* Add the path of the commentable to the export

Comments are attached to something but from the export that is not easily identifiable. We add the "commentable_path" to give some context.
This commit is contained in:
rhymes 2019-01-16 18:54:45 +01:00 committed by Ben Halpern
parent f1e7f12e1d
commit a0c9576bec
8 changed files with 184 additions and 59 deletions

View file

@ -11,9 +11,8 @@ module Exporter
def export(slug: nil)
articles = user.articles
articles = articles.where(slug: slug) if slug.present?
json_articles = jsonify_articles(articles)
{ "articles.json" => json_articles }
{ "#{name}.json" => jsonify(articles) }
end
private
@ -41,7 +40,6 @@ module Exporter
published
published_at
published_from_feed
reactions_count
show_comments
slug
social_image
@ -54,13 +52,14 @@ module Exporter
]
end
def jsonify_articles(articles)
def jsonify(articles)
articles_to_jsonify = []
# load articles in batches, we don't want to hog the DB
# if a user has lots and lots of articles
articles.find_each do |article|
# load articles in batches and select only needed attributes
articles.select([:id] + allowed_attributes).find_each do |article|
articles_to_jsonify << article
end
articles_to_jsonify.to_json(only: allowed_attributes)
end
end

View file

@ -0,0 +1,49 @@
module Exporter
class Comments
attr_reader :name
attr_reader :user
def initialize(user)
@name = :comments
@user = user
end
def export(id_code: nil)
comments = user.comments
comments = comments.where(id_code: id_code) if id_code.present?
{ "#{name}.json" => jsonify(comments) }
end
private
def allowed_attributes
%i[
body_markdown
created_at
deleted
edited
edited_at
id_code
markdown_character_count
positive_reactions_count
processed_html
receive_notifications
]
end
def jsonify(comments)
comments_to_jsonify = []
# the commentable polymorphic attributes are added to the select for the "includes" to work
attributes_to_select = %i[id commentable_id commentable_type] + allowed_attributes
comments.includes(:commentable).select(attributes_to_select).find_each do |comment|
# merge final json with the path of the commentable
comments_to_jsonify << comment.as_json(only: allowed_attributes).
merge(commentable_path: comment.commentable&.path)
end
comments_to_jsonify.to_json
end
end
end

View file

@ -6,6 +6,7 @@ module Exporter
EXPORTERS = [
Articles,
Comments,
].freeze
def initialize(user)

View file

@ -86,25 +86,25 @@
</div>
<% end %>
<h2>Export posts</h2>
<h2>Export content</h2>
<% if @user.export_requested? %>
<h4 style="font-weight:400">
You have recently requested an export of your data.
You have recently requested an export of your content.
Please check your email.
</h4>
<% else %>
<h4 style="font-weight:400">
You can request an export of all your data.
Currently we only support the export of posts.
It will be emailed to your inbox.
You can request an export of all your content.
Currently we only support the export of your posts and comments.
They will be emailed to your inbox.
</h4>
<%= form_for(@user) do |f| %>
<div class="checkbox-field">
<div class="sub-field">
<%= f.check_box :export_requested %>
<%= f.label :export_requested, "Request an export of your data" %>
<%= f.label :export_requested, "Request an export of your content" %>
</div>
</div>
<div class="field">

View file

@ -14,8 +14,11 @@ RSpec.describe "Registrations", type: :request do
context "when logged in" do
it "redirects to /dashboard" do
login_as user
get "/enter"
expect(response).to redirect_to("/dashboard?signed-in-already&t=#{Time.current.to_i}")
Timecop.freeze(Time.current) do
get "/enter"
expect(response).to redirect_to("/dashboard?signed-in-already&t=#{Time.current.to_i}")
end
end
end
end

View file

@ -33,7 +33,6 @@ RSpec.describe Exporter::Articles do
published
published_at
published_from_feed
reactions_count
show_comments
slug
social_image

View file

@ -0,0 +1,114 @@
require "rails_helper"
RSpec.describe Exporter::Comments do
let(:user) { create(:user) }
let(:article) { create(:article) }
let(:podcast_episode) { create(:podcast_episode) }
let(:comment) { create(:comment, user: user, commentable: article) }
let(:podcast_episode_comment) { create(:comment, user: user, commentable: podcast_episode) }
let(:other_user) { create(:user) }
let(:other_user_comment) { create(:comment, user: other_user, commentable: article) }
def valid_instance(user)
described_class.new(user)
end
def expected_fields
%w[
body_markdown
created_at
deleted
edited
edited_at
id_code
markdown_character_count
positive_reactions_count
processed_html
receive_notifications
commentable_path
]
end
def load_comments(data)
JSON.parse(data["comments.json"])
end
describe "#initialize" do
it "accepts a user" do
exporter = valid_instance(user)
expect(exporter.user).to be(user)
end
it "names itself comments" do
exporter = valid_instance(user)
expect(exporter.name).to eq(:comments)
end
end
describe "#export" do
context "when id code is unknown" do
it "returns no comments if the id code is not found" do
exporter = valid_instance(user)
result = exporter.export(id_code: "not found")
comments = load_comments(result)
expect(comments).to be_empty
end
it "no comments if id code belongs to another user" do
exporter = valid_instance(user)
result = exporter.export(id_code: other_user_comment.id_code)
comments = load_comments(result)
expect(comments).to be_empty
end
end
context "when id code is known" do
it "returns the comment" do
exporter = valid_instance(user)
result = exporter.export(id_code: comment.id_code)
comments = load_comments(result)
expect(comments.length).to eq(1)
end
it "returns only expected fields for the comment" do
exporter = valid_instance(user)
result = exporter.export(id_code: comment.id_code)
comments = load_comments(result)
expect(comments.first.keys).to eq(expected_fields)
end
end
context "when all comments are requested" do
it "returns all the comments as json" do
exporter = valid_instance(comment.user)
result = exporter.export
comments = load_comments(result)
user.reload
expect(comments.length).to eq(user.comments.size)
end
it "returns only expected fields for the comment" do
exporter = valid_instance(comment.user)
result = exporter.export
comments = load_comments(result)
expect(comments.first.keys).to eq(expected_fields)
end
end
describe "commentable path" do
it "contains the path of the article" do
exporter = valid_instance(user)
result = exporter.export(id_code: comment.id_code)
comments = load_comments(result)
expect(comments.first["commentable_path"]).to eq(article.path)
end
it "contains the path of the podcast episode" do
exporter = valid_instance(user)
result = exporter.export(id_code: podcast_episode_comment.id_code)
comments = load_comments(result)
expect(comments.first["commentable_path"]).to eq(podcast_episode.path)
end
end
end
end

View file

@ -31,49 +31,9 @@ RSpec.describe Exporter::Service do
exports
end
def expected_fields
[
"body_html",
"body_markdown",
"cached_tag_list",
"cached_user_name",
"cached_user_username",
"canonical_url",
"comments_count",
"created_at",
"crossposted_at",
"description",
"edited_at",
"feed_source_url",
"language",
"last_comment_at",
"lat",
"long",
"main_image",
"main_image_background_hex_color",
"path",
"positive_reactions_count",
"processed_html",
"published",
"published_at",
"published_from_feed",
"reactions_count",
"show_comments",
"slug",
"social_image",
"title",
"updated_at",
"video",
"video_closed_caption_track_url",
"video_code",
"video_source_url",
"video_thumbnail_url",
]
end
describe "EXPORTERS" do
it "is a list of supported exporters" do
expect(described_class::EXPORTERS).to eq([Exporter::Articles])
expect(described_class::EXPORTERS).to eq([Exporter::Articles, Exporter::Comments])
end
end
@ -89,14 +49,14 @@ RSpec.describe Exporter::Service do
service = valid_instance(article.user)
zipped_exports = service.export
exports = extract_zipped_exports(zipped_exports)
expect(exports.keys).to eq(["articles.json"])
expect(exports.keys).to eq(["articles.json", "comments.json"])
end
it "passes configuration to an exporter" do
service = valid_instance(article.user)
zipped_exports = service.export(config: { articles: { slug: article.slug } })
exports = extract_zipped_exports(zipped_exports)
expect(exports.length).to eq(1)
expect(exports.length).to eq(described_class::EXPORTERS.size)
end
context "when emailing the user" do