[deploy] Add subscriptions to the dashboard (#9161)

* Add /dashboard/subscriptions route

* Dashboard controller updates

* Update ArticlePolicy with subscriptions?

* Update article row view in dashboard

* Add subscriptions view

* Add specs

* Add comment for pagination limit

* Use cached user_subscriptions count

* Add spec for cache counter

* Add path helper
This commit is contained in:
Alex 2020-07-08 08:31:03 -04:00 committed by GitHub
parent 7bb194b305
commit 9f9236164c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 8 deletions

View file

@ -2,6 +2,7 @@ class DashboardsController < ApplicationController
before_action :set_no_cache_header
before_action :authenticate_user!
before_action :fetch_and_authorize_user, except: :pro
before_action :set_source, only: %i[subscriptions]
before_action -> { limit_per_page(default: 80, max: 1000) }, except: %i[show pro]
after_action :verify_authorized
@ -70,8 +71,22 @@ class DashboardsController < ApplicationController
@organizations = current_user.member_organizations
end
def subscriptions
authorize @source
@subscriptions = @source.user_subscriptions.
includes(:subscriber).order(created_at: :desc).page(params[:page]).per(100)
end
private
def set_source
source_type = params[:source_type]
not_found unless UserSubscription::ALLOWED_TYPES.include? source_type
source = source_type.constantize.find_by(id: params[:source_id])
@source = source || not_found
end
def fetch_and_authorize_user
@user = if params[:username] && current_user.any_admin?
User.find_by(username: params[:username])

View file

@ -5,6 +5,7 @@ class UserSubscription < ApplicationRecord
ALLOWED_TYPES = %w[Article].freeze
counter_culture :subscriber, column_name: "subscribed_to_user_subscriptions_count"
counter_culture :user_subscription_sourceable, column_name: "user_subscriptions_count"
belongs_to :author, class_name: "User", inverse_of: :source_authored_user_subscriptions
belongs_to :subscriber, class_name: "User", inverse_of: :subscribed_to_user_subscriptions

View file

@ -34,6 +34,10 @@ class ArticlePolicy < ApplicationPolicy
archived]
end
def subscriptions?
user_is_author? || user_admin?
end
private
def user_is_author?

View file

@ -58,11 +58,11 @@
<% end %>
</span>
<% if false %>
<span class="flex items-center p-1 ml-1" title="Data">
<% if article.user_subscriptions_count > 0 %>
<a href="<%= dashboard_subscriptions_path(source_type: "Article", source_id: article.id) %>" class="flex items-center crayons-btn crayons-btn--ghost crayons-btn--s fw-normal color-base-70" title="Subscriptions">
<%= inline_svg_tag("small-data.svg", aria: true, class: "crayons-icon mr-1", title: "Data") %>
4
</span>
<%= article.user_subscriptions_count %>
</a>
<% end %>
</div>
<% end %>
@ -94,9 +94,12 @@
<%= inline_svg_tag("overflow-horizontal.svg", aria: true, class: "crayons-icon", title: "More...") %>
</button>
<div class="crayons-dropdown top-100 right-0 z-10 align-left js-dashboard-row-more-dropdown">
<div class="crayons-dropdown top-100 right-0 z-10 align-left js-dashboard-row-more-dropdown p-1">
<% if article.user_subscriptions_count > 0 %>
<a href="<%= dashboard_subscriptions_path(source_type: "Article", source_id: article.id) %>" class="crayons-link crayons-link--block w-100"><%= "Subscriptions (#{article.user_subscriptions_count})" %></a>
<% end %>
<% if @current_user_pro %>
<a href="<%= article.path %>/stats" class="crayons-link crayons-link--block w-100 border-0 bg-transparent">Stats</a>
<a href="<%= article.path %>/stats" class="crayons-link crayons-link--block w-100">Stats</a>
<% end %>
<%= form_for(article, method: :patch, remote: true, authenticity_token: true, html: { "data-type": "json", class: "js-archive-toggle" }) do |f| %>
<%= f.hidden_field :archived, value: !article.archived, id: "article_archived_#{article.id}" %>

View file

@ -0,0 +1,39 @@
<% title "Subscriptions #{@source.title}" %>
<div class="crayons-layout crayons-layout--1-col">
<header class="flex items-center justify-between p-2 m:p-0">
<div>
<h1 class="fs-2xl s:fs-3xl">Subscriptions</h1>
<h2 class="fs-base s:fs-xl fw-medium">
<em class="fs-italic fw-normal color-base-60">for</em>
<a href=<%= @source.path %>><%= @source.title %></a>
</h2>
</div>
<%#TODO: [@thepracticaldev/delightful]: uncomment this when ready to implement CSV exports%>
<%#<button type="button" class="crayons-btn crayons-btn--secondary">Export CSV</button>%>
</header>
<main>
<% if @subscriptions.any? %>
<table class="crayons-table w-100">
<tbody class="crayons-card">
<% @subscriptions.each do |subscription| %>
<% subscriber = subscription.subscriber %>
<tr>
<td class="fw-medium"><a href=<%= user_profile_path(subscriber.username) %> <%= "#{subscriber.name} DEV profile" %>><%= subscriber.name %></a></td>
<td><a href="<%= "mailto: #{subscription.subscriber_email}" %>"><%= subscription.subscriber_email %></a></td>
<td class="fs-s color-base-60"><%= "#{time_ago_in_words(subscription.created_at)} ago" %></td>
</tr>
<% end %>
</tbody>
</table>
<div class="my-4 pl-3">
<%= paginate @subscriptions, params: { i: nil } %>
</div>
<% else %>
<div class="p-9 crayons-card crayons-card--secondary align-center fs-l h-100 flex items-center justify-center">
You don't have any subscribers for this <%= @source.class.name.downcase %> yet...
</div>
<% end %>
</main>
</div>

View file

@ -392,6 +392,7 @@ Rails.application.routes.draw do
get "dashboard/following_users" => "dashboards#following_users"
get "dashboard/following_organizations" => "dashboards#following_organizations"
get "dashboard/following_podcasts" => "dashboards#following_podcasts"
get "/dashboard/subscriptions" => "dashboards#subscriptions"
get "/dashboard/:which" => "dashboards#followers", :constraints => { which: /user_followers/ }
get "/dashboard/:which/:org_id" => "dashboards#show",
:constraints => {
@ -469,7 +470,7 @@ Rails.application.routes.draw do
get "/:username/:slug" => "stories#show"
get "/:sitemap" => "sitemaps#show",
:constraints => { format: /xml/, sitemap: /sitemap-.+/ }
get "/:username" => "stories#index"
get "/:username" => "stories#index", :as => "user_profile"
root "stories#index"
end

View file

@ -0,0 +1,9 @@
class AddUserSubscriptionsCountToArticles < ActiveRecord::Migration[6.0]
def self.up
add_column :articles, :user_subscriptions_count, :integer, null: false, default: 0
end
def self.down
remove_column :articles, :user_subscriptions_count
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_07_02_143618) do
ActiveRecord::Schema.define(version: 2020_07_06_184804) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -136,6 +136,7 @@ ActiveRecord::Schema.define(version: 2020_07_02_143618) do
t.string "title"
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "user_subscriptions_count", default: 0, null: false
t.string "video"
t.string "video_closed_caption_track_url"
t.string "video_code"

View file

@ -39,4 +39,23 @@ RSpec.shared_examples "UserSubscriptionSourceable" do
end
end
end
describe "counter_culture" do
context "when a UserSubscription is created" do
it "increments user_subscriptions_count" do
expect do
create(:user_subscription, user_subscription_sourceable: source)
end.to change { source.reload.user_subscriptions_count }.by(1)
end
end
context "when a UserSubscription is destroyed" do
it "decrements user_subscriptions_count" do
user_subscription = create(:user_subscription, user_subscription_sourceable: source)
expect do
user_subscription.destroy
end.to change { source.reload.user_subscriptions_count }.by(-1)
end
end
end
end

View file

@ -39,6 +39,19 @@ RSpec.describe "Dashboards", type: :request do
expect(response.body).to include "Delete"
end
it "renders subscriptions for articles with subscriptions" do
user.add_role(:admin) # TODO: (Alex Smith) - update roles before release
article_with_user_subscription_tag = create(:article, user: user, with_user_subscription_tag: true)
create(:user_subscription,
subscriber_id: second_user.id,
subscriber_email: second_user.email,
author_id: article_with_user_subscription_tag.user_id,
user_subscription_sourceable: article_with_user_subscription_tag)
get "/dashboard"
expect(response.body).to include "Subscriptions"
end
it "renders pagination if minimum amount of posts" do
create_list(:article, 52, user: user)
get "/dashboard"
@ -300,4 +313,77 @@ RSpec.describe "Dashboards", type: :request do
end
end
end
# TODO: (Alex Smith) - update roles before release
describe "GET /dashboard/subscriptions" do
before do
sign_in user
end
it "renders subscriptions" do
user.add_role(:admin)
article_with_user_subscription_tag = create(:article, user: user, with_user_subscription_tag: true)
user_subscription = create(:user_subscription,
subscriber_id: second_user.id,
subscriber_email: second_user.email,
author_id: article_with_user_subscription_tag.user_id,
user_subscription_sourceable: article_with_user_subscription_tag)
get "/dashboard/subscriptions", params: { source_type: article_with_user_subscription_tag.class.name, source_id: article_with_user_subscription_tag.id }
expect(response.body).to include(user_subscription.subscriber_email)
end
it "displays a message if no subscriptions are found" do
get "/dashboard/subscriptions", params: { source_type: article.class.name, source_id: article.id }
expect(response.body).to include("You don't have any subscribers for this")
end
it "raises unauthorized when trying to access a source the user doesn't own" do
user.add_role(:admin)
article_with_user_subscription_tag = create(:article, :with_user_subscription_tag_role_user, with_user_subscription_tag: true)
create(:user_subscription,
subscriber_id: second_user.id,
subscriber_email: second_user.email,
author_id: article_with_user_subscription_tag.user_id,
user_subscription_sourceable: article_with_user_subscription_tag)
expect do
get "/dashboard/subscriptions", params: { source_type: article_with_user_subscription_tag.class.name, source_id: article_with_user_subscription_tag.id }
end.to raise_error(Pundit::NotAuthorizedError)
end
it "raises an error for disallowed source_types" do
expect do
get "/dashboard/subscriptions", params: { source_type: "Comment", source_id: 1 }
end.to raise_error(ActiveRecord::RecordNotFound)
end
it "raises an error when the source can't be found" do
expect do
get "/dashboard/subscriptions", params: { source_type: article.class.name, source_id: article.id + 999 }
end.to raise_error(ActiveRecord::RecordNotFound)
end
it "renders pagination if minimum amount of subscriptions" do
user.add_role(:admin)
article_with_user_subscription_tag = create(:article, user: user, with_user_subscription_tag: true)
create_list(:user_subscription,
102, # Current pagination limit is 100
author: user,
user_subscription_sourceable: article_with_user_subscription_tag)
get "/dashboard/subscriptions", params: { source_type: article_with_user_subscription_tag.class.name, source_id: article_with_user_subscription_tag.id }
expect(response.body).to include "pagination"
end
it "does not render pagination if less than one full page" do
user.add_role(:admin)
article_with_user_subscription_tag = create(:article, user: user, with_user_subscription_tag: true)
create_list(:user_subscription,
5,
author: user,
user_subscription_sourceable: article_with_user_subscription_tag)
get "/dashboard/subscriptions", params: { source_type: article_with_user_subscription_tag.class.name, source_id: article_with_user_subscription_tag.id }
expect(response.body).not_to include "pagination"
end
end
end