diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 577bd04d656fd14dac84d395939af48ec673e399..b3a1b510db9c76fd3bc49c0bae7809435845eb45 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -10,6 +10,7 @@ class Explore::ProjectsController < Explore::ApplicationController MIN_SEARCH_LENGTH = 3 PAGE_LIMIT = 50 + RSS_ENTRIES_LIMIT = 20 before_action :set_non_archived_param before_action :set_sorting @@ -83,6 +84,14 @@ def topic params[:topic] = @topic.name @projects = load_projects + + respond_to do |format| + format.html + format.atom do + @projects = @projects.projects_order_id_desc.limit(RSS_ENTRIES_LIMIT) + render layout: 'xml' + end + end end private diff --git a/app/views/explore/projects/_project.atom.builder b/app/views/explore/projects/_project.atom.builder new file mode 100644 index 0000000000000000000000000000000000000000..f0500901a73f74ff5aa1d9cae5dbdff9c84ad6fb --- /dev/null +++ b/app/views/explore/projects/_project.atom.builder @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +xml.entry do + xml.title project.name + xml.link href: project_url(project), rel: "alternate", type: "text/html" + xml.id project_url(project) + xml.updated project.created_at + + if project.description.present? + xml.summary(type: "xhtml") do |summary| + summary << project.description + end + end +end diff --git a/app/views/explore/projects/topic.atom.builder b/app/views/explore/projects/topic.atom.builder new file mode 100644 index 0000000000000000000000000000000000000000..4712d415daa00bffac4b87abc4d0f474081cc3b4 --- /dev/null +++ b/app/views/explore/projects/topic.atom.builder @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +xml.title @topic.name +xml.link href: topic_explore_projects_url(@topic.name, rss_url_options), rel: "self", type: "application/atom+xml" +xml.link href: topic_explore_projects_url(@topic.name), rel: "alternate", type: "text/html" +xml.id topic_explore_projects_url(@topic.id) +xml.updated @projects[0].updated_at.xmlschema if @projects[0] + +xml << render(@projects) if @projects.any? diff --git a/app/views/explore/projects/topic.html.haml b/app/views/explore/projects/topic.html.haml index b26abefcb0e62338c6ae11b1440db28b283af9c1..bba0c1b9ca0c532d7b0785772eeb11b82fb20b9b 100644 --- a/app/views/explore/projects/topic.html.haml +++ b/app/views/explore/projects/topic.html.haml @@ -22,9 +22,11 @@ %div{ class: container_class } .gl-py-5.gl-border-gray-100.gl-border-b-solid.gl-border-b-1 %h3.gl-m-0= _('Projects with this topic') - .top-area.gl-pt-2.gl-pb-2 + .top-area.gl-pt-2.gl-pb-2.gl-justify-content-space-between .nav-controls = render 'shared/projects/search_form' = render 'filter' + = link_to topic_explore_projects_path(@topic.name, rss_url_options), title: s_("Topics|Subscribe to the new projects feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' do + = sprite_icon('rss', css_class: 'gl-icon') = render 'projects', projects: @projects diff --git a/config/routes/explore.rb b/config/routes/explore.rb index 6ddf4d2313861cb9d272014b41f232b48329d12e..6777571bb6830a90a5c428680fb33828d91ad3e3 100644 --- a/config/routes/explore.rb +++ b/config/routes/explore.rb @@ -6,7 +6,7 @@ get :trending get :starred get :topics - get 'topics/:topic_name', action: :topic, as: :topic, constraints: { topic_name: /.+/ } + get 'topics/:topic_name', action: :topic, as: :topic, constraints: { format: /(html|atom)/, topic_name: /.+?/ } end end diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 5bd7c12ed313a6ba66d9579e843a4614a68fbb47..ea937ed9244874f069280eac7fe33433b24c50b1 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -55,6 +55,10 @@ To explore project topics: The **Explore topics** page shows a list of topics, sorted by the number of associated projects. +If you want to know when new projects are added to the topic, you can use +its RSS feed for that, which is the RSS icon on the right of the filter +bar. + You can assign topics to a project on the [Project Settings page](settings/index.md#assign-topics-to-a-project). If you're an instance administrator, you can administer all project topics from the diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ee98ce52a0c9ce1d7cc9c17d5ced3a7f52efc8cf..943112f4e0d7bae14d44424eedb8ce884fd1694f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -48233,6 +48233,9 @@ msgstr "" msgid "Topics could not be merged!" msgstr "" +msgid "Topics|Subscribe to the new projects feed" +msgstr "" + msgid "Total" msgstr "" diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb index e73e115b77ded172e37033d5fbaaab738e22211e..68ae1ca218bf41d2a0096497fc72917ac00bfa53 100644 --- a/spec/controllers/explore/projects_controller_spec.rb +++ b/spec/controllers/explore/projects_controller_spec.rb @@ -122,6 +122,59 @@ end end end + + describe 'GET #topic.atom' do + context 'when topic does not exist' do + it 'renders a 404 error' do + get :topic, format: :atom, params: { topic_name: 'topic1' } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when topic exists' do + let(:topic) { create(:topic, name: 'topic1') } + let_it_be(:older_project) { create(:project, :public, updated_at: 1.day.ago) } + let_it_be(:newer_project) { create(:project, :public, updated_at: 2.days.ago) } + + before do + create(:project_topic, project: older_project, topic: topic) + create(:project_topic, project: newer_project, topic: topic) + end + + it 'renders the template' do + get :topic, format: :atom, params: { topic_name: 'topic1' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('topic', layout: :xml) + end + + it 'sorts repos by descending creation date' do + get :topic, format: :atom, params: { topic_name: 'topic1' } + + expect(assigns(:projects)).to match_array [newer_project, older_project] + end + + it 'finds topic by case insensitive name' do + get :topic, format: :atom, params: { topic_name: 'TOPIC1' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('topic', layout: :xml) + end + + describe 'when topic contains more than 20 projects' do + before do + create_list(:project, 22, :public, topics: [topic]) + end + + it 'does not assigns more than 20 projects' do + get :topic, format: :atom, params: { topic_name: 'topic1' } + + expect(assigns(:projects).count).to be(20) + end + end + end + end end shared_examples "blocks high page numbers" do diff --git a/spec/features/atom/topics_spec.rb b/spec/features/atom/topics_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..078c5b55eeb36ee4abdc2ea08880ff90c482413d --- /dev/null +++ b/spec/features/atom/topics_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Topic Feed", feature_category: :groups_and_projects do + let_it_be(:topic) { create(:topic, name: 'test-topic', title: 'Test topic') } + let_it_be(:empty_topic) { create(:topic, name: 'test-empty-topic', title: 'Test empty topic') } + let_it_be(:project1) { create(:project, :public, topic_list: topic.name) } + let_it_be(:project2) { create(:project, :public, topic_list: topic.name) } + + context 'when topic does not exist' do + let(:path) { topic_explore_projects_path(topic_name: 'non-existing', format: 'atom') } + + it 'renders 404' do + visit path + + expect(status_code).to eq(404) + end + end + + context 'when topic exists' do + before do + visit topic_explore_projects_path(topic_name: topic.name, format: 'atom') + end + + it "renders topic atom feed" do + expect(body).to have_selector('feed title') + end + + it "has project entries" do + expect(body).to have_content(project1.name) + expect(body).to have_content(project2.name) + end + end + + context 'when topic is empty' do + before do + visit topic_explore_projects_path(topic_name: empty_topic.name, format: 'atom') + end + + it "renders topic atom feed" do + expect(body).to have_selector('feed title') + end + + it "has no project entry" do + expect(body).to have_no_selector('entry') + end + end +end diff --git a/spec/views/explore/projects/topic.html.haml_spec.rb b/spec/views/explore/projects/topic.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1d2085b3be6d47595809ced1da20df79b76ab0bc --- /dev/null +++ b/spec/views/explore/projects/topic.html.haml_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'explore/projects/topic.html.haml', feature_category: :groups_and_projects do + let(:topic) { build_stubbed(:topic, name: 'test-topic', title: 'Test topic') } + let(:project) { build_stubbed(:project, :public, topic_list: topic.name) } + + before do + assign(:topic, topic) + assign(:projects, [project]) + + controller.params[:controller] = 'explore/projects' + controller.params[:action] = 'topic' + + allow(view).to receive(:current_user).and_return(nil) + + render + end + + it 'renders atom feed button with matching path' do + expect(rendered).to have_link(href: topic_explore_projects_path(topic.name, format: 'atom')) + end +end