Add JSON exporter for user profile download
This commit is contained in:
parent
e25a48cc1c
commit
e174514d69
18 changed files with 152 additions and 175 deletions
|
|
@ -124,6 +124,8 @@ This is disabled by default since it requires the installation of additional pac
|
|||
* 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)
|
||||
|
||||
|
||||
# 0.4.1.2
|
||||
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -162,6 +162,8 @@ gem 'zip-zip'
|
|||
# https://github.com/discourse/discourse/pull/238
|
||||
gem 'minitest'
|
||||
|
||||
# Serializers
|
||||
gem 'active_model_serializers'
|
||||
|
||||
# Windows and OSX have an execjs compatible runtime built-in, Linux users should
|
||||
# install Node.js or use 'therubyracer'.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ GEM
|
|||
erubis (~> 2.7.0)
|
||||
activemodel (4.1.8)
|
||||
activesupport (= 4.1.8)
|
||||
active_model_serializers (0.9.0)
|
||||
activemodel (>= 3.2)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.1.8)
|
||||
activemodel (= 4.1.8)
|
||||
|
|
@ -606,6 +608,7 @@ DEPENDENCIES
|
|||
actionpack-action_caching
|
||||
actionpack-page_caching
|
||||
activerecord-import (= 0.6.0)
|
||||
active_model_serializers
|
||||
acts-as-taggable-on (= 3.4.2)
|
||||
acts_as_api (= 0.4.2)
|
||||
addressable (= 2.3.6)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def default_serializer_options
|
||||
{root: false}
|
||||
end
|
||||
|
||||
def ensure_http_referer_is_set
|
||||
request.env['HTTP_REFERER'] ||= '/'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -136,8 +136,11 @@ class UsersController < ApplicationController
|
|||
end
|
||||
|
||||
def export
|
||||
exporter = Diaspora::Exporter.new(Diaspora::Exporters::XML)
|
||||
send_data exporter.execute(current_user), :filename => "#{current_user.username}_diaspora_data.xml", :type => :xml
|
||||
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
|
||||
end
|
||||
|
||||
def export_photos
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class User < ActiveRecord::Base
|
|||
serialize :hidden_shareables, Hash
|
||||
|
||||
has_one :person, :foreign_key => :owner_id
|
||||
has_one :profile, through: :person
|
||||
|
||||
delegate :guid, :public_key, :posts, :photos, :owns?, :image_url,
|
||||
:diaspora_handle, :name, :public_url, :profile, :url,
|
||||
:first_name, :last_name, :gender, :participations, to: :person
|
||||
|
|
|
|||
7
app/serializers/export/aspect_serializer.rb
Normal file
7
app/serializers/export/aspect_serializer.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module Export
|
||||
class AspectSerializer < ActiveModel::Serializer
|
||||
attributes :name,
|
||||
:contacts_visible,
|
||||
:chat_enabled
|
||||
end
|
||||
end
|
||||
12
app/serializers/export/contact_serializer.rb
Normal file
12
app/serializers/export/contact_serializer.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module Export
|
||||
class ContactSerializer < ActiveModel::Serializer
|
||||
attributes :sharing,
|
||||
:receiving,
|
||||
:person_guid,
|
||||
:person_name,
|
||||
:person_first_name,
|
||||
:person_diaspora_handle
|
||||
|
||||
has_many :aspects, each_serializer: Export::AspectSerializer
|
||||
end
|
||||
end
|
||||
14
app/serializers/export/profile_serializer.rb
Normal file
14
app/serializers/export/profile_serializer.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module Export
|
||||
class ProfileSerializer < ActiveModel::Serializer
|
||||
attributes :first_name,
|
||||
:last_name,
|
||||
:gender,
|
||||
:bio,
|
||||
:birthday,
|
||||
:location,
|
||||
:image_url,
|
||||
:diaspora_handle,
|
||||
:searchable,
|
||||
:nsfw
|
||||
end
|
||||
end
|
||||
16
app/serializers/export/user_serializer.rb
Normal file
16
app/serializers/export/user_serializer.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module Export
|
||||
class UserSerializer < ActiveModel::Serializer
|
||||
attributes :name,
|
||||
:email,
|
||||
:language,
|
||||
:username,
|
||||
:disable_mail,
|
||||
:show_community_spotlight_in_stream,
|
||||
:auto_follow_back,
|
||||
:auto_follow_back_aspect
|
||||
has_one :profile, serializer: Export::ProfileSerializer
|
||||
has_many :aspects, each_serializer: Export::AspectSerializer
|
||||
has_many :contacts, each_serializer: Export::ContactSerializer
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -180,7 +180,8 @@
|
|||
#account_data.span6
|
||||
%h3
|
||||
= t('.export_data')
|
||||
= link_to t('.download_xml'), export_user_path, :class => "button"
|
||||
.small-horizontal-spacer
|
||||
= link_to t('.download_profile'), export_user_path(format: :json), :class => "button"
|
||||
.small-horizontal-spacer
|
||||
= link_to t('.download_photos'), "#", :class => "button", :id => "photo-export-button", :title => t('.photo_export_unavailable')
|
||||
|
||||
|
|
|
|||
|
|
@ -1266,7 +1266,7 @@ en:
|
|||
current_password: "Current password"
|
||||
current_password_expl: "the one you sign in with..."
|
||||
character_minimum_expl: "must be at least six characters"
|
||||
download_xml: "download my xml"
|
||||
download_profile: "download my profile"
|
||||
download_photos: "download my photos"
|
||||
your_handle: "Your diaspora* ID"
|
||||
your_email: "Your email"
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Diaspora::Application.routes.draw do
|
|||
|
||||
resource :user, :only => [:edit, :update, :destroy], :shallow => true do
|
||||
get :getting_started_completed
|
||||
get :export
|
||||
get :export, format: :json
|
||||
get :export_photos
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class AccountDeleter
|
|||
end
|
||||
|
||||
def special_ar_user_associations
|
||||
[:invitations_from_me, :person, :contacts, :auto_follow_back_aspect]
|
||||
[:invitations_from_me, :person, :profile, :contacts, :auto_follow_back_aspect]
|
||||
end
|
||||
|
||||
def ignored_ar_user_associations
|
||||
|
|
|
|||
|
|
@ -5,87 +5,23 @@
|
|||
module Diaspora
|
||||
|
||||
class Exporter
|
||||
def initialize(strategy)
|
||||
self.class.send(:include, strategy)
|
||||
|
||||
SERIALIZED_VERSION = '1.0'
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
end
|
||||
|
||||
module Exporters
|
||||
module XML
|
||||
def execute(user)
|
||||
builder = Nokogiri::XML::Builder.new do |xml|
|
||||
user_person_id = user.person_id
|
||||
xml.export {
|
||||
xml.user {
|
||||
xml.username user.username
|
||||
xml.serialized_private_key user.serialized_private_key
|
||||
|
||||
xml.parent << user.person.to_xml
|
||||
}
|
||||
|
||||
|
||||
|
||||
xml.aspects {
|
||||
user.aspects.each do |aspect|
|
||||
xml.aspect {
|
||||
xml.name aspect.name
|
||||
|
||||
# xml.person_ids {
|
||||
#aspect.person_ids.each do |id|
|
||||
#xml.person_id id
|
||||
#end
|
||||
#}
|
||||
|
||||
xml.post_ids {
|
||||
aspect.posts.where(author_id: user_person_id).each do |post|
|
||||
xml.post_id post.id
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
xml.contacts {
|
||||
user.contacts.each do |contact|
|
||||
xml.contact {
|
||||
xml.user_id contact.user_id
|
||||
xml.person_id contact.person_id
|
||||
xml.person_guid contact.person_guid
|
||||
|
||||
xml.aspects {
|
||||
contact.aspects.each do |aspect|
|
||||
xml.aspect {
|
||||
xml.name aspect.name
|
||||
}
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
xml.posts {
|
||||
user.visible_shareables(Post).where(author_id: user_person_id).each do |post|
|
||||
#post.comments.each do |comment|
|
||||
# post_doc << comment.to_xml
|
||||
#end
|
||||
|
||||
xml.parent << post.to_xml
|
||||
end
|
||||
}
|
||||
|
||||
xml.people {
|
||||
user.contacts.each do |contact|
|
||||
person = contact.person
|
||||
xml.parent << person.to_xml
|
||||
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
builder.to_xml.to_s
|
||||
end
|
||||
def execute
|
||||
@export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialized_user
|
||||
@serialized_user ||= Export::UserSerializer.new(@user).as_json
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ describe UsersController, :type => :controller do
|
|||
end
|
||||
|
||||
describe '#export' do
|
||||
it 'returns an xml file' do
|
||||
get :export
|
||||
expect(response.header["Content-Type"]).to include "application/xml"
|
||||
it 'can return a json file' do
|
||||
get :export, format: :json
|
||||
expect(response.header["Content-Type"]).to include "application/json"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,21 @@ describe AccountDeleter do
|
|||
end
|
||||
end
|
||||
|
||||
context "profile deletion" do
|
||||
before do
|
||||
@profile_deletion = AccountDeleter.new(remote_raphael.diaspora_handle)
|
||||
@profile = remote_raphael.profile
|
||||
end
|
||||
|
||||
it "nulls out fields in the profile" do
|
||||
@profile_deletion.perform!
|
||||
expect(@profile.reload.first_name).to be_blank
|
||||
expect(@profile.last_name).to be_blank
|
||||
expect(@profile.searchable).to be_falsey
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "person deletion" do
|
||||
before do
|
||||
@person_deletion = AccountDeleter.new(remote_raphael.diaspora_handle)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ describe Diaspora::Exporter do
|
|||
|
||||
before do
|
||||
@user1 = alice
|
||||
@user2 = FactoryGirl.create(:user)
|
||||
@user3 = bob
|
||||
|
||||
@user1.person.profile.first_name = "<script>"
|
||||
@user1.person.profile.gender = "<script>"
|
||||
|
|
@ -19,108 +17,70 @@ describe Diaspora::Exporter do
|
|||
@user1.person.profile.save
|
||||
|
||||
@aspect = @user1.aspects.first
|
||||
@aspect1 = @user1.aspects.create(:name => "Work")
|
||||
@aspect2 = @user2.aspects.create(:name => "Family")
|
||||
@aspect3 = @user3.aspects.first
|
||||
@aspect1 = @user1.aspects.create(:name => "Work", :contacts_visible => false)
|
||||
@aspect.name = "<script>"
|
||||
@aspect.save
|
||||
|
||||
@status_message1 = @user1.post(:status_message, :text => "One", :public => true, :to => @aspect1.id)
|
||||
@status_message2 = @user1.post(:status_message, :text => "Two", :public => true, :to => @aspect1.id)
|
||||
@status_message3 = @user2.post(:status_message, :text => "Three", :public => false, :to => @aspect2.id)
|
||||
@status_message4 = @user1.post(:status_message, :text => "<script>", :public => true, :to => @aspect2.id)
|
||||
end
|
||||
|
||||
def exported
|
||||
Nokogiri::XML(Diaspora::Exporter.new(Diaspora::Exporters::XML).execute(@user1))
|
||||
end
|
||||
context "json" do
|
||||
|
||||
it 'escapes xml relevant characters' do
|
||||
expect(exported.to_s).to_not include "<script>"
|
||||
end
|
||||
|
||||
context '<user/>' do
|
||||
let(:user_xml) { exported.xpath('//user').to_s }
|
||||
|
||||
it 'includes a users private key' do
|
||||
expect(user_xml).to include @user1.serialized_private_key
|
||||
def json
|
||||
@json ||= JSON.parse Diaspora::Exporter.new(@user1).execute
|
||||
end
|
||||
|
||||
it 'includes the profile as xml' do
|
||||
expect(user_xml).to include "<profile>"
|
||||
end
|
||||
end
|
||||
it { matches :version, to: '1.0' }
|
||||
it { matches :user, :name }
|
||||
it { matches :user, :email }
|
||||
it { matches :user, :username }
|
||||
it { matches :user, :language }
|
||||
it { matches :user, :disable_mail }
|
||||
it { matches :user, :show_community_spotlight_in_stream }
|
||||
it { matches :user, :auto_follow_back }
|
||||
it { matches :user, :auto_follow_back_aspect }
|
||||
|
||||
context '<aspects/>' do
|
||||
let(:aspects_xml) { exported.xpath('//aspects').to_s }
|
||||
it { matches :user, :profile, :first_name, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :last_name, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :gender, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :bio, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :location, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :image_url, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :diaspora_handle, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :searchable, root: @user1.person.profile }
|
||||
it { matches :user, :profile, :nsfw, root: @user1.person.profile }
|
||||
|
||||
it 'includes the post_ids' do
|
||||
expect(aspects_xml).to include @status_message1.id.to_s
|
||||
expect(aspects_xml).to include @status_message2.id.to_s
|
||||
end
|
||||
end
|
||||
it { matches_relation :aspects, :name,
|
||||
:contacts_visible,
|
||||
:chat_enabled }
|
||||
|
||||
context '<contacts/>' do
|
||||
it { matches_relation :contacts, :sharing,
|
||||
:receiving,
|
||||
:person_guid,
|
||||
:person_name,
|
||||
:person_first_name,
|
||||
:person_diaspora_handle }
|
||||
|
||||
before do
|
||||
@aspect.name = "Safe"
|
||||
@aspect.save
|
||||
@user1.add_contact_to_aspect(@user1.contact_for(@user3.person), @aspect1)
|
||||
@user1.reload
|
||||
private
|
||||
|
||||
def matches(*fields, to: nil, root: @user1)
|
||||
expected = to || root.send(fields.last)
|
||||
expect(recurse_field(json, fields)).to eq expected
|
||||
end
|
||||
|
||||
let(:contacts_xml) {exported.xpath('//contacts').to_s}
|
||||
it "includes a person's guid" do
|
||||
expect(contacts_xml).to include @user3.person.guid
|
||||
def matches_relation(relation, *fields, to: nil, root: @user1)
|
||||
array = json['user'][to || relation.to_s]
|
||||
fields.each do |field|
|
||||
expected = root.send(relation).map(&:"#{field}")
|
||||
expect(array.map { |f| f[field.to_s] }).to eq expected
|
||||
end
|
||||
end
|
||||
|
||||
it "includes the names of all aspects they are in" do
|
||||
#contact specific xml needs to be tested
|
||||
expect(@user1.contacts.find_by_person_id(@user3.person.id).aspects.count).to be > 0
|
||||
@user1.contacts.find_by_person_id(@user3.person.id).aspects.each { |aspect|
|
||||
expect(contacts_xml).to include aspect.name
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context '<people/>' do
|
||||
let(:people_xml) {exported.xpath('//people').to_s}
|
||||
|
||||
it 'includes their guid' do
|
||||
expect(people_xml).to include @user3.person.guid
|
||||
def recurse_field(json, fields)
|
||||
if fields.any?
|
||||
recurse_field json[fields.shift.to_s], fields
|
||||
else
|
||||
json
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes their profile' do
|
||||
expect(people_xml).to include @user3.person.profile.first_name
|
||||
expect(people_xml).to include @user3.person.profile.last_name
|
||||
end
|
||||
|
||||
it 'includes their public key' do
|
||||
expect(people_xml).to include @user3.person.exported_key
|
||||
end
|
||||
|
||||
it 'includes their diaspora handle' do
|
||||
expect(people_xml).to include @user3.person.diaspora_handle
|
||||
end
|
||||
end
|
||||
|
||||
context '<posts>' do
|
||||
let(:posts_xml) {exported.xpath('//posts').to_s}
|
||||
it "includes many posts' xml" do
|
||||
expect(posts_xml).to include @status_message1.text
|
||||
expect(posts_xml).to include @status_message2.text
|
||||
expect(posts_xml).not_to include @status_message3.text
|
||||
end
|
||||
|
||||
it "includes the post's created at time" do
|
||||
@status_message1.update_attribute(:created_at, Time.now - 1.day) # make sure they have different created at times
|
||||
|
||||
doc = Nokogiri::XML::parse(posts_xml)
|
||||
created_at_text = doc.xpath('//posts/status_message').detect do |status|
|
||||
status.to_s.include?(@status_message1.guid)
|
||||
end.xpath('created_at').text
|
||||
|
||||
expect(Time.zone.parse(created_at_text).to_i).to eq(@status_message1.created_at.to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue