From 7b46bae1095811b7584f7c2d2241e6be200ff6b4 Mon Sep 17 00:00:00 2001 From: Laura Mosher <2660801+lauramosher@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:51:23 -0400 Subject: [PATCH] feat(API): introduce paging APIs (#977) Co-authored-by: Laura Mosher --- .../app/controllers/api/stories_controller.rb | 18 ++---- rails/app/pages/api/stories_page.rb | 29 +++++++++ rails/app/views/api/places/show.json.jbuilder | 8 --- .../app/views/api/stories/index.json.jbuilder | 11 +++- rails/spec/requests/api/public_place_spec.rb | 14 ----- .../spec/requests/api/public_stories_spec.rb | 60 ++++++++++++++++++- 6 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 rails/app/pages/api/stories_page.rb diff --git a/rails/app/controllers/api/stories_controller.rb b/rails/app/controllers/api/stories_controller.rb index dcc60352a..aac857f6e 100644 --- a/rails/app/controllers/api/stories_controller.rb +++ b/rails/app/controllers/api/stories_controller.rb @@ -2,19 +2,9 @@ module Api class StoriesController < BaseController def index community = Community.where(public: true).find_by!(slug: params[:community_id]) - @stories = community.stories.joins(:places, :speakers).where(permission_level: :anonymous).preload(:places, :speakers) + @page = Api::StoriesPage.new(community, story_params) - # Filters - @stories = @stories.where(places: {id: story_params[:places]}) if story_params[:places] - @stories = @stories.where(places: {region: story_params[:region]}) if story_params[:region] - @stories = @stories.where(places: {type_of_place: story_params[:type_of_place]}) if story_params[:type_of_place] - @stories = @stories.where(topic: story_params[:topic]) if story_params[:topic] - @stories = @stories.where(language: story_params[:language]) if story_params[:language] - @stories = @stories.where(speakers: {id: story_params[:speakers]}) if story_params[:speakers] - @stories = @stories.where(speakers: {speaker_community: story_params[:speaker_community]}) if story_params[:speaker_community] - - # Ensure distinct - @stories = @stories.distinct + @stories = @page.data end def show @@ -26,6 +16,10 @@ def show def story_params params.permit( + :sort_by, + :sort_dir, + :limit, + :offset, places: [], region: [], topic: [], diff --git a/rails/app/pages/api/stories_page.rb b/rails/app/pages/api/stories_page.rb new file mode 100644 index 000000000..ea2fc67a8 --- /dev/null +++ b/rails/app/pages/api/stories_page.rb @@ -0,0 +1,29 @@ +class Api::StoriesPage < Page + def initialize(community, meta = {}) + @community = community + @meta = meta + + @meta[:limit] ||= 10 + @meta[:offset] ||= 0 + @meta[:sort_by] ||= "created_at" + @meta[:sort_dir] ||= "desc" + end + + def relation + stories = @community.stories.joins(:places, :speakers).where(permission_level: :anonymous) + + # Filters + stories = stories.where(places: {id: @meta[:places]}) if @meta[:places] + stories = stories.where(places: {region: @meta[:region]}) if @meta[:region] + stories = stories.where(places: {type_of_place: @meta[:type_of_place]}) if @meta[:type_of_place] + stories = stories.where(topic: @meta[:topic]) if @meta[:topic] + stories = stories.where(language: @meta[:language]) if @meta[:language] + stories = stories.where(speakers: {id: @meta[:speakers]}) if @meta[:speakers] + stories = stories.where(speakers: {speaker_community: @meta[:speaker_community]}) if @meta[:speaker_community] + + # Ensure distinct + stories = stories.preload(:places, :speakers).distinct + + stories.order(@meta[:sort_by] => @meta[:sort_dir]) + end +end \ No newline at end of file diff --git a/rails/app/views/api/places/show.json.jbuilder b/rails/app/views/api/places/show.json.jbuilder index 9bd84c7c7..f69054b6a 100644 --- a/rails/app/views/api/places/show.json.jbuilder +++ b/rails/app/views/api/places/show.json.jbuilder @@ -3,12 +3,4 @@ json.(@place, :id, :name, :description, :region) json.placenameAudio @place.name_audio_url(full_url: true) json.typeOfPlace @place.type_of_place -json.stories @place.stories.where(permission_level: :anonymous) do |story| - json.extract! story, :id, :title, :topic, :desc, :language - json.mediaContentTypes story.media_types - - json.createdAt story.created_at - json.updatedAt story.updated_at -end - json.points [@place.public_point_feature] diff --git a/rails/app/views/api/stories/index.json.jbuilder b/rails/app/views/api/stories/index.json.jbuilder index 33430ca53..021761f4e 100644 --- a/rails/app/views/api/stories/index.json.jbuilder +++ b/rails/app/views/api/stories/index.json.jbuilder @@ -1,5 +1,9 @@ -json.total @stories.size -json.points @stories.flat_map { |s| s.public_points }.uniq +json.total @page.total + +# Regardless of Story list page, all points in the data relation +# should be returned for map markers. +json.points @page.relation.flat_map { |s| s.public_points }.uniq + json.stories @stories do |story| json.extract! story, :id, :title, :topic, :desc, :language json.mediaContentTypes story.media_types @@ -8,3 +12,6 @@ json.stories @stories do |story| json.createdAt story.created_at json.updatedAt story.updated_at end + +json.hasNextPage @page.has_next_page? +json.nextPageMeta @page.next_page_meta \ No newline at end of file diff --git a/rails/spec/requests/api/public_place_spec.rb b/rails/spec/requests/api/public_place_spec.rb index 6cbb15f13..ad5a08cef 100644 --- a/rails/spec/requests/api/public_place_spec.rb +++ b/rails/spec/requests/api/public_place_spec.rb @@ -51,21 +51,7 @@ def json_response "region", "placenameAudio", "typeOfPlace", - "stories", "points" ) end - - it "includes the places public stories" do - get "/api/communities/atlam/places/123" - - expect(json_response["stories"].length).to eq(1) - expect(json_response["stories"].first).to include( - "id" => public_story.id, - "title" => public_story.title, - "topic" => public_story.topic, - "desc" => public_story.desc, - "language" => public_story.language - ) - end end diff --git a/rails/spec/requests/api/public_stories_spec.rb b/rails/spec/requests/api/public_stories_spec.rb index 4f272a7b6..569d7e444 100644 --- a/rails/spec/requests/api/public_stories_spec.rb +++ b/rails/spec/requests/api/public_stories_spec.rb @@ -25,10 +25,10 @@ def json_response get "/api/communities/cool_community/stories" expect(response).to have_http_status(:ok) - expect(json_response.keys).to contain_exactly("total", "points", "stories") + expect(json_response.keys).to contain_exactly("total", "points", "stories", "hasNextPage", "nextPageMeta") end - context "filters" do + context "filters and sort" do let!(:place_1) { create(:place, community: community, region: "the internet") } let!(:place_2) { create(:place, community: community, type_of_place: "online") } let!(:speaker_1) { create(:speaker, community: community) } @@ -42,6 +42,7 @@ def json_response let!(:story_1) do create( :story, + title: "Zeta", community: community, places: [place_2], speakers: [speaker_1, speaker_2], @@ -58,6 +59,7 @@ def json_response let!(:story_2) do create( :story, + title: "Omega", community: community, topic: "tech", places: [place_1], @@ -74,6 +76,7 @@ def json_response let!(:story_3) do create( :story, + title: "Alpha", community: community, topic: "nonprofit work", language: "Spanish", @@ -125,7 +128,60 @@ def json_response expect(json_response["total"]).to eq(2) expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id, story_2.id) + end + + it "does not include stories without at least one place" do + place_1.destroy! + + get "/api/communities/cool_community/stories" + + + expect(json_response["total"]).to eq(1) + expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id) + end + + it "does not include stories without at least one speaker" do + speaker_2.destroy! + + get "/api/communities/cool_community/stories" + + expect(json_response["total"]).to eq(2) + expect(json_response["stories"].map { |s| s["id"] }).to contain_exactly(story_1.id, story_3.id) + end + it "correctly sorts" do + # alphabetical + get "/api/communities/cool_community/stories", params: {sort_by: "title", sort_dir: "asc"} + expect(json_response["stories"].map{ |s| s["title"] }).to eq( + ["Alpha", "Omega", "Zeta"] + ) + + # reverse alphabetical (default: desc) + get "/api/communities/cool_community/stories", params: {sort_by: "title"} + expect(json_response["stories"].map{ |s| s["title"] }).to eq( + ["Zeta", "Omega", "Alpha"] + ) + + # with filter + get "/api/communities/cool_community/stories", params: {sort_by: "title", sort_dir: "asc", region: ["the internet"]} + expect(json_response["stories"].map{ |s| s["title"] }).to eq( + ["Alpha", "Omega"] + ) + end + + it "correctly paginates with filters" do + get "/api/communities/cool_community/stories", params: {limit: 1} + + expect(json_response["total"]).to eq(3) + expect(json_response["stories"].count).to eq(1) + expect(json_response["hasNextPage"]).to be true + + # filter down to one place + get "/api/communities/cool_community/stories", params: {limit: 1, places: [place_2.id]} + + expect(json_response["total"]).to eq(1) + expect(json_response["stories"].count).to eq(1) + expect(json_response["hasNextPage"]).to be false end end end