From b96e0ca4cb45c42a24e8a7ffe5ca36989909ca31 Mon Sep 17 00:00:00 2001 From: Mayra Lucia Navarro Date: Wed, 16 Aug 2023 00:27:33 -0500 Subject: [PATCH] Create Speaker module for Admin (#182) * Create Factory for events * Generate Speaker module for admin - Add HTTParty to validate links - Add views, controllers, policies, model for speaker * Config custom error view to adapt to bootstrap * Fix lint errors --- .eslintrc.js | 5 + Gemfile | 3 + Gemfile.lock | 11 +- app/controllers/admin/speakers_controller.rb | 95 +++++++++++ app/helpers/admin/speakers_helper.rb | 6 + app/javascript/packs/hello_react.jsx | 24 --- app/models/speaker.rb | 38 +++++ app/policies/speaker_policy.rb | 26 +++ app/views/admin/events/_form.html.erb | 2 +- app/views/admin/events/edit.html.erb | 2 +- app/views/admin/events/index.html.erb | 63 +++---- app/views/admin/events/new.html.erb | 2 +- app/views/admin/speakers/_form.html.erb | 46 +++++ app/views/admin/speakers/edit.html.erb | 6 + app/views/admin/speakers/index.html.erb | 51 ++++++ .../speakers/index/_speaker_links.html.erb | 25 +++ app/views/admin/speakers/new.html.erb | 7 + app/views/admin/speakers/show.html.erb | 10 ++ app/views/layouts/admin/_flashes.html.erb | 2 +- app/views/layouts/admin/_form_errors.html.erb | 8 +- app/views/layouts/admin/_header.html.erb | 11 +- config/initializers/custom_error.rb | 5 + config/routes.rb | 2 + .../admin/events_controller_spec.rb | 39 +++-- .../admin/speakers_controller_spec.rb | 160 ++++++++++++++++++ .../controllers/api/events_controller_spec.rb | 55 ++---- spec/factories/speakers.rb | 24 +++ spec/models/speaker_spec.rb | 51 +++++- spec/rails_helper.rb | 6 + spec/system/managing_events_spec.rb | 2 +- spec/system/managing_speakers_spec.rb | 45 +++++ 31 files changed, 705 insertions(+), 127 deletions(-) create mode 100644 app/controllers/admin/speakers_controller.rb create mode 100644 app/helpers/admin/speakers_helper.rb delete mode 100644 app/javascript/packs/hello_react.jsx create mode 100644 app/policies/speaker_policy.rb create mode 100644 app/views/admin/speakers/_form.html.erb create mode 100644 app/views/admin/speakers/edit.html.erb create mode 100644 app/views/admin/speakers/index.html.erb create mode 100644 app/views/admin/speakers/index/_speaker_links.html.erb create mode 100644 app/views/admin/speakers/new.html.erb create mode 100644 app/views/admin/speakers/show.html.erb create mode 100644 config/initializers/custom_error.rb create mode 100644 spec/controllers/admin/speakers_controller_spec.rb create mode 100644 spec/factories/speakers.rb create mode 100644 spec/system/managing_speakers_spec.rb diff --git a/.eslintrc.js b/.eslintrc.js index 79d0adfc..89b530f4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,11 @@ module.exports = { }, ], }, + settings: { + react: { + version: 'detect', + }, + }, overrides: [ { files: [ diff --git a/Gemfile b/Gemfile index 4ea6e88f..46ef2662 100644 --- a/Gemfile +++ b/Gemfile @@ -34,6 +34,9 @@ gem 'google_drive', git: 'https://github.com/wnbrb/google-drive-ruby.git', branc # cuts off a string of HTML gem 'truncate_html', '~> 0.9.3' +# quickly call web links +gem 'httpparty', '~> 0.2' + group :development, :test do gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'faker', '~> 2.18.0' diff --git a/Gemfile.lock b/Gemfile.lock index 27c873d3..72ae619e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -120,7 +120,7 @@ GEM railties (>= 5.0.0) faker (2.18.0) i18n (>= 1.6, < 2) - faraday (2.7.10) + faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-mashify (0.1.1) @@ -154,8 +154,13 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) hashie (5.0.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.1) + httpparty (0.2.0) + httparty (> 0) + i18n (1.12.0) concurrent-ruby (~> 1.0) io-console (0.6.0) irb (1.7.4) @@ -188,6 +193,7 @@ GEM minitest (5.19.0) msgpack (1.7.2) multi_json (1.15.0) + multi_xml (0.6.0) multipart-post (2.3.0) net-imap (0.3.7) date @@ -396,6 +402,7 @@ DEPENDENCIES factory_bot_rails (~> 6.2.0) faker (~> 2.18.0) google_drive! + httpparty (~> 0.2) jbuilder (~> 2.7) jsbundling-rails (~> 1.1) jwt (>= 2.6.0) diff --git a/app/controllers/admin/speakers_controller.rb b/app/controllers/admin/speakers_controller.rb new file mode 100644 index 00000000..abcb7e11 --- /dev/null +++ b/app/controllers/admin/speakers_controller.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true +module Admin + class SpeakersController < AdminController + before_action :authorize_event + before_action :set_speaker, only: %w[show edit update destroy] + + # GET /admin/speakers + def index + @speakers = Speaker.ordered_by_name + end + + # GET /admin/speakers/1 + def show; end + + # GET /admin/speakers/new + def new + @speaker = Speaker.new + end + + # GET /admin/speakers/1/edit + def edit + render_not_found unless @speaker + end + + # POST /admin/speakers + def create + @speaker = Speaker.new(speaker_params) + + respond_to do |format| + if @speaker.save + format.html do + redirect_to edit_admin_speaker_url(@speaker), + notice: 'Speaker was successfully created.' + end + else + format.html { render :new, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /admin/speakers/1 + def update + respond_to do |format| + if @speaker.update(speaker_params) + format.html do + redirect_to edit_admin_speaker_url(@speaker), + notice: 'Speaker was successfully updated.' + end + format.json { render :show, status: :ok, location: @speaker } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @speaker.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /admin/speakers/1 + # def destroy + # @speaker.destroy + + # respond_to do |format| + # format.html do + # redirect_to admin_speakers_url, notice: 'Speaker was successfully destroyed.' + # end + # end + # end + + private + + def authorize_event + authorize Event + end + + # Use callbacks to share common setup or constraints between actions. + def set_speaker + @speaker = Speaker.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def speaker_params + params.require(:speaker).permit( + :name, + :bio, + :tagline, + :image_url, + :github, + :linkedin, + :mastodon, + :website, + :twitter, + :other, + ) + end + end +end diff --git a/app/helpers/admin/speakers_helper.rb b/app/helpers/admin/speakers_helper.rb new file mode 100644 index 00000000..d430c448 --- /dev/null +++ b/app/helpers/admin/speakers_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module Admin + module SpeakersHelper + #Add helpers + end +end diff --git a/app/javascript/packs/hello_react.jsx b/app/javascript/packs/hello_react.jsx deleted file mode 100644 index e23eec2c..00000000 --- a/app/javascript/packs/hello_react.jsx +++ /dev/null @@ -1,24 +0,0 @@ -// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file, -// like app/views/layouts/application.html.erb. All it does is render
Hello React
at the bottom -// of the page. - -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; - -const Hello = (props) =>
Hello {props.name}!
; - -Hello.defaultProps = { - name: 'David', -}; - -Hello.propTypes = { - name: PropTypes.string, -}; - -document.addEventListener('DOMContentLoaded', () => { - ReactDOM.render( - , - document.body.appendChild(document.createElement('div')), - ); -}); diff --git a/app/models/speaker.rb b/app/models/speaker.rb index 71aed937..251be285 100644 --- a/app/models/speaker.rb +++ b/app/models/speaker.rb @@ -1,8 +1,46 @@ # frozen_string_literal: true class Speaker < ApplicationRecord + include HTTParty + SOCIAL_MEDIA_LINKS = %w[github linkedin mastodon twitter website other].freeze + has_many :event_speakers, dependent: :destroy has_many :events, through: :event_speakers validates :name, :bio, :image_url, presence: true + validate :links, :validate_social_media_brand + validate :url_exists?, if: ->(s) { s.links.compact_blank.present? } + + store :links, accessors: SOCIAL_MEDIA_LINKS, coder: JSON + + before_validation :format_links + + scope :ordered_by_name, -> { order(:name) } + + def validate_social_media_brand + return if links.blank? || links.keys.all? { |key| SOCIAL_MEDIA_LINKS.include?(key) } + + errors.add(:links, 'This social media is not allowed') + end + + def format_links + return if links.blank? + + links.transform_keys!(&:downcase) + end + + def url_exists? + links.each { |_, url| errors.add(:links, 'This url is not valid') unless url_valid?(url) } + end + + def empty_links? + links.compact_blank.empty? + end + + private + + def url_valid?(url) + response = HTTParty.get(url) + response.code == 200 + end end diff --git a/app/policies/speaker_policy.rb b/app/policies/speaker_policy.rb new file mode 100644 index 00000000..80fc3dc2 --- /dev/null +++ b/app/policies/speaker_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +class SpeakerPolicy < ApplicationPolicy + def index? + user.admin? + end + + def new? + user.admin? + end + + def create? + user.admin? + end + + def edit? + user.admin? + end + + def show? + edit? + end + + def update? + user.admin? + end +end diff --git a/app/views/admin/events/_form.html.erb b/app/views/admin/events/_form.html.erb index 80cef8aa..74105c8e 100644 --- a/app/views/admin/events/_form.html.erb +++ b/app/views/admin/events/_form.html.erb @@ -1,4 +1,4 @@ - <%= form_for [:admin, @event.becomes(Event)] do |f| %> + <%= form_for [:admin, event.becomes(Event)] do |f| %> <%= render 'layouts/admin/form_errors', object: f.object %>
<%= f.text_field :title, class: "form-control" %> diff --git a/app/views/admin/events/edit.html.erb b/app/views/admin/events/edit.html.erb index 4208a30c..3b8fdfd0 100644 --- a/app/views/admin/events/edit.html.erb +++ b/app/views/admin/events/edit.html.erb @@ -1,6 +1,6 @@

Edit event

- <%=render 'form'%> + <%=render 'form', event: @event %>
diff --git a/app/views/admin/events/index.html.erb b/app/views/admin/events/index.html.erb index 98930c17..c48313f6 100644 --- a/app/views/admin/events/index.html.erb +++ b/app/views/admin/events/index.html.erb @@ -1,7 +1,7 @@

Event List

- <%= link_to new_admin_event_path, class: "btn btn-primary btn-lg mb-3" do %> + <%= link_to new_admin_event_path, class: "btn btn-primary btn-md mb-4" do %> Create new Event <% end %> @@ -9,34 +9,37 @@
- - - - - - - - - - - - - - <% @events.each do |event| %> - - - - - - - - - - <% end %> - -
TitleTypeSpeakersDateLocationDescriptionActions
<%= event.title %><%= event.type %><%= event.speakers.map(&:name).join(", ") %><%= event.date.strftime('%B %d, %Y %I:%M%p %:z') %><%= event.location %><%= truncate_html(event.description) %> - <%= link_to 'Edit', edit_admin_event_path(event) %> - <%= link_to 'Delete', admin_event_path(event), method: :delete %> -
+
+ + + + + + + + + + + + + + <% @events.each do |event| %> + + + + + + + + + + + <% end %> + +
TitleTypeSpeakersDateLocationDescriptionActions
<%= event.title %><%= event.type %><%= event.speakers.map(&:name).join(", ") %><%= event.date.strftime('%B %d, %Y %I:%M%p %:z') %><%= event.location %><%= truncate_html(event.description) %> + <%= link_to 'Edit', edit_admin_event_path(event) %> + <%= button_to "Delete", admin_event_url(event), form_class: "", class: "link-primary border-0 bg-transparent text-decoration-underline", method: :delete %> +
+
diff --git a/app/views/admin/events/new.html.erb b/app/views/admin/events/new.html.erb index a6f5ed2d..4f4c1d0a 100644 --- a/app/views/admin/events/new.html.erb +++ b/app/views/admin/events/new.html.erb @@ -1,6 +1,6 @@

New Event

- <%= render 'form'%> + <%= render 'form', event: @event %>
diff --git a/app/views/admin/speakers/_form.html.erb b/app/views/admin/speakers/_form.html.erb new file mode 100644 index 00000000..5592d23d --- /dev/null +++ b/app/views/admin/speakers/_form.html.erb @@ -0,0 +1,46 @@ +<%= form_for [:admin, speaker] do |f| %> + <%= render "layouts/admin/form_errors", object: f.object %> +
+ <%= f.text_field :name, class: "form-control" %> + <%= f.label :name %> +
+
+ <%= f.text_area :bio, rows: 40, class: "form-control", style: "height: 200px" %> + <%= f.label :bio %> +
+
+ <%= f.text_field :tagline, class: "form-control" %> + <%= f.label :tagline %> +
+
+ <%= f.url_field :image_url, class: "form-control" %> + <%= f.label :image_url, "Image link" %> +
+ +
+
+ + <% accordion_collapse_class = speaker.links.compact_blank.present? ? "accordion-collapse collapse show" : "accordion-collapse collapse" %> +
+
+ <% Speaker::SOCIAL_MEDIA_LINKS.each do |link| %> +
+
+ <%= f.label link.to_sym, class: "input-group-text" %> + <%= f.text_field link.to_sym, class: "form-control", placeholder: "https://socialmedia.url", aria: { label: link.capitalize, describedby: link } %> +
+
+ <% end %> +
+
+
+
+ +
+ <%= f.submit "Save", class: "btn btn-primary btn-lg" %> + <%=link_to "Cancel", admin_speakers_path, class: "btn btn-outline-primary btn-lg" %> +
+<% end %> diff --git a/app/views/admin/speakers/edit.html.erb b/app/views/admin/speakers/edit.html.erb new file mode 100644 index 00000000..9a1cce65 --- /dev/null +++ b/app/views/admin/speakers/edit.html.erb @@ -0,0 +1,6 @@ +
+
+

Edit speaker

+ <%=render 'form', speaker: @speaker %> +
+
diff --git a/app/views/admin/speakers/index.html.erb b/app/views/admin/speakers/index.html.erb new file mode 100644 index 00000000..26fb4c83 --- /dev/null +++ b/app/views/admin/speakers/index.html.erb @@ -0,0 +1,51 @@ +

Speaker List

+
+
+ <%= link_to new_admin_speaker_path, class: "btn btn-primary btn-md mb-4" do %> + Create new Speaker + + <% end %> +
+
+ +
+
+
+ + + + + + + + + + + + + + + <% @speakers.each do |speaker| %> + + + + + + + + + + <% end %> + +
IdNameBioTaglineImageLinksActions
<%= speaker.id %><%= speaker.name %><%= truncate_html(speaker.bio, length: 70) %><%= speaker.tagline %> +
+ <%=image_tag speaker.image_url, class:"rounded-circle object-fit-cover", height: 53, width: 53, alt:"Profile photo for #{speaker.name}" %> +
+
+ <%= render "admin/speakers/index/speaker_links", speaker: speaker%> + + <%= link_to "Edit", edit_admin_speaker_path(speaker) %> +
+
+
+
diff --git a/app/views/admin/speakers/index/_speaker_links.html.erb b/app/views/admin/speakers/index/_speaker_links.html.erb new file mode 100644 index 00000000..568e1aac --- /dev/null +++ b/app/views/admin/speakers/index/_speaker_links.html.erb @@ -0,0 +1,25 @@ +<% if speaker.empty_links? %> + - +<% else %> + <% if speaker.github.present? %> + + <% end %> + <% if speaker.mastodon.present? %> + + <% end %> + <% if speaker.linkedin.present? %> + + <% end %> + <% if speaker.twitter.present? %> + + <% end %> + <% if speaker.website.present? %> + + <% end %> + <% if speaker.other.present? %> + + <% end %> +<% end %> + + + diff --git a/app/views/admin/speakers/new.html.erb b/app/views/admin/speakers/new.html.erb new file mode 100644 index 00000000..24c66f79 --- /dev/null +++ b/app/views/admin/speakers/new.html.erb @@ -0,0 +1,7 @@ +
+
+

New Speaker

+ + <%= render "form", speaker: @speaker %> +
+
diff --git a/app/views/admin/speakers/show.html.erb b/app/views/admin/speakers/show.html.erb new file mode 100644 index 00000000..71136389 --- /dev/null +++ b/app/views/admin/speakers/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @speaker %> + +
+ <%= link_to "Edit this speaker", edit_admin_speaker_path(@speaker) %> | + <%= link_to "Back to speakers", admin_speakers_path %> + + <%= button_to "Destroy this speaker", @speaker, method: :delete %> +
diff --git a/app/views/layouts/admin/_flashes.html.erb b/app/views/layouts/admin/_flashes.html.erb index 39c35b92..d6958919 100644 --- a/app/views/layouts/admin/_flashes.html.erb +++ b/app/views/layouts/admin/_flashes.html.erb @@ -1,5 +1,5 @@ <% if flash.any? %> -