Merge pull request #5354 from gdpelican/feature/json-exporter
Feature/json exporter
This commit is contained in:
commit
6806b2daf8
18 changed files with 152 additions and 175 deletions
|
|
@ -125,6 +125,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)
|
* 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)
|
* 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)
|
* 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
|
# 0.4.1.2
|
||||||
|
|
||||||
|
|
|
||||||
2
Gemfile
2
Gemfile
|
|
@ -162,6 +162,8 @@ gem 'zip-zip'
|
||||||
# https://github.com/discourse/discourse/pull/238
|
# https://github.com/discourse/discourse/pull/238
|
||||||
gem 'minitest'
|
gem 'minitest'
|
||||||
|
|
||||||
|
# Serializers
|
||||||
|
gem 'active_model_serializers'
|
||||||
|
|
||||||
# Windows and OSX have an execjs compatible runtime built-in, Linux users should
|
# Windows and OSX have an execjs compatible runtime built-in, Linux users should
|
||||||
# install Node.js or use 'therubyracer'.
|
# install Node.js or use 'therubyracer'.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ GEM
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
activemodel (4.1.8)
|
activemodel (4.1.8)
|
||||||
activesupport (= 4.1.8)
|
activesupport (= 4.1.8)
|
||||||
|
active_model_serializers (0.9.0)
|
||||||
|
activemodel (>= 3.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
activerecord (4.1.8)
|
activerecord (4.1.8)
|
||||||
activemodel (= 4.1.8)
|
activemodel (= 4.1.8)
|
||||||
|
|
@ -606,6 +608,7 @@ DEPENDENCIES
|
||||||
actionpack-action_caching
|
actionpack-action_caching
|
||||||
actionpack-page_caching
|
actionpack-page_caching
|
||||||
activerecord-import (= 0.6.0)
|
activerecord-import (= 0.6.0)
|
||||||
|
active_model_serializers
|
||||||
acts-as-taggable-on (= 3.4.2)
|
acts-as-taggable-on (= 3.4.2)
|
||||||
acts_as_api (= 0.4.2)
|
acts_as_api (= 0.4.2)
|
||||||
addressable (= 2.3.6)
|
addressable (= 2.3.6)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def default_serializer_options
|
||||||
|
{root: false}
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_http_referer_is_set
|
def ensure_http_referer_is_set
|
||||||
request.env['HTTP_REFERER'] ||= '/'
|
request.env['HTTP_REFERER'] ||= '/'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,11 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def export
|
def export
|
||||||
exporter = Diaspora::Exporter.new(Diaspora::Exporters::XML)
|
if export = Diaspora::Exporter.new(current_user).execute
|
||||||
send_data exporter.execute(current_user), :filename => "#{current_user.username}_diaspora_data.xml", :type => :xml
|
send_data export, filename: "#{current_user.username}_diaspora_data.json", type: :json
|
||||||
|
else
|
||||||
|
head :not_acceptable
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def export_photos
|
def export_photos
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ class User < ActiveRecord::Base
|
||||||
serialize :hidden_shareables, Hash
|
serialize :hidden_shareables, Hash
|
||||||
|
|
||||||
has_one :person, :foreign_key => :owner_id
|
has_one :person, :foreign_key => :owner_id
|
||||||
|
has_one :profile, through: :person
|
||||||
|
|
||||||
delegate :guid, :public_key, :posts, :photos, :owns?, :image_url,
|
delegate :guid, :public_key, :posts, :photos, :owns?, :image_url,
|
||||||
:diaspora_handle, :name, :public_url, :profile, :url,
|
:diaspora_handle, :name, :public_url, :profile, :url,
|
||||||
:first_name, :last_name, :gender, :participations, to: :person
|
: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
|
#account_data.span6
|
||||||
%h3
|
%h3
|
||||||
= t('.export_data')
|
= 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
|
.small-horizontal-spacer
|
||||||
= link_to t('.download_photos'), "#", :class => "button", :id => "photo-export-button", :title => t('.photo_export_unavailable')
|
= 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: "Current password"
|
||||||
current_password_expl: "the one you sign in with..."
|
current_password_expl: "the one you sign in with..."
|
||||||
character_minimum_expl: "must be at least six characters"
|
character_minimum_expl: "must be at least six characters"
|
||||||
download_xml: "download my xml"
|
download_profile: "download my profile"
|
||||||
download_photos: "download my photos"
|
download_photos: "download my photos"
|
||||||
your_handle: "Your diaspora* ID"
|
your_handle: "Your diaspora* ID"
|
||||||
your_email: "Your email"
|
your_email: "Your email"
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ Diaspora::Application.routes.draw do
|
||||||
|
|
||||||
resource :user, :only => [:edit, :update, :destroy], :shallow => true do
|
resource :user, :only => [:edit, :update, :destroy], :shallow => true do
|
||||||
get :getting_started_completed
|
get :getting_started_completed
|
||||||
get :export
|
get :export, format: :json
|
||||||
get :export_photos
|
get :export_photos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class AccountDeleter
|
||||||
end
|
end
|
||||||
|
|
||||||
def special_ar_user_associations
|
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
|
end
|
||||||
|
|
||||||
def ignored_ar_user_associations
|
def ignored_ar_user_associations
|
||||||
|
|
|
||||||
|
|
@ -5,87 +5,23 @@
|
||||||
module Diaspora
|
module Diaspora
|
||||||
|
|
||||||
class Exporter
|
class Exporter
|
||||||
def initialize(strategy)
|
|
||||||
self.class.send(:include, strategy)
|
SERIALIZED_VERSION = '1.0'
|
||||||
|
|
||||||
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
module Exporters
|
def execute
|
||||||
module XML
|
@export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialized_user
|
||||||
|
@serialized_user ||= Export::UserSerializer.new(@user).as_json
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ describe UsersController, :type => :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#export' do
|
describe '#export' do
|
||||||
it 'returns an xml file' do
|
it 'can return a json file' do
|
||||||
get :export
|
get :export, format: :json
|
||||||
expect(response.header["Content-Type"]).to include "application/xml"
|
expect(response.header["Content-Type"]).to include "application/json"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,21 @@ describe AccountDeleter do
|
||||||
end
|
end
|
||||||
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
|
context "person deletion" do
|
||||||
before do
|
before do
|
||||||
@person_deletion = AccountDeleter.new(remote_raphael.diaspora_handle)
|
@person_deletion = AccountDeleter.new(remote_raphael.diaspora_handle)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ describe Diaspora::Exporter do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@user1 = alice
|
@user1 = alice
|
||||||
@user2 = FactoryGirl.create(:user)
|
|
||||||
@user3 = bob
|
|
||||||
|
|
||||||
@user1.person.profile.first_name = "<script>"
|
@user1.person.profile.first_name = "<script>"
|
||||||
@user1.person.profile.gender = "<script>"
|
@user1.person.profile.gender = "<script>"
|
||||||
|
|
@ -19,108 +17,70 @@ describe Diaspora::Exporter do
|
||||||
@user1.person.profile.save
|
@user1.person.profile.save
|
||||||
|
|
||||||
@aspect = @user1.aspects.first
|
@aspect = @user1.aspects.first
|
||||||
@aspect1 = @user1.aspects.create(:name => "Work")
|
@aspect1 = @user1.aspects.create(:name => "Work", :contacts_visible => false)
|
||||||
@aspect2 = @user2.aspects.create(:name => "Family")
|
|
||||||
@aspect3 = @user3.aspects.first
|
|
||||||
@aspect.name = "<script>"
|
@aspect.name = "<script>"
|
||||||
@aspect.save
|
@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
|
end
|
||||||
|
|
||||||
def exported
|
context "json" do
|
||||||
Nokogiri::XML(Diaspora::Exporter.new(Diaspora::Exporters::XML).execute(@user1))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'escapes xml relevant characters' do
|
def json
|
||||||
expect(exported.to_s).to_not include "<script>"
|
@json ||= JSON.parse Diaspora::Exporter.new(@user1).execute
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'includes the profile as xml' do
|
it { matches :version, to: '1.0' }
|
||||||
expect(user_xml).to include "<profile>"
|
it { matches :user, :name }
|
||||||
end
|
it { matches :user, :email }
|
||||||
end
|
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
|
it { matches :user, :profile, :first_name, root: @user1.person.profile }
|
||||||
let(:aspects_xml) { exported.xpath('//aspects').to_s }
|
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
|
it { matches_relation :aspects, :name,
|
||||||
expect(aspects_xml).to include @status_message1.id.to_s
|
:contacts_visible,
|
||||||
expect(aspects_xml).to include @status_message2.id.to_s
|
:chat_enabled }
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context '<contacts/>' do
|
it { matches_relation :contacts, :sharing,
|
||||||
|
:receiving,
|
||||||
|
:person_guid,
|
||||||
|
:person_name,
|
||||||
|
:person_first_name,
|
||||||
|
:person_diaspora_handle }
|
||||||
|
|
||||||
before do
|
private
|
||||||
@aspect.name = "Safe"
|
|
||||||
@aspect.save
|
def matches(*fields, to: nil, root: @user1)
|
||||||
@user1.add_contact_to_aspect(@user1.contact_for(@user3.person), @aspect1)
|
expected = to || root.send(fields.last)
|
||||||
@user1.reload
|
expect(recurse_field(json, fields)).to eq expected
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:contacts_xml) {exported.xpath('//contacts').to_s}
|
def matches_relation(relation, *fields, to: nil, root: @user1)
|
||||||
it "includes a person's guid" do
|
array = json['user'][to || relation.to_s]
|
||||||
expect(contacts_xml).to include @user3.person.guid
|
fields.each do |field|
|
||||||
|
expected = root.send(relation).map(&:"#{field}")
|
||||||
|
expect(array.map { |f| f[field.to_s] }).to eq expected
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "includes the names of all aspects they are in" do
|
def recurse_field(json, fields)
|
||||||
#contact specific xml needs to be tested
|
if fields.any?
|
||||||
expect(@user1.contacts.find_by_person_id(@user3.person.id).aspects.count).to be > 0
|
recurse_field json[fields.shift.to_s], fields
|
||||||
@user1.contacts.find_by_person_id(@user3.person.id).aspects.each { |aspect|
|
else
|
||||||
expect(contacts_xml).to include aspect.name
|
json
|
||||||
}
|
end
|
||||||
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
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue