FEATURE: Improving bookmarks part 2 -- Topic Bookmarking (#8954)

### UI Changes

If `SiteSetting.enable_bookmarks_with_reminders` is enabled:

* Clicking "Bookmark" on a topic will create a new Bookmark record instead of a post + user action
* Clicking "Clear Bookmarks" on a topic will delete all the new Bookmark records on a topic
* The topic bookmark buttons control the post bookmark flags correctly and vice-versa
Disabled selecting the "reminder type" for bookmarks in the UI because the backend functionality is not done yet (of sending users notifications etc.)

### Other Changes

* Added delete bookmark route (but no UI yet)
* Added a rake task to sync the old PostAction bookmarks to the new Bookmark table, which can be run as many times as we want for a site (it will not create duplicates).
This commit is contained in:
Martin Brennan 2020-02-13 16:26:02 +10:00 committed by GitHub
parent e7c4ebc6d5
commit e1e74abd4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 342 additions and 26 deletions

View File

@ -48,6 +48,7 @@ export default Controller.extend(ModalFunctionality, {
}, },
usingMobileDevice: reads("site.mobileView"), usingMobileDevice: reads("site.mobileView"),
showBookmarkReminderControls: false,
@discourseComputed() @discourseComputed()
reminderTypes: () => { reminderTypes: () => {

View File

@ -0,0 +1,24 @@
import { none } from "@ember/object/computed";
import { computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { Promise } from "rsvp";
import RestModel from "discourse/models/rest";
const Bookmark = RestModel.extend({
newBookmark: none("id"),
@computed
get url() {
return Discourse.getURL(`/bookmarks/${this.id}`);
},
destroy() {
if (this.newBookmark) return Promise.resolve();
return ajax(this.url, {
type: "DELETE"
});
}
});
export default Bookmark;

View File

@ -353,14 +353,20 @@ const Post = RestModel.extend({
this.appEvents.trigger("post-stream:refresh", { id: this.id }); this.appEvents.trigger("post-stream:refresh", { id: this.id });
}, },
afterSave: reminderAtISO => { afterSave: reminderAtISO => {
this.set("bookmark_reminder_at", reminderAtISO); this.setProperties({
"topic.bookmarked": true,
bookmark_reminder_at: reminderAtISO
});
this.appEvents.trigger("post-stream:refresh", { id: this.id }); this.appEvents.trigger("post-stream:refresh", { id: this.id });
} }
}); });
} else { } else {
this.set("bookmark_reminder_at", null); this.set("bookmark_reminder_at", null);
return Post.destroyBookmark(this.id) return Post.destroyBookmark(this.id)
.then(() => this.appEvents.trigger("page:bookmark-post-toggled", this)) .then(result => {
this.set("topic.bookmarked", result.topic_bookmarked);
this.appEvents.trigger("page:bookmark-post-toggled", this);
})
.catch(error => { .catch(error => {
this.toggleProperty("bookmarked_with_reminder"); this.toggleProperty("bookmarked_with_reminder");
throw new Error(error); throw new Error(error);

View File

@ -402,6 +402,9 @@ const Topic = RestModel.extend({
this.toggleProperty("bookmarked"); this.toggleProperty("bookmarked");
if (bookmark && firstPost) { if (bookmark && firstPost) {
firstPost.set("bookmarked", true); firstPost.set("bookmarked", true);
if (this.siteSettings.enable_bookmarks_with_reminders) {
firstPost.set("bookmarked_with_reminder", true);
}
return [firstPost.id]; return [firstPost.id];
} }
if (!bookmark && posts) { if (!bookmark && posts) {
@ -409,7 +412,14 @@ const Topic = RestModel.extend({
posts.forEach(post => { posts.forEach(post => {
if (post.get("bookmarked")) { if (post.get("bookmarked")) {
post.set("bookmarked", false); post.set("bookmarked", false);
updated.push(post.get("id")); updated.push(post.id);
}
if (
this.siteSettings.enable_bookmarks_with_reminders &&
post.get("bookmarked_with_reminder")
) {
post.set("bookmarked_with_reminder", false);
updated.push(post.id);
} }
}); });
return updated; return updated;
@ -424,7 +434,9 @@ const Topic = RestModel.extend({
const unbookmarkedPosts = []; const unbookmarkedPosts = [];
if (!bookmark && posts) { if (!bookmark && posts) {
posts.forEach( posts.forEach(
post => post.get("bookmarked") && unbookmarkedPosts.push(post) post =>
(post.get("bookmarked") || post.get("bookmarked_with_reminder")) &&
unbookmarkedPosts.push(post)
); );
} }

View File

@ -48,7 +48,7 @@ export default function() {
}); });
}); });
// filters // filters (e.g. bookmarks, posted, read, unread, latest)
Site.currentProp("filters").forEach(filter => { Site.currentProp("filters").forEach(filter => {
// legacy route // legacy route
this.route(filter + "ParentCategory", { path: "/c/:slug/l/" + filter }); this.route(filter + "ParentCategory", { path: "/c/:slug/l/" + filter });

View File

@ -1,4 +1,4 @@
{{#d-modal-body}} {{#d-modal-body id="bookmark-reminder-modal"}}
{{#conditional-loading-spinner condition=loading}} {{#conditional-loading-spinner condition=loading}}
{{#if errorMessage}} {{#if errorMessage}}
<div class="control-group"> <div class="control-group">
@ -13,9 +13,10 @@
{{i18n 'post.bookmarks.name'}} {{i18n 'post.bookmarks.name'}}
</label> </label>
{{input value=name name="name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}} {{input value=name name="name" class="bookmark-name" enter=(action "saveAndClose") placeholder=(i18n "post.bookmarks.name_placeholder")}}
</div> </div>
{{#if showBookmarkReminderControls}}
<div class="control-group"> <div class="control-group">
<label class="control-label" for="set_reminder"> <label class="control-label" for="set_reminder">
{{i18n 'post.bookmarks.set_reminder'}} {{i18n 'post.bookmarks.set_reminder'}}
@ -40,6 +41,7 @@
<div class="alert alert-info">{{{i18n "bookmarks.no_timezone" basePath=basePath }}}</div> <div class="alert alert-info">{{{i18n "bookmarks.no_timezone" basePath=basePath }}}</div>
{{/if}} {{/if}}
</div> </div>
{{/if}}
<div class="control-group"> <div class="control-group">
{{d-button label="bookmarks.save" class="btn-primary" action=(action "saveAndClose")}} {{d-button label="bookmarks.save" class="btn-primary" action=(action "saveAndClose")}}

View File

@ -13,7 +13,7 @@ class BookmarksController < ApplicationController
bookmark = Bookmark.create( bookmark = Bookmark.create(
user_id: current_user.id, user_id: current_user.id,
topic_id: params[:topic_id], topic_id: Post.select(:topic_id).find(params[:post_id]).topic_id,
post_id: params[:post_id], post_id: params[:post_id],
name: params[:name], name: params[:name],
reminder_type: Bookmark.reminder_types[params[:reminder_type].to_sym], reminder_type: Bookmark.reminder_types[params[:reminder_type].to_sym],
@ -23,4 +23,16 @@ class BookmarksController < ApplicationController
return render json: success_json if bookmark.save return render json: success_json if bookmark.save
render json: failed_json.merge(errors: bookmark.errors.full_messages), status: 400 render json: failed_json.merge(errors: bookmark.errors.full_messages), status: 400
end end
def destroy
params.require(:id)
bookmark = Bookmark.find_by(id: params[:id])
raise Discourse::NotFound if bookmark.blank?
raise Discourse::InvalidAccess.new if !guardian.can_delete?(bookmark)
bookmark.destroy
render json: success_json
end
end end

View File

@ -514,7 +514,8 @@ class PostsController < ApplicationController
existing_bookmark = Bookmark.find_by(post_id: params[:post_id], user_id: current_user.id) existing_bookmark = Bookmark.find_by(post_id: params[:post_id], user_id: current_user.id)
existing_bookmark.destroy if existing_bookmark.present? existing_bookmark.destroy if existing_bookmark.present?
render json: success_json topic_bookmarked = Bookmark.exists?(topic_id: existing_bookmark.topic_id, user_id: current_user.id)
render json: success_json.merge(topic_bookmarked: topic_bookmarked)
end end
def wiki def wiki

View File

@ -486,11 +486,15 @@ class TopicsController < ApplicationController
def remove_bookmarks def remove_bookmarks
topic = Topic.find(params[:topic_id].to_i) topic = Topic.find(params[:topic_id].to_i)
PostAction.joins(:post) if SiteSetting.enable_bookmarks_with_reminders?
.where(user_id: current_user.id) Bookmark.where(user_id: current_user.id, topic_id: topic.id).destroy_all
.where('topic_id = ?', topic.id).each do |pa| else
PostAction.joins(:post)
.where(user_id: current_user.id)
.where('topic_id = ?', topic.id).each do |pa|
PostActionDestroyer.destroy(current_user, pa.post, :bookmark) PostActionDestroyer.destroy(current_user, pa.post, :bookmark)
end
end end
render body: nil render body: nil
@ -543,8 +547,15 @@ class TopicsController < ApplicationController
topic = Topic.find(params[:topic_id].to_i) topic = Topic.find(params[:topic_id].to_i)
first_post = topic.ordered_posts.first first_post = topic.ordered_posts.first
result = PostActionCreator.create(current_user, first_post, :bookmark) if SiteSetting.enable_bookmarks_with_reminders?
return render_json_error(result) if result.failed? if Bookmark.exists?(user: current_user, post: first_post)
return render_json_error(I18n.t("bookmark.topic_already_bookmarked"), status: 403)
end
Bookmark.create(user: current_user, post: first_post, topic: topic)
else
result = PostActionCreator.create(current_user, first_post, :bookmark)
return render_json_error(result) if result.failed?
end
render body: nil render body: nil
end end

View File

@ -29,7 +29,7 @@ end
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# user_id :bigint not null # user_id :bigint not null
# topic_id :bigint # topic_id :bigint not null
# post_id :bigint not null # post_id :bigint not null
# name :string # name :string
# reminder_type :integer # reminder_type :integer

View File

@ -37,6 +37,7 @@ class Post < ActiveRecord::Base
has_many :uploads, through: :post_uploads has_many :uploads, through: :post_uploads
has_one :post_stat has_one :post_stat
has_many :bookmarks
has_one :incoming_email has_one :incoming_email

View File

@ -96,6 +96,7 @@ class Topic < ActiveRecord::Base
belongs_to :category belongs_to :category
has_many :category_users, through: :category has_many :category_users, through: :category
has_many :posts has_many :posts
has_many :bookmarks
has_many :ordered_posts, -> { order(post_number: :asc) }, class_name: "Post" has_many :ordered_posts, -> { order(post_number: :asc) }, class_name: "Post"
has_many :topic_allowed_users has_many :topic_allowed_users
has_many :topic_allowed_groups has_many :topic_allowed_groups

View File

@ -2649,6 +2649,10 @@ en:
name: "Name" name: "Name"
name_placeholder: "Name the bookmark to help jog your memory" name_placeholder: "Name the bookmark to help jog your memory"
set_reminder: "Set a reminder" set_reminder: "Set a reminder"
actions:
delete_bookmark:
name: "Delete bookmark"
description: "Removes the bookmark from your profile and stops all reminders for the bookmark"
category: category:
can: "can&hellip; " can: "can&hellip; "

View File

@ -599,7 +599,7 @@ Discourse::Application.routes.draw do
end end
end end
resources :bookmarks, only: %i[create] resources :bookmarks, only: %i[create destroy]
resources :notifications, except: :show do resources :notifications, except: :show do
collection do collection do

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class PopulateTopicIdOnBookmarks < ActiveRecord::Migration[6.0]
def up
Bookmark.where(topic_id: nil).includes(:post).find_each do |bookmark|
bookmark.update_column(:topic_id, bookmark.post.topic_id)
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class MarkBookmarksTopicIdNotNull < ActiveRecord::Migration[6.0]
def change
change_column_null :bookmarks, :topic_id, false
end
end

View File

@ -3,6 +3,7 @@
require 'guardian/category_guardian' require 'guardian/category_guardian'
require 'guardian/ensure_magic' require 'guardian/ensure_magic'
require 'guardian/post_guardian' require 'guardian/post_guardian'
require 'guardian/bookmark_guardian'
require 'guardian/topic_guardian' require 'guardian/topic_guardian'
require 'guardian/user_guardian' require 'guardian/user_guardian'
require 'guardian/post_revision_guardian' require 'guardian/post_revision_guardian'
@ -14,6 +15,7 @@ class Guardian
include EnsureMagic include EnsureMagic
include CategoryGuardian include CategoryGuardian
include PostGuardian include PostGuardian
include BookmarkGuardian
include TopicGuardian include TopicGuardian
include UserGuardian include UserGuardian
include PostRevisionGuardian include PostRevisionGuardian

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module BookmarkGuardian
def can_delete_bookmark?(bookmark)
@user == bookmark.user
end
end

60
lib/tasks/bookmarks.rake Normal file
View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
require_dependency "rake_helpers"
##
# This will create records in the new bookmarks table from PostAction
# records. The task is idempotent, it will not create additional bookmark
# records for PostActions that have already been created in the new table.
# You can provide a sync_limit for a smaller batch run.
#
desc "migrates old PostAction bookmarks to the new Bookmark model & table"
task "bookmarks:sync_to_table", [:sync_limit] => :environment do |_t, args|
sync_limit = args[:sync_limit] || 0
post_action_bookmarks = PostAction
.select('post_actions.id', 'post_actions.post_id', 'posts.topic_id', 'post_actions.user_id')
.where(post_action_type_id: PostActionType.types[:bookmark])
.joins(:post)
.where(deleted_at: nil)
.joins('LEFT JOIN bookmarks ON bookmarks.post_id = post_actions.post_id AND bookmarks.user_id = post_actions.user_id')
.where('bookmarks.id IS NULL')
# if sync_limit is provided as an argument this will cap
# the number of bookmarks that will be created in a run of
# this task (for huge bookmark count communities)
if sync_limit > 0
post_action_bookmarks = post_action_bookmarks.limit(sync_limit)
end
post_action_bookmark_count = post_action_bookmarks.count('post_actions.id')
bookmarks_to_create = []
i = 0
Bookmark.transaction do
post_action_bookmarks.find_each(batch_size: 1000) do |pab|
RakeHelpers.print_status_with_label("Creating post new bookmarks.......", i, post_action_bookmark_count)
now = Time.zone.now
bookmarks_to_create << {
topic_id: pab.topic_id,
post_id: pab.post_id,
user_id: pab.user_id,
created_at: now,
updated_at: now
}
i += 1
end
# this will ignore conflicts in the bookmarks table so
# if the user already has a post bookmarked in the new way,
# then we don't error and keep on truckin'
#
# we shouldn't have duplicates here at any rate because of
# the above LEFT JOIN but best to be safe knowing this
# won't blow up
Bookmark.insert_all(bookmarks_to_create)
end
RakeHelpers.print_status_with_label("Bookmark creation complete! ", i, post_action_bookmark_count)
puts ""
end

View File

@ -3,7 +3,7 @@
Fabricator(:bookmark) do Fabricator(:bookmark) do
user user
post { Fabricate(:post) } post { Fabricate(:post) }
topic nil topic { |attrs| attrs[:post].topic }
name "This looked interesting" name "This looked interesting"
reminder_type { Bookmark.reminder_types[:tomorrow] } reminder_type { Bookmark.reminder_types[:tomorrow] }
reminder_at { (Time.now.utc + 1.day).iso8601 } reminder_at { (Time.now.utc + 1.day).iso8601 }

View File

@ -5,6 +5,7 @@ require 'rails_helper'
describe BookmarksController do describe BookmarksController do
let(:current_user) { Fabricate(:user) } let(:current_user) { Fabricate(:user) }
let(:bookmark_post) { Fabricate(:post) } let(:bookmark_post) { Fabricate(:post) }
let(:bookmark_user) { current_user }
before do before do
sign_in(current_user) sign_in(current_user)
@ -13,7 +14,7 @@ describe BookmarksController do
describe "#create" do describe "#create" do
context "if the user already has bookmarked the post" do context "if the user already has bookmarked the post" do
before do before do
Fabricate(:bookmark, post: bookmark_post, user: current_user) Fabricate(:bookmark, post: bookmark_post, user: bookmark_user)
end end
it "returns failed JSON with a 422 error" do it "returns failed JSON with a 422 error" do
@ -44,4 +45,38 @@ describe BookmarksController do
end end
end end
end end
describe "#destroy" do
let!(:bookmark) { Fabricate(:bookmark, post: bookmark_post, user: bookmark_user) }
it "destroys the bookmark" do
delete "/bookmarks/#{bookmark.id}.json"
expect(Bookmark.find_by(id: bookmark.id)).to eq(nil)
end
context "if the bookmark has already been destroyed" do
it "returns failed JSON with a 403 error" do
bookmark.destroy!
delete "/bookmarks/#{bookmark.id}.json"
expect(response.status).to eq(404)
expect(JSON.parse(response.body)['errors'].first).to include(
I18n.t("not_found")
)
end
end
context "if the bookmark does not belong to the user" do
let(:bookmark_user) { Fabricate(:user) }
it "returns failed JSON with a 403 error" do
delete "/bookmarks/#{bookmark.id}.json"
expect(response.status).to eq(403)
expect(JSON.parse(response.body)['errors'].first).to include(
I18n.t("invalid_access")
)
end
end
end
end end

View File

@ -2327,6 +2327,64 @@ RSpec.describe TopicsController do
put "/t/#{pm.topic_id}/bookmark.json" put "/t/#{pm.topic_id}/bookmark.json"
expect(response).to be_forbidden expect(response).to be_forbidden
end end
context "when SiteSetting.enable_bookmarks_with_reminders is true" do
before do
SiteSetting.enable_bookmarks_with_reminders = true
end
it "deletes all the bookmarks for the user in the topic" do
sign_in(user)
post = create_post
Fabricate(:bookmark, post: post, topic: post.topic, user: user)
put "/t/#{post.topic_id}/remove_bookmarks.json"
expect(Bookmark.where(user: user, topic: topic).count).to eq(0)
end
end
end
describe "#bookmark" do
before do
sign_in(user)
end
it "should create a new post action for the bookmark on the first post of the topic" do
post = create_post
post2 = create_post(topic_id: post.topic_id)
put "/t/#{post.topic_id}/bookmark.json"
expect(PostAction.find_by(user_id: user.id, post_action_type: PostActionType.types[:bookmark]).post_id).to eq(post.id)
end
it "errors if the topic is already bookmarked for the user" do
post = create_post
PostActionCreator.new(user, post, PostActionType.types[:bookmark]).perform
put "/t/#{post.topic_id}/bookmark.json"
expect(response).to be_forbidden
end
context "when SiteSetting.enable_bookmarks_with_reminders is true" do
before do
SiteSetting.enable_bookmarks_with_reminders = true
end
it "should create a new bookmark on the first post of the topic" do
post = create_post
post2 = create_post(topic_id: post.topic_id)
put "/t/#{post.topic_id}/bookmark.json"
bookmarks_for_topic = Bookmark.where(topic: post.topic, user: user)
expect(bookmarks_for_topic.count).to eq(1)
expect(bookmarks_for_topic.first.post_id).to eq(post.id)
end
it "errors if the topic is already bookmarked for the user" do
post = create_post
Bookmark.create(post: post, topic: post.topic, user: user)
put "/t/#{post.topic_id}/bookmark.json"
expect(response).to be_forbidden
end
end
end end
describe '#reset_new' do describe '#reset_new' do

View File

@ -6,11 +6,10 @@ describe TopicListItemSerializer do
let(:topic) do let(:topic) do
date = Time.zone.now date = Time.zone.now
Fabricate.build(:topic, Fabricate(:topic,
title: 'test', title: 'This is a test topic title',
created_at: date - 2.minutes, created_at: date - 2.minutes,
bumped_at: date, bumped_at: date
posters: [],
) )
end end
@ -18,7 +17,7 @@ describe TopicListItemSerializer do
SiteSetting.topic_featured_link_enabled = true SiteSetting.topic_featured_link_enabled = true
serialized = TopicListItemSerializer.new(topic, scope: Guardian.new, root: false).as_json serialized = TopicListItemSerializer.new(topic, scope: Guardian.new, root: false).as_json
expect(serialized[:title]).to eq("test") expect(serialized[:title]).to eq("This is a test topic title")
expect(serialized[:bumped]).to eq(true) expect(serialized[:bumped]).to eq(true)
expect(serialized[:featured_link]).to eq(nil) expect(serialized[:featured_link]).to eq(nil)
expect(serialized[:featured_link_root_domain]).to eq(nil) expect(serialized[:featured_link_root_domain]).to eq(nil)

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe "bookmarks tasks" do
let(:user1) { Fabricate(:user) }
let(:user2) { Fabricate(:user) }
let(:user3) { Fabricate(:user) }
let(:post1) { Fabricate(:post) }
let(:post2) { Fabricate(:post) }
let(:post3) { Fabricate(:post) }
before do
Rake::Task.clear
Discourse::Application.load_tasks
create_post_actions_and_existing_bookmarks
end
it "migrates all PostActions" do
Rake::Task['bookmarks:sync_to_table'].invoke
expect(Bookmark.all.count).to eq(3)
end
it "does not create bookmarks that already exist in the bookmarks table for a user" do
Fabricate(:bookmark, user: user1, post: post1)
Rake::Task['bookmarks:sync_to_table'].invoke
expect(Bookmark.all.count).to eq(3)
expect(Bookmark.where(post: post1, user: user1).count).to eq(1)
end
it "respects the sync_limit if provided and stops creating bookmarks at the limit (so this can be run progrssively" do
Rake::Task['bookmarks:sync_to_table'].invoke(1)
expect(Bookmark.all.count).to eq(1)
end
def create_post_actions_and_existing_bookmarks
Fabricate(:post_action, user: user1, post: post1, post_action_type_id: PostActionType.types[:bookmark])
Fabricate(:post_action, user: user2, post: post2, post_action_type_id: PostActionType.types[:bookmark])
Fabricate(:post_action, user: user3, post: post3, post_action_type_id: PostActionType.types[:bookmark])
end
end

View File

@ -333,3 +333,17 @@ QUnit.test("Quoting a quote keeps the original poster name", async assert => {
.indexOf('quote="codinghorror said, post:3, topic:280"') !== -1 .indexOf('quote="codinghorror said, post:3, topic:280"') !== -1
); );
}); });
acceptance("Topic + Post Bookmarks with Reminders", {
loggedIn: true,
settings: {
enable_bookmarks_with_reminders: true
}
});
QUnit.test("Bookmarks Modal", async assert => {
await visit("/t/internationalization-localization/280");
await click(".topic-post:first-child button.show-more-actions");
await click(".topic-post:first-child button.bookmark");
assert.ok(exists("#bookmark-reminder-modal"), "it shows the bookmark modal");
});

View File

@ -12,8 +12,8 @@ Discourse.SiteSettingsOriginal = {
ga_universal_tracking_code: "", ga_universal_tracking_code: "",
ga_universal_domain_name: "auto", ga_universal_domain_name: "auto",
top_menu: "latest|new|unread|categories|top", top_menu: "latest|new|unread|categories|top",
post_menu: "like|share|flag|edit|bookmark|delete|admin|reply", post_menu: "like|share|flag|edit|bookmark|bookmarkWithReminder|delete|admin|reply",
post_menu_hidden_items: "flag|edit|delete|admin", post_menu_hidden_items: "flag|bookmark|bookmarkWithReminder|edit|delete|admin",
share_links: "twitter|facebook|email", share_links: "twitter|facebook|email",
category_colors: category_colors:
"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|27AA5B|B3B5B4|E45735", "BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|27AA5B|B3B5B4|E45735",
@ -28,6 +28,7 @@ Discourse.SiteSettingsOriginal = {
allow_new_registrations: true, allow_new_registrations: true,
enable_google_logins: true, enable_google_logins: true,
enable_google_oauth2_logins: false, enable_google_oauth2_logins: false,
enable_bookmarks_with_reminders: false,
enable_twitter_logins: true, enable_twitter_logins: true,
enable_facebook_logins: true, enable_facebook_logins: true,
enable_github_logins: true, enable_github_logins: true,