Commit e174514d authored by James Kiesel's avatar James Kiesel
Browse files

Add JSON exporter for user profile download

parent e25a48cc
......@@ -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
......
......@@ -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
......
module Export
class AspectSerializer < ActiveModel::Serializer
attributes :name,
:contacts_visible,
:chat_enabled
end
end
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
module Export
class ProfileSerializer < ActiveModel::Serializer
attributes :first_name,
:last_name,
:gender,
:bio,
:birthday,
:location,
:image_url,
:diaspora_handle,
:searchable,
:nsfw
end
end
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
\ No newline at end of file
......@@ -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)
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
#}
SERIALIZED_VERSION = '1.0'
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
}
def initialize(user)
@user = user
end
xml.people {
user.contacts.each do |contact|
person = contact.person
xml.parent << person.to_xml
def execute
@export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION)
end
end
}
}
end
private
builder.to_xml.to_s
end
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
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 }
context "json" do
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>"
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 }
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 { matches_relation :aspects, :name,
:contacts_visible,
:chat_enabled }
it { matches_relation :contacts, :sharing,
:receiving,
:person_guid,
:person_name,
:person_first_name,
:person_diaspora_handle }
private
def matches(*fields, to: nil, root: @user1)
expected = to || root.send(fields.last)
expect(recurse_field(json, fields)).to eq expected
end
end
context '<aspects/>' do
let(:aspects_xml) { exported.xpath('//aspects').to_s }
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
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
end
context '<contacts/>' do
before do
@aspect.name = "Safe"
@aspect.save
@user1.add_contact_to_aspect(@user1.contact_for(@user3.person), @aspect1)
@user1.reload
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 recurse_field(json, fields)
if fields.any?
recurse_field json[fields.shift.to_s], fields
else
json
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
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment