FEATURE: Admin > Subscriptions Pagination (#50)

Meta topic: https://meta.discourse.org/t/subscriptions-add-pagination-to-admin-subscriptions-view/172500

This adds support for pagination using our `{{load-more}}` component in core. Implementation on the backend was a bit tricky because we don't return all results from Stripe, only those that match local subscriptions stored in the `DiscourseSubscriptions::Subscription` model.
This commit is contained in:
Justin DiRose 2021-02-05 11:57:53 -06:00 committed by GitHub
parent eaf1729f6f
commit a282475da3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 50 deletions

View File

@ -7,14 +7,33 @@ module DiscourseSubscriptions
include DiscourseSubscriptions::Group
before_action :set_api_key
PAGE_LIMIT = 10
def index
begin
subscription_ids = Subscription.all.pluck(:external_id)
subscriptions = []
subscriptions = {
has_more: false,
data: [],
length: 0,
last_record: params[:last_record]
}
if subscription_ids.present? && is_stripe_configured?
subscriptions = ::Stripe::Subscription.list(expand: ['data.plan.product'])
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
while subscriptions[:length] < PAGE_LIMIT
current_set = get_subscriptions(subscriptions[:last_record])
until valid_subscriptions = find_valid_subscriptions(current_set[:data], subscription_ids) do
current_set = get_subscriptions(current_set[:data].last)
break if current_set[:has_more] == false
end
subscriptions[:data] = subscriptions[:data].concat(valid_subscriptions.to_a)
subscriptions[:last_record] = current_set[:data].last[:id] if current_set[:data].present?
subscriptions[:length] = subscriptions[:data].length
subscriptions[:has_more] = current_set[:has_more]
break if subscriptions[:has_more] == false
end
elsif !is_stripe_configured?
subscriptions = nil
end
@ -54,6 +73,15 @@ module DiscourseSubscriptions
private
def get_subscriptions(start)
::Stripe::Subscription.list(expand: ['data.plan.product'], limit: PAGE_LIMIT, starting_after: start)
end
def find_valid_subscriptions(data, ids)
valid = data.select { |sub| ids.include?(sub[:id]) }
valid.empty? ? nil : valid
end
# this will only refund the most recent subscription payment
def refund_subscription(subscription_id)
subscription = ::Stripe::Subscription.retrieve(subscription_id)

View File

@ -1,12 +1,30 @@
import AdminSubscription from "discourse/plugins/discourse-subscriptions/discourse/models/admin-subscription";
import Controller from "@ember/controller";
import showModal from "discourse/lib/show-modal";
export default Controller.extend({
loading: false,
actions: {
showCancelModal(subscription) {
showModal("admin-cancel-subscription", {
model: subscription,
});
},
loadMore() {
if (!this.loading && this.model.has_more) {
this.set("loading", true);
return AdminSubscription.loadMore(this.model.last_record).then(
(result) => {
const updated = this.model.data.concat(result.data);
this.set("model", result);
this.set("model.data", updated);
this.set("loading", false);
}
);
}
},
},
});

View File

@ -38,9 +38,20 @@ AdminSubscription.reopenClass({
if (result === null) {
return { unconfigured: true };
}
return result.map((subscription) =>
result.data = result.data.map((subscription) =>
AdminSubscription.create(subscription)
);
return result;
});
},
loadMore(lastRecord) {
return ajax(`/s/admin/subscriptions?last_record=${lastRecord}`, {
method: "get",
}).then((result) => {
result.data = result.data.map((subscription) =>
AdminSubscription.create(subscription)
);
return result;
});
},
});

View File

@ -2,42 +2,45 @@
<p>{{i18n 'discourse_subscriptions.admin.unconfigured'}}</p>
<p><a href="https://meta.discourse.org/t/discourse-subscriptions/140818/">Discourse Subscriptions on Meta</a></p>
{{else}}
<table class="table discourse-patrons-table">
<thead>
<tr>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.user'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.subscription_id'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.customer'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.product'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.plan'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.status'}}</th>
<th class="td-right">{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.created_at'}}</th>
<th></th>
</tr>
</thead>
{{#each model as |subscription|}}
<tr>
<td>
{{#if subscription.metadataUserExists}}
<a href="{{unbound subscription.subscriptionUserPath}}">
{{subscription.metadata.username}}
</a>
{{/if}}
</td>
<td>{{subscription.id}}</td>
<td>{{subscription.customer}}</td>
<td>{{subscription.plan.product.name}}</td>
<td>{{subscription.plan.nickname}}</td>
<td>{{subscription.status}}</td>
<td class="td-right">{{format-unix-date subscription.created}}</td>
<td class="td-right">
{{#if subscription.loading}}
{{loading-spinner size="small"}}
{{else}}
{{d-button disabled=subscription.canceled label="cancel" action=(action "showCancelModal" subscription) icon="times"}}
{{/if}}
</td>
</tr>
{{/each}}
</table>
{{#load-more selector=".discourse-patrons-table tr" action=(action "loadMore")}}
<table class="table discourse-patrons-table">
<thead>
<tr>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.user'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.subscription_id'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.customer'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.product'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.plan'}}</th>
<th>{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.status'}}</th>
<th class="td-right">{{i18n 'discourse_subscriptions.admin.subscriptions.subscription.created_at'}}</th>
<th></th>
</tr>
</thead>
{{#each model.data as |subscription|}}
<tr>
<td>
{{#if subscription.metadataUserExists}}
<a href="{{unbound subscription.subscriptionUserPath}}">
{{subscription.metadata.username}}
</a>
{{/if}}
</td>
<td>{{subscription.id}}</td>
<td>{{subscription.customer}}</td>
<td>{{subscription.plan.product.name}}</td>
<td>{{subscription.plan.nickname}}</td>
<td>{{subscription.status}}</td>
<td class="td-right">{{format-unix-date subscription.created}}</td>
<td class="td-right">
{{#if subscription.loading}}
{{loading-spinner size="small"}}
{{else}}
{{d-button disabled=subscription.canceled label="cancel" action=(action "showCancelModal" subscription) icon="times"}}
{{/if}}
</td>
</tr>
{{/each}}
</table>
{{/load-more}}
{{conditional-loading-spinner condition=loading}}
{{/if}}

View File

@ -13,6 +13,7 @@ module DiscourseSubscriptions
before do
Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id)
Fabricate(:subscription, external_id: "sub_77777", customer_id: customer.id)
end
context 'unauthenticated' do
@ -34,21 +35,44 @@ module DiscourseSubscriptions
before { sign_in(admin) }
describe "index" do
it "gets the subscriptions and products" do
before do
SiteSetting.discourse_subscriptions_public_key = "public-key"
SiteSetting.discourse_subscriptions_secret_key = "secret-key"
::Stripe::Subscription.expects(:list).with(expand: ['data.plan.product']).returns(
[
{ id: "sub_12345" },
{ id: "sub_nope" }
]
)
end
it "gets the subscriptions and products" do
::Stripe::Subscription.expects(:list)
.with(expand: ['data.plan.product'], limit: 10, starting_after: nil)
.returns(
has_more: false,
data: [
{ id: "sub_12345" },
{ id: "sub_nope" }
]
)
get "/s/admin/subscriptions.json"
subscriptions = response.parsed_body[0]["id"]
subscriptions = response.parsed_body["data"][0]["id"]
expect(response.status).to eq(200)
expect(subscriptions).to eq("sub_12345")
end
it "handles starting at a different point in the set" do
::Stripe::Subscription.expects(:list)
.with(expand: ['data.plan.product'], limit: 10, starting_after: 'sub_nope')
.returns(
has_more: false,
data: [
{ id: "sub_77777" },
{ id: "sub_yepnoep" }
]
)
get "/s/admin/subscriptions.json", params: { last_record: 'sub_nope' }
subscriptions = response.parsed_body["data"][0]["id"]
expect(response.status).to eq(200)
expect(subscriptions).to eq("sub_77777")
end
end
describe "destroy" do