diff --git a/Changelog.md b/Changelog.md index a0d4c81e453c9b0c3cbc6a0f21da850e623d95f6..e5fa600d8e520fdb0836389317734b8c1d355a2a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -137,12 +137,12 @@ diaspora.yml file**. The existing settings from 0.4.x and before will not work a * Truncate too long OpenGraph descriptions [#5387](https://github.com/diaspora/diaspora/pull/5387) * Make the source code URL configurable [#5410](https://github.com/diaspora/diaspora/pull/5410) * Prefill publisher on the tag pages [#5442](https://github.com/diaspora/diaspora/pull/5442) -* Allows users to export their data in JSON format from their user settings page [#5354](https://github.com/diaspora/diaspora/pull/5354) * Don't include the content of non-public posts into notification mails [#5494](https://github.com/diaspora/diaspora/pull/5494) * Allow to set unhosted button and currency for paypal donation [#5452](https://github.com/diaspora/diaspora/pull/5452) * Add followed tags in the mobile menu [#5468](https://github.com/diaspora/diaspora/pull/5468) * Replace Pagedown with markdown-it [#5526](https://github.com/diaspora/diaspora/pull/5526) * Do not truncate notification emails anymore [#4342](https://github.com/diaspora/diaspora/issues/4342) +* Allows users to export their data in gzipped JSON format from their user settings page [#5499](https://github.com/diaspora/diaspora/pull/5499) # 0.4.1.2 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 91ac6b3d3816bae2fd695e2d03865d9b8e2354fd..fabb3f3a43a74566b249a3e1178aecd155c484ed 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -135,12 +135,14 @@ class UsersController < ApplicationController redirect_to stream_path end - def export - if export = Diaspora::Exporter.new(current_user).execute - send_data export, filename: "#{current_user.username}_diaspora_data.json", type: :json - else - head :not_acceptable - end + def export_profile + current_user.queue_export + flash[:notice] = I18n.t('users.edit.export_in_progress') + redirect_to edit_user_path + end + + def download_profile + send_data File.open(current_user.export.path).read, type: :json, filename: current_user.export.filename end def export_photos diff --git a/app/mailers/export_mailer.rb b/app/mailers/export_mailer.rb new file mode 100644 index 0000000000000000000000000000000000000000..8559fa8b1a2af67cb6a2920758dfaf1258d177b4 --- /dev/null +++ b/app/mailers/export_mailer.rb @@ -0,0 +1,22 @@ +class ExportMailer < ActionMailer::Base + default from: AppConfig.mail.sender_address + + def export_complete_for(user) + @user = user + + mail(to: @user.email, subject: I18n.t('notifier.export_email.subject')) do |format| + format.html { render 'users/export_email' } + format.text { render 'users/export_email' } + end.deliver + end + + def export_failure_for(user) + @user = user + + mail(to: @user.email, subject: I18n.t('notifier.export_failure_email.subject')) do |format| + format.html { render 'users/export_failure_email' } + format.text { render 'users/export_failure_email' } + end.deliver + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index 0a7c2d863d72d1d28fa92f66469690e6ac1960c5..3fdbec790e96618950bf75fb488ab9681c7fbdaf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -291,6 +291,28 @@ class User < ActiveRecord::Base end end + ######### Data export ################## + mount_uploader :export, ExportedUser + + def queue_export + update exporting: true + Workers::ExportUser.perform_async(id) + end + + def perform_export! + export = Tempfile.new([username, '.json.gz'], encoding: 'ascii-8bit') + export.write(compressed_export) && export.close + if export.present? + update exporting: false, export: export, exported_at: Time.zone.now + else + update exporting: false + end + end + + def compressed_export + ActiveSupport::Gzip.compress Diaspora::Exporter.new(self).execute + end + ######### Mailer ####################### def mail(job, *args) pref = job.to_s.gsub('Workers::Mail::', '').underscore @@ -505,6 +527,6 @@ class User < ActiveRecord::Base "created_at", "updated_at", "locked_at", "serialized_private_key", "getting_started", "disable_mail", "show_community_spotlight_in_stream", - "email", "remove_after"] + "email", "remove_after", "export", "exporting", "exported_at"] end end diff --git a/app/serializers/export/comment_serializer.rb b/app/serializers/export/comment_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..72e9ccbf51f8e3bc077d358d3ce6f209b0df073c --- /dev/null +++ b/app/serializers/export/comment_serializer.rb @@ -0,0 +1,10 @@ +module Export + class CommentSerializer < ActiveModel::Serializer + attributes :text, + :post_guid + + def post_guid + object.post.guid + end + end +end diff --git a/app/serializers/export/post_serializer.rb b/app/serializers/export/post_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..3459c2a51d630f41e9da839f91aeecb79818fec1 --- /dev/null +++ b/app/serializers/export/post_serializer.rb @@ -0,0 +1,14 @@ +module Export + class PostSerializer < ActiveModel::Serializer + attributes :text, + :public, + :diaspora_handle, + :type, + :image_url, + :image_height, + :image_width, + :likes_count, + :comments_count, + :reshares_count + end +end diff --git a/app/serializers/export/user_serializer.rb b/app/serializers/export/user_serializer.rb index da277dc7fa8d58f225c57e47879b31ad09da5322..9621d2095bbf6aa67ec3da08de2946d55833852b 100644 --- a/app/serializers/export/user_serializer.rb +++ b/app/serializers/export/user_serializer.rb @@ -11,6 +11,12 @@ module Export has_one :profile, serializer: Export::ProfileSerializer has_many :aspects, each_serializer: Export::AspectSerializer has_many :contacts, each_serializer: Export::ContactSerializer + has_many :posts, each_serializer: Export::PostSerializer + has_many :comments, each_serializer: Export::CommentSerializer + + def comments + object.person.comments + end end end \ No newline at end of file diff --git a/app/uploaders/exported_user.rb b/app/uploaders/exported_user.rb new file mode 100644 index 0000000000000000000000000000000000000000..ff323ecce7bc7877371c250b042fe5558a95c8f9 --- /dev/null +++ b/app/uploaders/exported_user.rb @@ -0,0 +1,19 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class ExportedUser < CarrierWave::Uploader::Base + + def store_dir + "uploads/users" + end + + def extension_white_list + %w(gz) + end + + def filename + "#{model.username}_diaspora_data.json.gz" + end + +end diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml index 597979811b60027458b0cdb4d6798f4535601ac4..38645adcf15dabfcb9880a37d95fa9443e9bfc56 100644 --- a/app/views/users/edit.html.haml +++ b/app/views/users/edit.html.haml @@ -180,8 +180,20 @@ #account_data.span6 %h3 = t('.export_data') - .small-horizontal-spacer - = link_to t('.download_profile'), export_user_path(format: :json), :class => "button" + - if current_user.exporting + .small-horizontal-spacer + .export-in-progress= t('.export_in_progress') + - elsif current_user.export.present? + .small-horizontal-spacer + = link_to t('.download_export'), download_profile_user_path, class: "button" + .small-horizontal-spacer + = t('.last_exported_at', timestamp: current_user.exported_at) + .small-horizontal-spacer + = link_to t('.request_export_update'), export_profile_user_path + - else + .small-horizontal-spacer + = link_to t('.request_export'), export_profile_user_path, :class => "button" + .small-horizontal-spacer = link_to t('.download_photos'), "#", :class => "button", :id => "photo-export-button", :title => t('.photo_export_unavailable') diff --git a/app/views/users/edit.mobile.haml b/app/views/users/edit.mobile.haml index d71ce824aca87034dd720cbf937af43956a24a34..c26390eb8555b924c780d45a499482930dedcf07 100644 --- a/app/views/users/edit.mobile.haml +++ b/app/views/users/edit.mobile.haml @@ -161,7 +161,19 @@ #account_data.span-5.append-2 %h4 = t('.export_data') - = link_to t('.download_xml'), export_user_path, :class => "btn" + - if current_user.exporting + .small-horizontal-spacer + .export-in-progress= t('.export_in_progress') + - elsif current_user.export.present? + .small-horizontal-spacer + = link_to t('.download_export'), download_profile_user_path, class: "button" + .small-horizontal-spacer + = t('.last_exported_at', timestamp: current_user.exported_at) + .small-horizontal-spacer + = link_to t('.request_export_update'), export_profile_user_path + - else + .small-horizontal-spacer + = link_to t('.request_export'), export_profile_user_path, :class => "button" %br %br = link_to t('.download_photos'), "#", :class => "btn", :id => "photo-export-button", :title => t('.photo_export_unavailable') diff --git a/app/views/users/export_email.markerb b/app/views/users/export_email.markerb new file mode 100644 index 0000000000000000000000000000000000000000..c6a9c156c8369d3201e648393edf2bcc358b1555 --- /dev/null +++ b/app/views/users/export_email.markerb @@ -0,0 +1 @@ +<%= t('notifier.export_email.body', url: @user.export.url, username: @user.username) %> diff --git a/app/views/users/export_failure_email.markerb b/app/views/users/export_failure_email.markerb new file mode 100644 index 0000000000000000000000000000000000000000..64dd314ad4fe97ca57c90cf425e2b1f8d2ef2bb5 --- /dev/null +++ b/app/views/users/export_failure_email.markerb @@ -0,0 +1 @@ +<%= t('notifier.export_failure_email.body', username: @user.username) %> diff --git a/app/workers/export_user.rb b/app/workers/export_user.rb new file mode 100644 index 0000000000000000000000000000000000000000..f01535ce93c24a5f0034b6d168ca72b9e4152a31 --- /dev/null +++ b/app/workers/export_user.rb @@ -0,0 +1,21 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + + +module Workers + class ExportUser < Base + sidekiq_options queue: :export_user + + def perform(user_id) + @user = User.find(user_id) + @user.perform_export! + + if @user.reload.export.present? + ExportMailer.export_complete_for(@user) + else + ExportMailer.export_failure_for(@user) + end + end + end +end diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 6ab014aaa7bee7c5bb14839b59824a750fde72e1..80f13fda3142fd5e77271cdfcb1c311579cbf3d1 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -781,6 +781,27 @@ en: The diaspora* email robot! [1]: %{url} + export_email: + subject: "Your personal data is ready for download, %{username}" + body: |- + Hello %{username} + + Your data has been processed and is ready for download by following [this link][%{url}]. + + Cheers, + + The diaspora* email robot! + export_failure_email: + subject: "We're sorry, there was an issue with your data, %{username}" + body: |- + Hello %{username} + + We''ve encountered an issue while processing your personal data for download. + Please try again! + + Cheers, + + The diaspora* email robot! accept_invite: "Accept Your diaspora* invite!" invited_you: "%{name} invited you to diaspora*" invite: @@ -1254,7 +1275,11 @@ en: current_password: "Current password" current_password_expl: "the one you sign in with..." character_minimum_expl: "must be at least six characters" - download_profile: "download my profile" + export_in_progress: 'We are currently processing your data. Please check back in a few moments.' + last_exported_at: '(Last updated at %{timestamp})' + request_export: 'request my profile data' + download_export: 'download my profile' + request_export_update: 'refresh my profile data' download_photos: "download my photos" your_handle: "Your diaspora* ID" your_email: "Your email" diff --git a/config/routes.rb b/config/routes.rb index ce1211a9755f15895f96d1a89360bec6cd9ad769..7d6e9785f056f0ace97b9e83ed4ecef1efbecdbe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,7 +101,8 @@ Diaspora::Application.routes.draw do resource :user, :only => [:edit, :update, :destroy], :shallow => true do get :getting_started_completed - get :export, format: :json + get :export_profile + get :download_profile get :export_photos end diff --git a/db/migrate/20141227120907_add_export_to_user.rb b/db/migrate/20141227120907_add_export_to_user.rb new file mode 100644 index 0000000000000000000000000000000000000000..376d5fe360f39c784b550e49530b2f3a435df6f7 --- /dev/null +++ b/db/migrate/20141227120907_add_export_to_user.rb @@ -0,0 +1,7 @@ +class AddExportToUser < ActiveRecord::Migration + def change + add_column :users, :export, :string + add_column :users, :exported_at, :datetime + add_column :users, :exporting, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index b5112c5e8843f2c8ca83f8c1192a9df1a29fbd83..d176940cdcc8471bce60416b75e85f00c2c22a60 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141216213423) do +ActiveRecord::Schema.define(version: 20141227120907) do create_table "account_deletions", force: true do |t| t.string "diaspora_handle" @@ -558,6 +558,9 @@ ActiveRecord::Schema.define(version: 20141216213423) do t.datetime "reset_password_sent_at" t.datetime "last_seen" t.datetime "remove_after" + t.string "export" + t.datetime "exported_at" + t.boolean "exporting", default: false end add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index a2bc3d511dace90100d8c771fc53be661577da45..82a3561a8711b47c1a1a2245422d15f8b8a61718 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -11,10 +11,21 @@ describe UsersController, :type => :controller do allow(@controller).to receive(:current_user).and_return(@user) end - describe '#export' do - it 'can return a json file' do - get :export, format: :json - expect(response.header["Content-Type"]).to include "application/json" + describe '#export_profile' do + it 'queues an export job' do + expect(@user).to receive :queue_export + get :export_profile + expect(request.flash[:notice]).to eql(I18n.t('users.edit.export_in_progress')) + expect(response).to redirect_to(edit_user_path) + end + end + + describe "#download_profile" do + it "downloads a user's export file" do + @user.perform_export! + get :download_profile + parsed = JSON.parse(ActiveSupport::Gzip.decompress(response.body)) + expect(parsed['user']['username']).to eq @user.username end end diff --git a/spec/mailers/export_spec.rb b/spec/mailers/export_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b699c10d5e95f06514ab9664472247d338b35c90 --- /dev/null +++ b/spec/mailers/export_spec.rb @@ -0,0 +1,37 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe ExportMailer, :type => :mailer do + describe '#export_complete_for' do + it "should deliver successfully" do + expect { ExportMailer.export_complete_for(alice) }.to_not raise_error + end + + it "should be added to the delivery queue" do + expect { ExportMailer.export_complete_for(alice) }.to change(ActionMailer::Base.deliveries, :size).by(1) + end + + it "should include correct recipient" do + ExportMailer.export_complete_for(alice) + expect(ActionMailer::Base.deliveries[0].to[0]).to include(alice.email) + end + end + + describe '#export_failure_for' do + it "should deliver successfully" do + expect { ExportMailer.export_failure_for(alice) }.to_not raise_error + end + + it "should be added to the delivery queue" do + expect { ExportMailer.export_failure_for(alice) }.to change(ActionMailer::Base.deliveries, :size).by(1) + end + + it "should include correct recipient" do + ExportMailer.export_failure_for(alice) + expect(ActionMailer::Base.deliveries[0].to[0]).to include(alice.email) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4648b31969e9d2d4c987434ef9e294a4259cabda..33be531b013406ae1a190af685d3458ba683e858 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -996,6 +996,33 @@ describe User, :type => :model do end end + describe "queue_export" do + it "queues up a job to perform the export" do + user = FactoryGirl.create :user + expect(Workers::ExportUser).to receive(:perform_async).with(user.id) + user.queue_export + expect(user.exporting).to be_truthy + end + end + + describe "perform_export!" do + it "saves a json export to the user" do + user = FactoryGirl.create :user, exporting: true + user.perform_export! + expect(user.export).to be_present + expect(user.exported_at).to be_present + expect(user.exporting).to be_falsey + expect(user.export.filename).to match /.json/ + expect(ActiveSupport::Gzip.decompress(user.export.file.read)).to include user.username + end + + it "compresses the result" do + user = FactoryGirl.create :user, exporting: true + expect(ActiveSupport::Gzip).to receive :compress + user.perform_export! + end + end + describe "sign up" do before do params = {:username => "ohai", diff --git a/spec/workers/export_user_spec.rb b/spec/workers/export_user_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f6ecfba558c703bbbcdac1e17146705f98c164f1 --- /dev/null +++ b/spec/workers/export_user_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Workers::ExportUser do + + before do + allow(User).to receive(:find).with(alice.id).and_return(alice) + end + + it 'calls export! on user with given id' do + expect(alice).to receive(:perform_export!) + Workers::ExportUser.new.perform(alice.id) + end + + it 'sends a success message when the export is successful' do + alice.stub(:export).and_return(OpenStruct.new) + expect(ExportMailer).to receive(:export_complete_for).with(alice) + Workers::ExportUser.new.perform(alice.id) + end + + it 'sends a failure message when the export fails' do + alice.stub(:export).and_return(nil) + expect(alice).to receive(:perform_export!).and_return(false) + expect(ExportMailer).to receive(:export_failure_for).with(alice) + Workers::ExportUser.new.perform(alice.id) + end +end