├── log └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── tasks │ ├── .keep │ ├── db.rake │ └── kill_postgres_connections.rake └── json_web_token.rb ├── .ruby-version ├── app ├── graphql │ ├── types │ │ ├── .keep │ │ ├── role_type.rb │ │ ├── outquote_type.rb │ │ ├── profile_type.rb │ │ ├── auth_provider_email_input.rb │ │ ├── search_document_type.rb │ │ ├── comment_type.rb │ │ ├── mutation_type.rb │ │ ├── medium_type.rb │ │ ├── article_type.rb │ │ ├── section_type.rb │ │ ├── user_type.rb │ │ └── query_type.rb │ ├── mutations │ │ └── .keep │ ├── stuy_spec_api_schema.rb │ ├── resolvers │ │ ├── mutation_function.rb │ │ ├── article_query_function.rb │ │ ├── get_featured_article.rb │ │ ├── get_profile_by_user_and_role.rb │ │ ├── delete_article.rb │ │ ├── delete_section.rb │ │ ├── get_column_articles.rb │ │ ├── get_featured_subsection.rb │ │ ├── create_section.rb │ │ ├── get_latest_articles.rb │ │ ├── get_latest_unpublished_articles.rb │ │ ├── create_user.rb │ │ ├── get_featured_articles_by_section_id.rb │ │ ├── get_top_ranked_articles.rb │ │ ├── get_featured_articles_by_section_slug.rb │ │ ├── update_user.rb │ │ ├── create_medium.rb │ │ ├── create_article.rb │ │ └── update_article.rb │ └── authentication.rb ├── models │ ├── concerns │ │ └── .keep │ ├── outquote.rb │ ├── subscriber.rb │ ├── application_record.rb │ ├── authorship.rb │ ├── role.rb │ ├── profile.rb │ ├── comment.rb │ ├── medium.rb │ ├── section.rb │ ├── user.rb │ └── article.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── cms_controller.rb │ ├── client_app_controller.rb │ ├── graphql_controller.rb │ ├── roles_controller.rb │ ├── subscribers_controller.rb │ ├── application_controller.rb │ ├── users_controller.rb │ ├── sections_controller.rb │ ├── initial_controller.rb │ ├── authorships_controller.rb │ ├── profiles_controller.rb │ ├── outquotes_controller.rb │ ├── media_controller.rb │ ├── comments_controller.rb │ └── articles_controller.rb ├── helpers │ ├── initial_helper.rb │ └── articles_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ └── application_mailer.rb ├── assets │ ├── stylesheets │ │ ├── initial.css │ │ ├── articles.css │ │ └── scaffold.css │ └── javascripts │ │ ├── initial.js │ │ └── articles.js └── views │ ├── devise │ └── mailer │ │ ├── confirmation_instructions.html.erb │ │ └── reset_password_instructions.html.erb │ └── client_app │ └── index.html.erb ├── .rspec ├── .elasticbeanstalk └── config.yml ├── public ├── ads.txt ├── favicon.ico └── robots.txt ├── db ├── sample_photo.jpg └── migrate │ ├── 20170826061308_add_slug_to_user.rb │ ├── 20171119060337_rename_media_type.rb │ ├── 20170825202208_rename_is_draft.rb │ ├── 20170804210950_add_thumbnail_to_users.rb │ ├── 20170808033957_add_last_name_to_users.rb │ ├── 20170828183304_add_description_to_user.rb │ ├── 20170902194926_add_summary_to_articles.rb │ ├── 20180208000854_add_preview_to_articles.rb │ ├── 20170904052134_remove_username_from_user.rb │ ├── 20171211221219_add_permalink_to_sections.rb │ ├── 20170730200024_add_section_to_article.rb │ ├── 20170730204001_change_articles_slug.rb │ ├── 20170829032135_rename_name_to_firstname.rb │ ├── 20171008191007_add_is_visible_to_sections.rb │ ├── 20180124161100_add_index_to_subscribers.rb │ ├── 20180126190601_rename_user_roles_to_profiles.rb │ ├── 20170827004918_remove_comment_id_from_comments.rb │ ├── 20170828015408_add_published_at_to_comments.rb │ ├── 20170829032002_rename_nickname_to_username.rb │ ├── 20170829125043_remove_integer_from_comments.rb │ ├── 20170730205508_add_parent_section_to_sections.rb │ ├── 20171109210813_add_outquotes_to_articles.rb │ ├── 20171124015042_add_security_level_to_users.rb │ ├── 20170802181149_rename_parent_section_to_parent_id.rb │ ├── 20171109210835_remove_outquotes_from_articles.rb │ ├── 20180215150806_add_default_to_articles_is_published.rb │ ├── 20170825201413_add_rank.rb │ ├── 20170901152914_fix_media_foreign_keys.rb │ ├── 20171120054722_create_subscribers.rb │ ├── 20170827074544_create_roles.rb │ ├── 20171107211226_create_outquotes.rb │ ├── 20170827074658_create_user_roles.rb │ ├── 20180208021849_generate_article_previews.rb │ ├── 20170730193411_create_sections.rb │ ├── 20200209144504_add_profile_picture.rb │ ├── 20170802045342_create_authorships.rb │ ├── 20180209202255_change_comments_published_at_to_datetime.rb │ ├── 20170829124457_change_foreign_key_type_in_comments.rb │ ├── 20170901152440_add_attachment_attachment_to_media.rb │ ├── 20170814024225_create_comments.rb │ ├── 20180124130832_fix_devise_token_auth_issue.rb │ ├── 20170730190411_create_articles.rb │ ├── 20180208032553_generate_missing_user_slugs.rb │ ├── 20170802051859_create_media.rb │ ├── 20171211222546_generate_section_permalinks.rb │ ├── 20171124230849_create_pg_search_documents.rb │ ├── 20170730203331_create_friendly_id_slugs.rb │ ├── 20180129034322_change_media_user_id_to_profile_id.rb │ └── 20170731175931_devise_token_auth_create_users.rb ├── bin ├── rake ├── bundle ├── rails ├── spring ├── update └── setup ├── config ├── aws.yml ├── initializers │ ├── aws.rb │ ├── omniauth.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── paperclip.rb │ ├── backtrace_silencers.rb │ ├── postgresql_database_tasks.rb │ ├── wrap_parameters.rb │ ├── cors.rb │ ├── inflections.rb │ ├── devise.rb │ ├── devise_token_auth.rb │ └── friendly_id.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── locales │ ├── devise.en.yml │ └── en.yml ├── secrets.yml ├── routes.rb ├── deploy.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── application.rb ├── deploy │ └── production.rb ├── puma.rb └── database.yml ├── spec ├── controllers │ ├── initial_controller_spec.rb │ ├── roles_controller_spec.rb │ ├── media_controller_spec.rb │ ├── articles_controller_spec.rb │ ├── comments_controller_spec.rb │ ├── profiles_controller_spec.rb │ └── sections_controller_spec.rb ├── models │ ├── role_spec.rb │ ├── article_spec.rb │ ├── comment_spec.rb │ ├── medium_spec.rb │ ├── profile_spec.rb │ ├── section_spec.rb │ ├── authorship_spec.rb │ ├── outquote_spec.rb │ └── subscriber_spec.rb ├── requests │ ├── media_spec.rb │ ├── roles_spec.rb │ ├── sections_spec.rb │ ├── profiles_spec.rb │ ├── subscribers_spec.rb │ ├── authorships_spec.rb │ ├── outquotes_spec.rb │ ├── comments_spec.rb │ └── articles_spec.rb ├── helpers │ ├── initial_helper_spec.rb │ └── articles_helper_spec.rb ├── routing │ ├── media_routing_spec.rb │ ├── roles_routing_spec.rb │ ├── articles_routing_spec.rb │ ├── comments_routing_spec.rb │ ├── profiles_routing_spec.rb │ ├── sections_routing_spec.rb │ ├── outquotes_routing_spec.rb │ ├── authorships_routing_spec.rb │ └── subscribers_routing_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── config.ru ├── .ebextensions └── 01_files.config ├── Rakefile ├── Dockerfile ├── docker-compose.yml ├── Capfile ├── .gitignore ├── LICENSE ├── .circleci └── config.yml └── Gemfile /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.0 2 | -------------------------------------------------------------------------------- /app/graphql/types/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/mutations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/initial_helper.rb: -------------------------------------------------------------------------------- 1 | module InitialHelper 2 | end 3 | -------------------------------------------------------------------------------- /.elasticbeanstalk/config.yml: -------------------------------------------------------------------------------- 1 | deploy: 2 | artifact: api.zip 3 | -------------------------------------------------------------------------------- /app/helpers/articles_helper.rb: -------------------------------------------------------------------------------- 1 | module ArticlesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-6227330768557696, DIRECT, f08c47fec0942fa0 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuyspec/stuyspec-api/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/models/outquote.rb: -------------------------------------------------------------------------------- 1 | class Outquote < ApplicationRecord 2 | belongs_to :article 3 | end 4 | -------------------------------------------------------------------------------- /db/sample_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuyspec/stuyspec-api/HEAD/db/sample_photo.jpg -------------------------------------------------------------------------------- /app/models/subscriber.rb: -------------------------------------------------------------------------------- 1 | class Subscriber < ApplicationRecord 2 | validates :email, uniqueness: true 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/authorship.rb: -------------------------------------------------------------------------------- 1 | class Authorship < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :article 4 | end 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/models/role.rb: -------------------------------------------------------------------------------- 1 | class Role < ApplicationRecord 2 | has_many :profiles 3 | has_many :users, through: :profiles 4 | end 5 | -------------------------------------------------------------------------------- /config/aws.yml: -------------------------------------------------------------------------------- 1 | access_key_id: <%= ENV['S3_ACCESS_KEY_ID'] %> 2 | secret_access_key: <%= ENV['S3_SECRET_ACCESS_KEY'] %> 3 | 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/aws.rb: -------------------------------------------------------------------------------- 1 | Aws::VERSION = Gem.loaded_specs["aws-sdk"].version 2 | 3 | S3_CREDENTIALS = Rails.root.join("config/aws.yml") -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/controllers/initial_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe InitialController, type: :controller do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /app/controllers/cms_controller.rb: -------------------------------------------------------------------------------- 1 | class CmsController < ActionController::Base 2 | def index 3 | render file: 'public/cms/index.html' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/stuy_spec_api_schema.rb: -------------------------------------------------------------------------------- 1 | StuySpecApiSchema = GraphQL::Schema.define do 2 | mutation(Types::MutationType) 3 | query(Types::QueryType) 4 | end 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | 5 | -------------------------------------------------------------------------------- /app/graphql/resolvers/mutation_function.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::MutationFunction < GraphQL::Function 2 | 3 | #insert mutation specific logic here 4 | 5 | end 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/models/role_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Role, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/resolvers/article_query_function.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::ArticleQueryFunction < GraphQL::Function 2 | 3 | # Insert shared query logic here 4 | 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/models/article_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Article, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Comment, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/medium_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Medium, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/profile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Profile, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/section_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Section, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/initial.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /app/models/profile.rb: -------------------------------------------------------------------------------- 1 | class Profile < ApplicationRecord 2 | belongs_to :user, dependent: :destroy 3 | belongs_to :role, dependent: :destroy 4 | has_many :media 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170826061308_add_slug_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :slug, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/authorship_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Authorship, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/outquote_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Outquote, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/subscriber_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Subscriber, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/articles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/initial.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /db/migrate/20171119060337_rename_media_type.rb: -------------------------------------------------------------------------------- 1 | class RenameMediaType < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :media, :type, :media_type 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/articles.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | provider :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET'], scope: 'email,profile' 3 | end 4 | -------------------------------------------------------------------------------- /db/migrate/20170825202208_rename_is_draft.rb: -------------------------------------------------------------------------------- 1 | class RenameIsDraft < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :articles, :is_draft, :is_published 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170804210950_add_thumbnail_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddThumbnailToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :thumbnail, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170808033957_add_last_name_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLastNameToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :last_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170828183304_add_description_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :description, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170902194926_add_summary_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddSummaryToArticles < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :articles, :summary, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180208000854_add_preview_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddPreviewToArticles < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :articles, :preview, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/role_type.rb: -------------------------------------------------------------------------------- 1 | Types::RoleType = GraphQL::ObjectType.define do 2 | name "Role" 3 | field :id, !types.ID 4 | field :title, !types.String 5 | field :slug, !types.String 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170904052134_remove_username_from_user.rb: -------------------------------------------------------------------------------- 1 | class RemoveUsernameFromUser < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :username, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171211221219_add_permalink_to_sections.rb: -------------------------------------------------------------------------------- 1 | class AddPermalinkToSections < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :sections, :permalink, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20170730200024_add_section_to_article.rb: -------------------------------------------------------------------------------- 1 | class AddSectionToArticle < ActiveRecord::Migration[5.1] 2 | def change 3 | add_reference :articles, :section, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170730204001_change_articles_slug.rb: -------------------------------------------------------------------------------- 1 | class ChangeArticlesSlug < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :articles, :slug, :string, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170829032135_rename_name_to_firstname.rb: -------------------------------------------------------------------------------- 1 | class RenameNameToFirstname < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :users, :name, :first_name 4 | end 5 | end 6 | 7 | -------------------------------------------------------------------------------- /db/migrate/20171008191007_add_is_visible_to_sections.rb: -------------------------------------------------------------------------------- 1 | class AddIsVisibleToSections < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :sections, :is_visible, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180124161100_add_index_to_subscribers.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToSubscribers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :subscribers, :email, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180126190601_rename_user_roles_to_profiles.rb: -------------------------------------------------------------------------------- 1 | class RenameUserRolesToProfiles < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_table "user_roles", "profiles" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170827004918_remove_comment_id_from_comments.rb: -------------------------------------------------------------------------------- 1 | class RemoveCommentIdFromComments < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :comments, :comment_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170828015408_add_published_at_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddPublishedAtToComments < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :comments, :published_at, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170829032002_rename_nickname_to_username.rb: -------------------------------------------------------------------------------- 1 | class RenameNicknameToUsername < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :users, :nickname, :username 4 | end 5 | end 6 | 7 | -------------------------------------------------------------------------------- /db/migrate/20170829125043_remove_integer_from_comments.rb: -------------------------------------------------------------------------------- 1 | class RemoveIntegerFromComments < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :comments, :integer, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170730205508_add_parent_section_to_sections.rb: -------------------------------------------------------------------------------- 1 | class AddParentSectionToSections < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :sections, :parent_section, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171109210813_add_outquotes_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddOutquotesToArticles < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :articles, :outquotes, :string, array: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.ebextensions/01_files.config: -------------------------------------------------------------------------------- 1 | files: 2 | "/etc/nginx/conf.d/proxy.conf" : 3 | mode: "000755" 4 | owner: root 5 | group: root 6 | content: | 7 | client_max_body_size 20M; 8 | -------------------------------------------------------------------------------- /app/graphql/types/outquote_type.rb: -------------------------------------------------------------------------------- 1 | Types::OutquoteType = GraphQL::ObjectType.define do 2 | name "Outquote" 3 | field :id, !types.ID 4 | field :text, !types.String 5 | field :article, !Types::ArticleType 6 | end 7 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: stuy-spec-api_production 11 | -------------------------------------------------------------------------------- /db/migrate/20171124015042_add_security_level_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSecurityLevelToUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :security_level, :integer, :default => 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170802181149_rename_parent_section_to_parent_id.rb: -------------------------------------------------------------------------------- 1 | class RenameParentSectionToParentId < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :sections, :parent_section, :parent_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171109210835_remove_outquotes_from_articles.rb: -------------------------------------------------------------------------------- 1 | class RemoveOutquotesFromArticles < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :articles, :outquotes, :string, array: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180215150806_add_default_to_articles_is_published.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultToArticlesIsPublished < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_default :articles, :is_published, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /db/migrate/20170825201413_add_rank.rb: -------------------------------------------------------------------------------- 1 | class AddRank < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :articles, :rank, :integer, :default => 1 4 | add_column :sections, :rank, :integer, :default => 1 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc 'Drop, create, load then seed the development database' 3 | task reseed: [ 'db:drop:_unsafe', 'db:create', 'db:schema:load', 'db:seed' ] do 4 | puts 'Reseeding completed.' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /db/migrate/20170901152914_fix_media_foreign_keys.rb: -------------------------------------------------------------------------------- 1 | class FixMediaForeignKeys < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :media, :user_id_id, :user_id 4 | rename_column :media, :article_id_id, :article_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171120054722_create_subscribers.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscribers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :subscribers do |t| 4 | t.string :email 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/graphql/types/profile_type.rb: -------------------------------------------------------------------------------- 1 | Types::ProfileType = GraphQL::ObjectType.define do 2 | name "Profile" 3 | field :id, !types.ID 4 | field :role, !Types::RoleType 5 | field :user, !Types::UserType 6 | field :media, !types[Types::MediumType] 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/paperclip.rb: -------------------------------------------------------------------------------- 1 | Paperclip::Attachment.default_options[:url] = ':s3_domain_url' 2 | Paperclip::Attachment.default_options[:path] = '/:class/:attachment/:id_partition/:style/:filename' 3 | Paperclip::DataUriAdapter.register # for paperclip 5.2+ 4 | -------------------------------------------------------------------------------- /db/migrate/20170827074544_create_roles.rb: -------------------------------------------------------------------------------- 1 | class CreateRoles < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :roles do |t| 4 | t.text :title 5 | t.text :slug 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ApplicationRecord 2 | belongs_to :article 3 | belongs_to :user 4 | 5 | def is_author?(user) 6 | user.id == self.user_id 7 | end 8 | 9 | def publish 10 | self.touch(:published_at) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20171107211226_create_outquotes.rb: -------------------------------------------------------------------------------- 1 | class CreateOutquotes < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :outquotes do |t| 4 | t.integer :article_id 5 | t.text :text 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170827074658_create_user_roles.rb: -------------------------------------------------------------------------------- 1 | class CreateUserRoles < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :user_roles do |t| 4 | t.integer :user_id 5 | t.integer :role_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/types/auth_provider_email_input.rb: -------------------------------------------------------------------------------- 1 | Types::AuthProviderEmailInput = GraphQL::InputObjectType.define do 2 | name 'AUTH_PROVIDER_EMAIL' 3 | 4 | argument :email, !types.String 5 | argument :password, !types.String 6 | argument :password_confirmation, !types.String 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/search_document_type.rb: -------------------------------------------------------------------------------- 1 | Types::SearchDocumentType = GraphQL::ObjectType.define do 2 | name 'SearchDocument' 3 | field :id, !types.ID 4 | field :content, !types.String 5 | field :searchable_type, !types.String 6 | field :searchable, !Types::ArticleType 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/comment_type.rb: -------------------------------------------------------------------------------- 1 | Types::CommentType = GraphQL::ObjectType.define do 2 | name "Comment" 3 | field :id, !types.ID 4 | field :content, !types.String 5 | field :created_at, !types.String 6 | field :published_at, types.String 7 | field :user, !Types::UserType 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180208021849_generate_article_previews.rb: -------------------------------------------------------------------------------- 1 | class GenerateArticlePreviews < ActiveRecord::Migration[5.1] 2 | def up 3 | Article.reset_column_information 4 | Article.all.each do |article| 5 | article.update(preview: article.generate_preview) 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /db/migrate/20170730193411_create_sections.rb: -------------------------------------------------------------------------------- 1 | class CreateSections < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :sections do |t| 4 | t.text :name 5 | t.text :description 6 | t.text :slug 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200209144504_add_profile_picture.rb: -------------------------------------------------------------------------------- 1 | class AddProfilePicture < ActiveRecord::Migration[5.1] 2 | def self.up 3 | add_attachment :users, :profile_picture 4 | end 5 | 6 | def self.down 7 | remove_attachment :users, :profile_picture 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/requests/media_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Media", type: :request do 4 | describe "GET /media" do 5 | it "works! (now write some real specs)" do 6 | get media_path 7 | expect(response).to have_http_status(200) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/requests/roles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Roles", type: :request do 4 | describe "GET /roles" do 5 | it "works! (now write some real specs)" do 6 | get roles_path 7 | expect(response).to have_http_status(200) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170802045342_create_authorships.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthorships < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :authorships do |t| 4 | t.belongs_to :article, index: true 5 | t.belongs_to :user, index: true 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180209202255_change_comments_published_at_to_datetime.rb: -------------------------------------------------------------------------------- 1 | class ChangeCommentsPublishedAtToDatetime < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :comments, :published_at, "timestamp without time zone USING CAST(published_at AS timestamp without time zone)" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/requests/sections_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Sections", type: :request do 4 | describe "GET /sections" do 5 | it "works! (now write some real specs)" do 6 | get sections_path 7 | expect(response).to have_http_status(200) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/requests/profiles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Profiles", type: :request do 4 | describe "GET /profiles" do 5 | it "has the right types" do 6 | get profiles_path, params: { limit: 10 } 7 | expect(response).to have_http_status(200) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/requests/subscribers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Subscribers", type: :request do 4 | describe "GET /subscribers" do 5 | it "works! (now write some real specs)" do 6 | get subscribers_path 7 | expect(response).to have_http_status(200) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4.2 2 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs 3 | RUN mkdir /stuy-spec-api 4 | WORKDIR /stuy-spec-api 5 | ADD Gemfile /stuy-spec-api/Gemfile 6 | ADD Gemfile.lock /stuy-spec-api/Gemfile.lock 7 | RUN gem install bundler 8 | RUN bundle install 9 | ADD . /stuy-spec-api 10 | -------------------------------------------------------------------------------- /db/migrate/20170829124457_change_foreign_key_type_in_comments.rb: -------------------------------------------------------------------------------- 1 | class ChangeForeignKeyTypeInComments < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :comments, :article_id, 'integer USING CAST(article_id AS integer)' 4 | change_column :comments, :user_id, 'integer USING CAST(user_id AS integer)' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170901152440_add_attachment_attachment_to_media.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentAttachmentToMedia < ActiveRecord::Migration[5.1] 2 | def self.up 3 | change_table :media do |t| 4 | t.attachment :attachment 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :media, :attachment 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres 5 | web: 6 | environment: 7 | - PG_HOST=db 8 | build: . 9 | command: bundle exec rails s -p 3000 -b '0.0.0.0' 10 | volumes: 11 | - .:/stuy-spec-api 12 | ports: 13 | - "3000:3000" 14 | depends_on: 15 | - db 16 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t(:welcome).capitalize + ' ' + @email %>!

2 | 3 |

<%= t '.confirm_link_msg' %>

4 | 5 |

<%= link_to t('.confirm_account_link'), confirmation_url(@resource, {confirmation_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url']}).html_safe %>

6 | -------------------------------------------------------------------------------- /db/migrate/20170814024225_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :comments do |t| 4 | t.string :article_id 5 | t.string :comment_id 6 | t.string :integer 7 | t.string :user_id 8 | t.text :content 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180124130832_fix_devise_token_auth_issue.rb: -------------------------------------------------------------------------------- 1 | # https://github.com/lynndylanhurley/devise_token_auth/issues/121 2 | 3 | class FixDeviseTokenAuthIssue < ActiveRecord::Migration[5.1] 4 | def change 5 | User.find_each do |user| 6 | user.uid = user.email 7 | user.tokens = nil 8 | user.save! 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170730190411_create_articles.rb: -------------------------------------------------------------------------------- 1 | class CreateArticles < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :articles do |t| 4 | t.text :title 5 | t.text :slug 6 | t.text :content 7 | t.integer :volume 8 | t.integer :issue 9 | t.boolean :is_draft 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180208032553_generate_missing_user_slugs.rb: -------------------------------------------------------------------------------- 1 | class GenerateMissingUserSlugs < ActiveRecord::Migration[5.1] 2 | def up 3 | User.all.each do |user| 4 | if user.slug.nil? and not (user.first_name.nil? or user.last_name.nil?) 5 | user.update_attributes :slug => (user.first_name + user.last_name).parameterize 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /spec/requests/authorships_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Authorships", type: :request do 4 | describe "GET /authorships" do 5 | it 'returns correct types' do 6 | get authorships_path 7 | expect_json_types( 8 | '?', 9 | user_id: :integer, 10 | article_id: :integer 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/requests/outquotes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Outquotes", type: :request do 4 | describe "GET /outquotes" do 5 | it "works! (now write some real specs)" do 6 | get outquotes_path, params: { limit: 10 } 7 | expect_json_types( 8 | '?', 9 | text: :string, 10 | article_id: :integer 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20170802051859_create_media.rb: -------------------------------------------------------------------------------- 1 | class CreateMedia < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :media do |t| 4 | t.belongs_to :user_id 5 | t.belongs_to :article_id 6 | t.string :url 7 | t.string :title 8 | t.text :caption 9 | t.boolean :is_featured 10 | t.string :type 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20171211222546_generate_section_permalinks.rb: -------------------------------------------------------------------------------- 1 | class GenerateSectionPermalinks < ActiveRecord::Migration[5.1] 2 | def up 3 | Section.all.each do |section| 4 | permalink = '/' + section.slug 5 | if !section.parent_id.nil? 6 | permalink = '/' + Section.find(section.parent_id).slug + permalink 7 | end 8 | section.update_attributes :permalink => permalink 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tasks/kill_postgres_connections.rake: -------------------------------------------------------------------------------- 1 | # lib/tasks/kill_postgres_connections.rake 2 | task :kill_postgres_connections => :environment do 3 | db_name = "#{File.basename(Rails.root)}_#{Rails.env}" 4 | sh = < :kill_postgres_connections 16 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

<%= t(:hello).capitalize %> <%= @resource.email %>!

2 | 3 |

<%= t '.request_reset_link_msg' %>

4 | 5 |

<%= link_to t('.password_change_link'), edit_password_url(@resource, reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s).html_safe %>

6 | 7 |

<%= t '.ignore_mail_msg' %>

8 |

<%= t '.no_changes_msg' %>

-------------------------------------------------------------------------------- /lib/json_web_token.rb: -------------------------------------------------------------------------------- 1 | class JsonWebToken 2 | class << self 3 | def encode(payload, exp = 24.hours.from_now) 4 | payload[:exp] = exp.to_i 5 | JWT.encode(payload, Rails.application.secrets.secret_key_base) 6 | end 7 | def decode(token) 8 | body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0] 9 | HashWithIndifferentAccess.new body 10 | rescue 11 | nil 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/postgresql_database_tasks.rb: -------------------------------------------------------------------------------- 1 | # config/initializers/postgresql_database_tasks.rb 2 | module ActiveRecord 3 | module Tasks 4 | class PostgreSQLDatabaseTasks 5 | def drop 6 | establish_master_connection 7 | connection.select_all "select pg_terminate_backend(pg_stat_activity.pid) from pg_stat_activity where datname='#{configuration['database']}' AND state='idle';" 8 | connection.drop_database configuration['database'] 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/helpers/initial_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # Specs in this file have access to a helper object that includes 4 | # the InitialHelper. For example: 5 | # 6 | # describe InitialHelper do 7 | # describe "string concat" do 8 | # it "concats two strings with spaces" do 9 | # expect(helper.concat_strings("this","that")).to eq("this that") 10 | # end 11 | # end 12 | # end 13 | RSpec.describe InitialHelper, type: :helper do 14 | pending "add some examples to (or delete) #{__FILE__}" 15 | end 16 | -------------------------------------------------------------------------------- /spec/helpers/articles_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # Specs in this file have access to a helper object that includes 4 | # the ArticlesHelper. For example: 5 | # 6 | # describe ArticlesHelper do 7 | # describe "string concat" do 8 | # it "concats two strings with spaces" do 9 | # expect(helper.concat_strings("this","that")).to eq("this that") 10 | # end 11 | # end 12 | # end 13 | RSpec.describe ArticlesHelper, type: :helper do 14 | pending "add some examples to (or delete) #{__FILE__}" 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /db/migrate/20171124230849_create_pg_search_documents.rb: -------------------------------------------------------------------------------- 1 | class CreatePgSearchDocuments < ActiveRecord::Migration[5.1] 2 | def self.up 3 | say_with_time("Creating table for pg_search multisearch") do 4 | create_table :pg_search_documents do |t| 5 | t.text :content 6 | t.belongs_to :searchable, :polymorphic => true, :index => true 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | 12 | def self.down 13 | say_with_time("Dropping table for pg_search multisearch") do 14 | drop_table :pg_search_documents 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | Types::MutationType = GraphQL::ObjectType.define do 2 | name "Mutation" 3 | 4 | field :createArticle, function: Resolvers::CreateArticle.new 5 | field :createMedium, function: Resolvers::CreateMedium.new 6 | field :updateArticle, function: Resolvers::UpdateArticle.new 7 | field :createUser, function: Resolvers::CreateUser.new 8 | field :createSection, function: Resolvers::CreateSection.new 9 | field :deleteArticle, function: Resolvers::DeleteArticle.new 10 | field :deleteSection, function: Resolvers::DeleteSection.new 11 | field :updateUser, function: Resolvers::UpdateUser.new 12 | end 13 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_featured_article.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetFeaturedArticle < Resolvers::ArticleQueryFunction 2 | 3 | # return type from the mutation 4 | type Types::ArticleType 5 | 6 | # the mutation method 7 | # _obj - is parent object, which in this case is nil 8 | # _args - are the arguments passed 9 | # _ctx - is the GraphQL context (which would be discussed later) 10 | def call(_obj, _args, _ctx) 11 | 12 | articles = Article 13 | .order_by_rank # already JOINS on Section 14 | .where("sections.name != 'News'") 15 | .joins(:media) 16 | .published 17 | 18 | return articles.first 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # These variables override Devise's default copy. Defaults: 2 | # - https://github.com/plataformatec/devise/blob/master/config/locales/en.yml 3 | # - https://github.com/lynndylanhurley/devise_token_auth/blob/123cfb730ebaf4e2acc124edd39e68816cb0b8cf/config/locales/en.yml 4 | 5 | en: 6 | devise: 7 | mailer: 8 | confirmation_instructions: 9 | subject: "The Spectator Web Department: Confirmation Instructions" 10 | confirm_link_msg: "You are receiving this e-mail either because you registered on our website or your first creations have just been added to the website." 11 | confirm_account_link: "Confirm your account" -------------------------------------------------------------------------------- /db/migrate/20170730203331_create_friendly_id_slugs.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendlyIdSlugs < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :friendly_id_slugs do |t| 4 | t.string :slug, :null => false 5 | t.integer :sluggable_id, :null => false 6 | t.string :sluggable_type, :limit => 50 7 | t.string :scope 8 | t.datetime :created_at 9 | end 10 | add_index :friendly_id_slugs, :sluggable_id 11 | add_index :friendly_id_slugs, [:slug, :sluggable_type] 12 | add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :unique => true 13 | add_index :friendly_id_slugs, :sluggable_type 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_profile_by_user_and_role.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetProfileByUserAndRole < GraphQL::Function 2 | 3 | argument :user_slug, !types.String 4 | argument :role_slug, !types.String 5 | 6 | # return type from the mutation 7 | type Types::ProfileType 8 | 9 | # the mutation method 10 | # _obj - is parent object, which in this case is nil 11 | # args - are the arguments passed 12 | # _ctx - is the GraphQL context (which would be discussed later) 13 | def call(_obj, args, _ctx) 14 | return Profile 15 | .joins(:user, :role) 16 | .find_by( 17 | "users.slug = ? AND roles.slug = ?", 18 | args["user_slug"], 19 | args["role_slug"] 20 | ) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/medium.rb: -------------------------------------------------------------------------------- 1 | class Medium < ApplicationRecord 2 | belongs_to :article, optional: true 3 | belongs_to :profile 4 | has_one :user, through: :profile 5 | has_attached_file :attachment, 6 | storage: :s3, 7 | styles: { medium: "300x300>", thumb: "100x100>" }, 8 | default_url: "/images/:style/missing.png" 9 | 10 | validates_attachment :attachment, 11 | content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] } 12 | 13 | def attachment_url 14 | attachment.url 15 | end 16 | def medium_attachment_url 17 | attachment.url :medium 18 | end 19 | def thumb_attachment_url 20 | attachment.url :thumb 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | Devise.setup do |config| 2 | # The secret key used by Devise. Devise uses this key to generate 3 | # random tokens. Changing this key will render invalid all existing 4 | # confirmation, reset password and unlock tokens in the database. 5 | # Devise will use the `secret_key_base` as its `secret_key` 6 | # by default. You can change it below and use your own secret key. 7 | config.secret_key = ENV['DEVISE_SECRET_KEY'] 8 | 9 | config.mailer_sender = 'web@stuyspec.com' 10 | 11 | # Configure the class responsible to send e-mails. 12 | config.mailer = 'Devise::Mailer' 13 | 14 | # Configure the parent class responsible to send e-mails. 15 | config.parent_mailer = 'ActionMailer::Base' 16 | 17 | end 18 | -------------------------------------------------------------------------------- /app/graphql/resolvers/delete_article.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::DeleteArticle < Resolvers::MutationFunction 2 | # arguments passed as "args" 3 | argument :id, !types.ID 4 | 5 | # return type from the mutation 6 | type Types::ArticleType 7 | 8 | # the mutation method 9 | # _obj - is parent object, which in this case is nil 10 | # args - are the arguments passed 11 | # _ctx - is the GraphQL context (which would be discussed later) 12 | def call(_obj, args, ctx) 13 | if !Authentication::admin_is_valid(ctx) 14 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 15 | end 16 | article = Article.find(args["id"]).destroy! 17 | Authentication::generate_new_header(ctx) if article 18 | return article 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/graphql/resolvers/delete_section.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::DeleteSection < Resolvers::MutationFunction 2 | # arguments passed as "args" 3 | argument :id, !types.ID 4 | 5 | # return type from the mutation 6 | type Types::SectionType 7 | 8 | # the mutation method 9 | # _obj - is parent object, which in this case is nil 10 | # args - are the arguments passed 11 | # _ctx - is the GraphQL context (which would be discussed later) 12 | def call(_obj, args, ctx) 13 | if !Authentication::admin_is_valid(ctx) 14 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 15 | end 16 | section = Section.find(args["id"]).destroy! 17 | Authentication::generate_new_header(ctx) if section 18 | return section 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require "capistrano/setup" 3 | 4 | # Include default deployment tasks 5 | require "capistrano/deploy" 6 | 7 | require "capistrano/scm/git" 8 | install_plugin Capistrano::SCM::Git 9 | 10 | # Include tasks from other gems included in your Gemfile 11 | # 12 | # For documentation on these, see for example: 13 | # 14 | # https://github.com/capistrano/rvm 15 | # https://github.com/capistrano/bundler 16 | # https://github.com/capistrano/rails 17 | # https://github.com/capistrano/passenger 18 | # 19 | require "capistrano/rvm" 20 | require "capistrano/rails" 21 | require "capistrano/passenger" 22 | 23 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 24 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } 25 | -------------------------------------------------------------------------------- /app/graphql/types/medium_type.rb: -------------------------------------------------------------------------------- 1 | Types::MediumType = GraphQL::ObjectType.define do 2 | name "Medium" 3 | field :id, !types.ID 4 | field :created_at, !types.String 5 | field :profile, !Types::ProfileType 6 | field :user, !Types::UserType 7 | field :title, !types.String 8 | field :media_type do 9 | type !types.String 10 | description 'Currently supports "illustration" and "photo".' 11 | end 12 | field :is_featured do 13 | type !types.Boolean 14 | description 'Whether the medium will be shown at the top of its article.' 15 | end 16 | field :caption, types.String 17 | field :article, Types::ArticleType 18 | field :attachment_url, !types.String 19 | field :medium_attachment_url, !types.String 20 | field :thumb_attachment_url, !types.String 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/models/section.rb: -------------------------------------------------------------------------------- 1 | class Section < ApplicationRecord 2 | before_create :add_permalink 3 | extend FriendlyId 4 | friendly_id :name, use: :slugged 5 | has_many :articles, dependent: :destroy 6 | belongs_to :parent_section, 7 | class_name: 'Section', 8 | foreign_key: "parent_id", 9 | dependent: :destroy, 10 | optional: true 11 | has_many :subsections, class_name: 'Section', foreign_key: "parent_id" 12 | 13 | def add_permalink 14 | permalink = "/#{self.slug}" 15 | parent_tracker = self.parent_section 16 | while (!parent_tracker.nil?) 17 | permalink = "/#{parent_tracker.slug}#{permalink}" 18 | parent_tracker = parent_tracker.parent_section 19 | end 20 | self.permalink = permalink 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /spec/routing/media_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe MediaController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/media").to route_to("media#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/media/1").to route_to("media#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/media").to route_to("media#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/media/1").to route_to("media#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/media/1").to route_to("media#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/media/1").to route_to("media#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/roles_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe RolesController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/roles").to route_to("roles#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/roles/1").to route_to("roles#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/roles").to route_to("roles#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/roles/1").to route_to("roles#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/roles/1").to route_to("roles#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/roles/1").to route_to("roles#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/articles_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe ArticlesController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/articles").to route_to("articles#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/articles/1").to route_to("articles#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/articles").to route_to("articles#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/articles/1").to route_to("articles#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/articles/1").to route_to("articles#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/articles/1").to route_to("articles#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/comments_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe CommentsController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/comments").to route_to("comments#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/comments/1").to route_to("comments#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/comments").to route_to("comments#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/comments/1").to route_to("comments#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/comments/1").to route_to("comments#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/comments/1").to route_to("comments#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/profiles_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe ProfilesController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/profiles").to route_to("profiles#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/profiles/1").to route_to("profiles#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/profiles").to route_to("profiles#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/profiles/1").to route_to("profiles#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/profiles/1").to route_to("profiles#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/profiles/1").to route_to("profiles#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/sections_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SectionsController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/sections").to route_to("sections#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/sections/1").to route_to("sections#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/sections").to route_to("sections#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/sections/1").to route_to("sections#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/sections/1").to route_to("sections#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/sections/1").to route_to("sections#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_column_articles.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetColumnArticles < Resolvers::ArticleQueryFunction 2 | 3 | # return type from the mutation 4 | type types[Types::ArticleType] 5 | 6 | # the mutation method 7 | # _obj - is parent object, which in this case is nil 8 | # _args - are the arguments passed 9 | # _ctx - is the GraphQL context (which would be discussed later) 10 | def call(_obj, _args, _ctx) 11 | main_left_column_articles = Article.order_by_rank.published.offset(6).first(3) 12 | left_column_articles = [*main_left_column_articles] 13 | right_column_articles = 14 | Article 15 | .order_by_rank 16 | .published 17 | .offset(6) 18 | .where.not(id: left_column_articles) 19 | .first(2) 20 | return [*left_column_articles, *right_column_articles] 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /app/controllers/client_app_controller.rb: -------------------------------------------------------------------------------- 1 | class ClientAppController < ActionController::Base 2 | def index 3 | article = Article.find_by(slug: params[:slug]) 4 | @title = article&.title || "The Stuyvesant Spectator" 5 | @image = article&.media&.first&.attachment_url || "https://s3.amazonaws.com/stuyspec-images/the_logo.png" 6 | @description = article&.preview || "The Stuyvesant Spectator is a newspaper published by Stuyvesant High School students every two weeks."\ 7 | "It contains sections such as news, features, opinions, arts & entertainment, humor, sports, photography, art, layout, copy, "\ 8 | "business, and web. With such a wide range of topics, readers would never run out of reading material, which makes this "\ 9 | "newspaper awesome!" 10 | @url = (params[:section] and params[:slug]) ? "https://stuyspec.com/#{params[:section]}/#{params[:slug]}" 11 | : "https://stuyspec.com" 12 | render template: "client_app/index", status: :ok 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/routing/outquotes_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe OutquotesController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/outquotes").to route_to("outquotes#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/outquotes/1").to route_to("outquotes#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/outquotes").to route_to("outquotes#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/outquotes/1").to route_to("outquotes#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/outquotes/1").to route_to("outquotes#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/outquotes/1").to route_to("outquotes#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_featured_subsection.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetFeaturedSubsection < GraphQL::Function 2 | 3 | argument :section_id, !types.ID 4 | # return type from the mutation 5 | type Types::SectionType 6 | 7 | # the mutation method 8 | # _obj - is parent object, which in this case is nil 9 | # _args - are the arguments passed 10 | # _ctx - is the GraphQL context (which would be discussed later) 11 | def call(_obj, args, _ctx) 12 | section = Section.find(args['section_id']) 13 | if section.slug == '10-31-terror-attack' 14 | return Section.find_by(slug: 'creative-responses') 15 | elsif section.slug == 'news' 16 | return Section.find_by(slug: 'black-lives-matter') 17 | elsif section.slug == 'ae' 18 | return Section.find_by(slug: 'music') 19 | elsif section.slug == 'opinions' 20 | return Section.find_by(slug: 'financial-literacy') 21 | end 22 | subsections = Section.where(parent_id: section.id) 23 | return subsections[0] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/routing/authorships_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe AuthorshipsController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/authorships").to route_to("authorships#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/authorships/1").to route_to("authorships#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/authorships").to route_to("authorships#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/authorships/1").to route_to("authorships#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/authorships/1").to route_to("authorships#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/authorships/1").to route_to("authorships#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/routing/subscribers_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe SubscribersController, type: :routing do 4 | describe "routing" do 5 | 6 | it "routes to #index" do 7 | expect(:get => "/subscribers").to route_to("subscribers#index") 8 | end 9 | 10 | 11 | it "routes to #show" do 12 | expect(:get => "/subscribers/1").to route_to("subscribers#show", :id => "1") 13 | end 14 | 15 | 16 | it "routes to #create" do 17 | expect(:post => "/subscribers").to route_to("subscribers#create") 18 | end 19 | 20 | it "routes to #update via PUT" do 21 | expect(:put => "/subscribers/1").to route_to("subscribers#update", :id => "1") 22 | end 23 | 24 | it "routes to #update via PATCH" do 25 | expect(:patch => "/subscribers/1").to route_to("subscribers#update", :id => "1") 26 | end 27 | 28 | it "routes to #destroy" do 29 | expect(:delete => "/subscribers/1").to route_to("subscribers#destroy", :id => "1") 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/graphql/types/article_type.rb: -------------------------------------------------------------------------------- 1 | Types::ArticleType = GraphQL::ObjectType.define do 2 | name "Article" 3 | 4 | field :id, !types.ID 5 | field :title, !types.String 6 | field :slug, !types.String 7 | field :content, !types.String 8 | field :preview, types.String 9 | field :volume, !types.Int 10 | field :issue, !types.Int 11 | field :section, !Types::SectionType 12 | field :comments, types[!Types::CommentType] 13 | field :contributors, types[!Types::UserType] 14 | field :outquotes, types[Types::OutquoteType] 15 | field :media, types[!Types::MediumType] 16 | 17 | field :published_comments, types[!Types::CommentType] do 18 | resolve -> (obj, args, ctx) { 19 | obj.comments.where.not(published_at: nil) 20 | } 21 | end 22 | 23 | # created_at must be in this specific format for it to be readable, as a 24 | # timestamp, across all browsers. 25 | field :created_at, types.String do 26 | resolve -> (obj, args, ctx) { 27 | obj.created_at.strftime('%Y/%m/%d %H:%M:%S %z') 28 | } 29 | end 30 | end -------------------------------------------------------------------------------- /app/graphql/types/section_type.rb: -------------------------------------------------------------------------------- 1 | Types::SectionType = GraphQL::ObjectType.define do 2 | name "Section" 3 | field :id, !types.ID 4 | field :name, !types.String 5 | field :slug, !types.String 6 | field :permalink, !types.String 7 | field :description, types.String 8 | field :rank, !types.Int 9 | field :parent_section, Types::SectionType 10 | field :subsections, types[Types::SectionType] 11 | field :articles, types[!Types::ArticleType] do 12 | resolve -> (obj, args, ctx) { 13 | obj.articles.published 14 | } 15 | end 16 | 17 | field :unpublished_articles, types[!Types::ArticleType] do 18 | resolve -> (obj, args, ctx) { 19 | if !Authentication::admin_is_valid(ctx) 20 | # Unpublished articles may only be viewed by authorized users. 21 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 22 | end 23 | 24 | Authentication::generate_new_header(ctx) 25 | return obj.articles.unpublished 26 | } 27 | end 28 | 29 | field :parent_id, types.ID 30 | end 31 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?('config/database.yml') 24 | # cp 'config/database.yml.sample', 'config/database.yml' 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:setup' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /db/migrate/20180129034322_change_media_user_id_to_profile_id.rb: -------------------------------------------------------------------------------- 1 | class ChangeMediaUserIdToProfileId < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :media, :profile_id, :bigint 4 | media_type_to_role = { 5 | "photo" => "Photographer", 6 | "illustration" => "Illustrator", 7 | } 8 | Medium.transaction do 9 | Medium.find_each do |medium| 10 | title = media_type_to_role[medium.media_type] 11 | throw "No title for given media type: #{medium.media_type}" if title.nil? 12 | profile = Profile 13 | .joins(:role, :user) 14 | .find_by( 15 | "roles.title = ? AND users.id = ?", 16 | title, 17 | medium.user_id) 18 | if profile.nil? 19 | role = Role.find_by(title: title) 20 | profile = Profile.create(user_id: medium.user_id, role_id: role.id) 21 | end 22 | medium.update(profile_id: profile.id) 23 | end 24 | remove_column :media, :user_id 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | .idea 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | /spec-env 16 | .byebug_history 17 | /public/system/media 18 | # Elastic Beanstalk Files 19 | .elasticbeanstalk/* 20 | !.elasticbeanstalk/*.cfg.yml 21 | !.elasticbeanstalk/*.global.yml 22 | 23 | # client-app or stuyspec.com files. These are managed in the stuyspec.com repo 24 | /public/client_app/* 25 | # cms folders managed in cms repo 26 | /public/cms/* 27 | 28 | # ignore folders used during Capistrano deploy 29 | /stuyspec.com 30 | /cms 31 | 32 | /vendor 33 | 34 | .DS_Store 35 | 36 | .idea/* 37 | /public/system 38 | 39 | /.env 40 | 41 | # Emacs files 42 | \#*\# 43 | *~ 44 | \.\#* 45 | 46 | # DB Prod Dumps 47 | /db/dumps 48 | *.sql 49 | -------------------------------------------------------------------------------- /app/graphql/types/user_type.rb: -------------------------------------------------------------------------------- 1 | Types::UserType = GraphQL::ObjectType.define do 2 | name "User" 3 | field :id, !types.ID 4 | field :first_name, types.String 5 | field :last_name, types.String 6 | field :email, !types.String 7 | field :slug, !types.String 8 | field :description, types.String 9 | field :profile_pic_url, types.String 10 | field :roles, types[Types::RoleType] 11 | field :profiles, types[Types::ProfileType] 12 | field :articles, types[!Types::ArticleType] do 13 | resolve -> (obj, args, ctx) { 14 | obj.articles.published 15 | } 16 | end 17 | 18 | field :unpublished_articles, types[!Types::ArticleType] do 19 | resolve -> (obj, args, ctx) { 20 | if !Authentication::admin_is_valid(ctx) 21 | # Unpublished articles may only be viewed by authorized users. 22 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 23 | end 24 | 25 | Authentication::generate_new_header(ctx) 26 | return obj.articles.unpublished 27 | } 28 | end 29 | 30 | field :media, types[Types::MediumType] 31 | field :comments, types[Types::CommentType] 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 The Stuyvesant Spectator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/controllers/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | class GraphqlController < ApplicationController 2 | 3 | def execute 4 | variables = ensure_hash(params[:variables]) 5 | query = params[:query] 6 | operation_name = params[:operationName] 7 | context = { 8 | # Query context goes here, for example: 9 | # current_user: current_user, 10 | request: request, 11 | response: response 12 | } 13 | result = StuySpecApiSchema.execute( 14 | query, 15 | variables: variables, 16 | context: context, 17 | operation_name: operation_name 18 | ) 19 | render json: result 20 | end 21 | 22 | private 23 | 24 | # Handle form data, JSON body, or a blank value 25 | def ensure_hash(ambiguous_param) 26 | case ambiguous_param 27 | when String 28 | if ambiguous_param.present? 29 | ensure_hash(JSON.parse(ambiguous_param)) 30 | else 31 | {} 32 | end 33 | when Hash, ActionController::Parameters 34 | ambiguous_param 35 | when nil 36 | {} 37 | else 38 | raise ArgumentError, "Unexpected parameter: #{ambiguous_param}" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/requests/comments_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Comments", type: :request do 4 | before(:all) do 5 | unless @user = User.where(email: "jkao1@stuy.edu").first 6 | @user = User.create( 7 | email: "jkao1@stuy.edu", 8 | password: "hunter2hunter2", 9 | password_confirmation: "hunter2hunter2" 10 | ) 11 | end 12 | @article = Article.first 13 | end 14 | 15 | describe "POST /comments" do 16 | it 'correctly creates a new article' do 17 | auth_token = @user.create_new_auth_token 18 | post(comments_path, 19 | params: { 20 | comment: { 21 | article_id: @article.id, 22 | content: "This is my comment" 23 | } 24 | }, 25 | headers: auth_token 26 | ) 27 | end 28 | end 29 | describe "GET /comments" do 30 | it 'returns correct types' do 31 | get comments_path, params: { limit: 10 } 32 | expect_json_types( 33 | '?', 34 | content: :string, 35 | user_id: :integer, 36 | article_id: :integer 37 | ) 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /app/graphql/authentication.rb: -------------------------------------------------------------------------------- 1 | module Authentication 2 | def self.editor_is_valid(ctx) 3 | headers = Authentication::get_headers(ctx) 4 | user = Authentication::get_user(headers) 5 | 6 | client_id = headers['client'] 7 | token = headers['access-token'] 8 | 9 | return user && user.is_editor?(token, client_id) 10 | end 11 | 12 | def self.admin_is_valid(ctx) 13 | headers = Authentication::get_headers(ctx) 14 | user = Authentication::get_user(headers) 15 | 16 | client_id = headers['client'] 17 | token = headers['access-token'] 18 | 19 | return user && user.is_admin?(token, client_id) 20 | end 21 | 22 | def self.generate_new_header(ctx) 23 | # currently disable header change 24 | 25 | #headers = Authentication::get_headers(ctx) 26 | #user = Authentication::get_user(headers) 27 | 28 | #client_id = headers['client'] 29 | #new_auth_header = user.create_new_auth_token(client_id) 30 | #response = ctx[:response] 31 | #response.headers.merge!(new_auth_header) 32 | end 33 | 34 | def self.get_headers(ctx) 35 | return ctx[:request].headers 36 | end 37 | 38 | def self.get_user(headers) 39 | uid = headers["uid"] 40 | return User.find_by(email: uid) 41 | end 42 | end -------------------------------------------------------------------------------- /app/graphql/resolvers/create_section.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::CreateSection < Resolvers::MutationFunction 2 | # arguments passed as "args" 3 | argument :name, !types.String 4 | argument :parent_id, types.Int 5 | argument :description, types.String 6 | argument :rank, !types.Int 7 | 8 | # return type from the mutation 9 | type Types::SectionType 10 | 11 | # the mutation method 12 | # _obj - is parent object, which in this case is nil 13 | # args - are the arguments passed 14 | # _ctx - is the GraphQL context (which would be discussed later) 15 | def call(_obj, args, ctx) 16 | if !Authentication::admin_is_valid(ctx) 17 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 18 | end 19 | if args["parent_id"] 20 | parent_section = Section.find(args["parent_id"]) 21 | section = parent_section.subsections.build( 22 | name: args["name"], 23 | description: args["description"], 24 | rank: args["rank"] 25 | ) 26 | Authentication::generate_new_header(ctx) if section.save 27 | else 28 | section = Section.create( 29 | name: args["name"], 30 | description: args["description"], 31 | rank: args["rank"] 32 | ) 33 | Authentication::generate_new_header(ctx) if section 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_latest_articles.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetLatestArticles < Resolvers::ArticleQueryFunction 2 | 3 | argument :section_id, types.ID 4 | argument :offset, types.Int 5 | argument :limit, types.Int 6 | argument :include_subsections, types.Boolean 7 | 8 | # return type from the mutation 9 | type types[!Types::ArticleType] 10 | 11 | # the mutation method 12 | # _obj - is parent object, which in this case is nil 13 | # _args - are the arguments passed 14 | # _ctx - is the GraphQL context (which would be discussed later) 15 | def call(_obj, args, _ctx) 16 | articles = Article.order(created_at: :desc).published 17 | 18 | if args['section_id'] && args['include_subsections'] 19 | # NOTE: only works with one level of nesting 20 | # Fix if multiple levels of nesting introduced 21 | subsection_ids = Section.find(args['section_id']).subsections.map do |s| s.id end 22 | subsection_ids << args['section_id'] 23 | articles = articles.where(section_id: subsection_ids) 24 | elsif args['section_id'] 25 | articles = articles.where(section_id: args['section_id']) 26 | end 27 | 28 | articles = articles.offset(args['offset']) if args['offset'] 29 | articles = articles.limit(args['limit']) if args['limit'] 30 | 31 | return articles 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scaffold.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | color: #333; 4 | margin: 33px; 5 | } 6 | 7 | body, p, ol, ul, td { 8 | font-family: verdana, arial, helvetica, sans-serif; 9 | font-size: 13px; 10 | line-height: 18px; 11 | } 12 | 13 | pre { 14 | background-color: #eee; 15 | padding: 10px; 16 | font-size: 11px; 17 | } 18 | 19 | a { 20 | color: #000; 21 | } 22 | 23 | a:visited { 24 | color: #666; 25 | } 26 | 27 | a:hover { 28 | color: #fff; 29 | background-color: #000; 30 | } 31 | 32 | th { 33 | padding-bottom: 5px; 34 | } 35 | 36 | td { 37 | padding: 0 5px 7px; 38 | } 39 | 40 | div.field, 41 | div.actions { 42 | margin-bottom: 10px; 43 | } 44 | 45 | #notice { 46 | color: green; 47 | } 48 | 49 | .field_with_errors { 50 | padding: 2px; 51 | background-color: red; 52 | display: table; 53 | } 54 | 55 | #error_explanation { 56 | width: 450px; 57 | border: 2px solid red; 58 | padding: 7px 7px 0; 59 | margin-bottom: 20px; 60 | background-color: #f0f0f0; 61 | } 62 | 63 | #error_explanation h2 { 64 | text-align: left; 65 | font-weight: bold; 66 | padding: 5px 5px 5px 15px; 67 | font-size: 12px; 68 | margin: -7px -7px 0; 69 | background-color: #c00; 70 | color: #fff; 71 | } 72 | 73 | #error_explanation ul li { 74 | font-size: 12px; 75 | list-style: square; 76 | } 77 | 78 | label { 79 | display: block; 80 | } 81 | -------------------------------------------------------------------------------- /app/controllers/roles_controller.rb: -------------------------------------------------------------------------------- 1 | class RolesController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :authenticate_admin!, only: [:create, :update, :destroy] 4 | before_action :set_role, only: [:show, :update, :destroy] 5 | 6 | 7 | # GET /roles 8 | def index 9 | @roles = Role.all 10 | 11 | render json: @roles 12 | end 13 | 14 | # GET /roles/1 15 | def show 16 | render json: @role 17 | end 18 | 19 | # POST /roles 20 | def create 21 | @role = Role.new(role_params) 22 | 23 | if @role.save 24 | render json: @role, status: :created, location: @role 25 | else 26 | render json: @role.errors, status: :unprocessable_entity 27 | end 28 | end 29 | 30 | # PATCH/PUT /roles/1 31 | def update 32 | if @role.update(role_params) 33 | render json: @role 34 | else 35 | render json: @role.errors, status: :unprocessable_entity 36 | end 37 | end 38 | 39 | # DELETE /roles/1 40 | def destroy 41 | @role.destroy 42 | end 43 | 44 | private 45 | # Use callbacks to share common setup or constraints between actions. 46 | def set_role 47 | @role = Role.find(params[:id]) 48 | end 49 | 50 | # Only allow a trusted parameter "white list" through. 51 | def role_params 52 | params.require(:role).permit(:title, :slug) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: f9a0fde6647aebca135cdde55e1e1a7f73b8d08f8d5dbd178686302c7d6723fbc945b01f471ef18ced3fe6372d26670817f859c5a2638ba7df5988c9e0dd6c90 22 | 23 | test: 24 | secret_key_base: b6b962d63a00c0b059f75699e9deedd8475e58daf4aab90c1a43f3d781c82f473a2c80dc576ec712cc1ec19f2a9b90cfbc75cb08156899253ee38ce32f6ff4f9 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /app/controllers/subscribers_controller.rb: -------------------------------------------------------------------------------- 1 | class SubscribersController < ApplicationController 2 | before_action :set_subscriber, only: [:show, :update, :destroy] 3 | 4 | # GET /subscribers 5 | def index 6 | @subscribers = Subscriber.all 7 | 8 | render json: @subscribers 9 | end 10 | 11 | # GET /subscribers/1 12 | def show 13 | render json: @subscriber 14 | end 15 | 16 | # POST /subscribers 17 | def create 18 | @subscriber = Subscriber.new(subscriber_params) 19 | 20 | if @subscriber.save 21 | render json: @subscriber, status: :created, location: @subscriber 22 | else 23 | render json: @subscriber.errors, status: :unprocessable_entity 24 | end 25 | end 26 | 27 | # PATCH/PUT /subscribers/1 28 | def update 29 | if @subscriber.update(subscriber_params) 30 | render json: @subscriber 31 | else 32 | render json: @subscriber.errors, status: :unprocessable_entity 33 | end 34 | end 35 | 36 | # DELETE /subscribers/1 37 | def destroy 38 | @subscriber.destroy 39 | end 40 | 41 | private 42 | # Use callbacks to share common setup or constraints between actions. 43 | def set_subscriber 44 | @subscriber = Subscriber.find(params[:id]) 45 | end 46 | 47 | # Only allow a trusted parameter "white list" through. 48 | def subscriber_params 49 | params.require(:subscriber).permit(:email) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | constraints(subdomain: "api") do 3 | post "/graphql", to: "graphql#execute" 4 | resources :outquotes 5 | resources :sections 6 | resources :articles 7 | resources :profiles 8 | resources :subscribers 9 | resources :roles do 10 | resources :profiles 11 | end 12 | resources :comments 13 | resources :media 14 | resources :authorships 15 | mount_devise_token_auth_for 'User', at: 'auth' 16 | resources :users do 17 | resources :comments 18 | resources :profiles 19 | end 20 | resources :sections do 21 | resources :articles do 22 | resources :media 23 | end 24 | end 25 | resources :articles do 26 | resources :outquotes 27 | resources :authorships 28 | resources :media 29 | resources :comments 30 | end 31 | 32 | get '/init', to: 'initial#index' 33 | get '/', to: 'initial#welcome' 34 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 35 | end 36 | 37 | constraints lambda { |request| request.subdomain == "www" or request.subdomain == "" } do 38 | get '/:section/:slug', to: 'client_app#index' 39 | get '/:section/:subsection/:slug', to: 'client_app#index' 40 | get '/*all', to: 'client_app#index' 41 | get '/', to: 'client_app#index' 42 | end 43 | 44 | constraints(subdomain: "cms") do 45 | get '/', to: 'cms#index' 46 | get '/*all', to: 'cms#index' 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid for current version and patch releases of Capistrano 2 | lock "~> 3.12.0" 3 | 4 | set :rvm_type, :user 5 | set :rvm_ruby_version, '2.7.0' 6 | 7 | set :application, "stuyspec-api" 8 | set :repo_url, "https://github.com/stuyspec/stuyspec-api.git" 9 | #set :migration_role, :app 10 | set :deploy_to, "/home/ubuntu/deploy" 11 | set :branch, "master" 12 | set :passenger_restart_with_touch, false 13 | 14 | task :build_client_app do 15 | run_locally do 16 | execute "if cd stuyspec.com; then git reset --hard HEAD && git pull; else git clone https://github.com/stuyspec/stuyspec.com.git; cd stuyspec.com; fi" 17 | execute "cd stuyspec.com; npm install" 18 | execute "cd stuyspec.com; npm run build" 19 | execute "cd stuyspec.com; rm -rf client-app; cp -a build/ client-app/" 20 | end 21 | end 22 | 23 | task :build_cms do 24 | run_locally do 25 | execute "if cd cms; then git reset --hard HEAD && git pull; else git clone https://github.com/stuyspec/cms.git; cd cms; fi" 26 | execute "cd cms; npm install" 27 | execute "cd cms; npm run build" 28 | execute "cd cms; rm -rf cms; cp -a build/ cms/" 29 | end 30 | end 31 | 32 | task :upload_apps do 33 | on roles(:app) do 34 | upload! "./stuyspec.com/client-app", release_path + "public", recursive: true 35 | upload! "./cms/cms", release_path + "public", recursive: true 36 | end 37 | end 38 | 39 | before "deploy:starting", "build_client_app" 40 | before "deploy:starting", "build_cms" 41 | 42 | before "deploy:publishing", "upload_apps" 43 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include DeviseTokenAuth::Concerns::SetUserByToken 3 | before_action :cors_preflight_check 4 | 5 | def cors_preflight_check 6 | if request.method == 'OPTIONS' 7 | headers['Access-Control-Allow-Origin'] = '*' 8 | headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS' 9 | headers['Access-Control-Allow-Headers'] = 10 | 'X-Requested-With, X-Prototype-Version, Token' 11 | headers['Access-Control-Max-Age'] = '1728000' 12 | render :text => '', :content_type => 'text/plain' 13 | end 14 | end 15 | 16 | def authenticate_admin! 17 | token = request.headers["access-token"] 18 | client_id = request.headers["client"] 19 | return render json: { 20 | success: false, 21 | errors: ["You do not have the relevant permissions"] 22 | }, status: 401 unless current_user.is_admin?(token, client_id) 23 | end 24 | 25 | # rescue_from ActiveRecord::RecordNotFound do |exception| 26 | rescue_from ActiveRecord::RecordNotFound do |exception| 27 | render json: { 28 | success: false, 29 | errors: ["Record not found"] 30 | }, status: 404 31 | end 32 | 33 | # missing template 34 | rescue_from ActionView::MissingTemplate do |exception| 35 | Rails.logger.error exception "Missing template" 36 | render json: { 37 | success: false, 38 | errors: ["Missing template"] 39 | }, status: 404 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_action :set_user, only: [:show, :update, :destroy, :index] 3 | 4 | # GET /users 5 | def index 6 | @users = User.all 7 | render json: @users 8 | end 9 | 10 | # GET /users/1 11 | def show 12 | render json: @user.to_json( 13 | :only => [:id, :username, :first_name, :last_name], 14 | :methods => [:profile_pic_url] 15 | ) 16 | end 17 | 18 | # POST /users 19 | def create 20 | @user = User.new(user_params) 21 | 22 | if @user.save 23 | render json: @user.to_json( 24 | :only => [:id, :username, :first_name, :last_name], 25 | :methods => [:profile_pic_url] 26 | ), status: :created, location: @user 27 | else 28 | render json: @user.errors, status: :unprocessable_entity 29 | end 30 | end 31 | 32 | # PATCH/PUT /users/1 33 | def update 34 | if @user.update(user_params) 35 | render json: @user 36 | else 37 | render json: @user.errors, status: :unprocessable_entity 38 | end 39 | end 40 | 41 | # DELETE /users/1 42 | def destroy 43 | @user.destroy 44 | end 45 | 46 | private 47 | # Use callbacks to share common setup or constraints between actions. 48 | def set_user 49 | @user = User.find(params[:id]) 50 | end 51 | 52 | # Only allow a trusted parameter "white list" through. 53 | def user_params 54 | params.require(:user).permit(:first_name, :last_name, :email, :description, :slug) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/sections_controller.rb: -------------------------------------------------------------------------------- 1 | class SectionsController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :authenticate_admin!, only: [:create, :update, :destroy] 4 | before_action :set_section, only: [:show, :update, :destroy] 5 | 6 | # GET /sections 7 | def index 8 | @sections = Section.where("is_visible = true") 9 | if params[:include_invisible] 10 | @sections = Section.all 11 | end 12 | 13 | render json: @sections 14 | end 15 | 16 | # GET /sections/1 17 | def show 18 | render json: @section 19 | end 20 | 21 | # POST /sections 22 | def create 23 | @section = Section.new(section_params) 24 | 25 | if @section.save 26 | render json: @section, status: :created, location: @section 27 | else 28 | render json: @section.errors, status: :unprocessable_entity 29 | end 30 | end 31 | 32 | # PATCH/PUT /sections/1 33 | def update 34 | if @section.update(section_params) 35 | render json: @section 36 | else 37 | render json: @section.errors, status: :unprocessable_entity 38 | end 39 | end 40 | 41 | # DELETE /sections/1 42 | def destroy 43 | @section.destroy 44 | end 45 | 46 | private 47 | # Use callbacks to share common setup or constraints between actions. 48 | def set_section 49 | @section = Section.friendly.find(params[:id]) 50 | end 51 | 52 | # Only allow a trusted parameter "white list" through. 53 | def section_params 54 | params.require(:section).permit(:name, :description, :slug, :parent_id, :rank) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_latest_unpublished_articles.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetLatestUnpublishedArticles < Resolvers::ArticleQueryFunction 2 | 3 | argument :section_id, types.ID 4 | argument :offset, types.Int 5 | argument :limit, types.Int 6 | argument :include_subsections, types.Boolean 7 | 8 | # return type from the mutation 9 | type types[!Types::ArticleType] 10 | 11 | # the mutation method 12 | # _obj - is parent object, which in this case is nil 13 | # _args - are the arguments passed 14 | # _ctx - is the GraphQL context (which would be discussed later) 15 | def call(_obj, args, ctx) 16 | if !Authentication::editor_is_valid(ctx) 17 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 18 | end 19 | 20 | articles = Article.order(created_at: :desc).unpublished 21 | 22 | if args['section_id'] && args['include_subsections'] 23 | # NOTE: only works with one level of nesting 24 | # Fix if multiple levels of nesting introduced 25 | subsection_ids = Section.find(args['section_id']).subsections.map do |s| s.id end 26 | subsection_ids << args['section_id'] 27 | articles = articles.where(section_id: subsection_ids) 28 | elsif args['section_id'] 29 | articles = articles.where(section_id: args['section_id']) 30 | end 31 | 32 | articles = articles.offset(args['offset']) if args['offset'] 33 | articles = articles.limit(args['limit']) if args['limit'] 34 | 35 | Authentication::generate_new_header(ctx) if articles 36 | 37 | return articles 38 | end 39 | end -------------------------------------------------------------------------------- /app/controllers/initial_controller.rb: -------------------------------------------------------------------------------- 1 | class InitialController < ApplicationController 2 | def index 3 | media_with_urls = Medium.all.map do |medium| 4 | medium.attributes.merge({ 5 | attachment_url: medium.attachment_url, 6 | medium_attachment_url: medium.medium_attachment_url, 7 | thumb_attachment_url: medium.thumb_attachment_url 8 | }) 9 | end 10 | users_with_urls = User.all.map do |user| 11 | user.attributes.merge({ 12 | profile_pic_url: user.profile_pic_url, 13 | }) 14 | end 15 | # restrict the following to logged in users 16 | 17 | render json: { 18 | :articles => Article 19 | .joins("LEFT JOIN sections ON articles.section_id = sections.id") 20 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue + 192 * articles.volume DESC"), 21 | :sections => Section.where("is_visible = true"), 22 | # :comments => Comment.where.not(published_at: nil).all, 23 | # :media => media_with_urls, 24 | # :users => User.all, 25 | :roles => Role.all, 26 | # :profiles => Profile.all, 27 | # :authorships => Authorship.all, 28 | # :outquotes => Outquote.all 29 | } 30 | end 31 | 32 | def welcome 33 | render json: { 34 | message: "Welcome to StuySpec's API! We do not provide public documentation"\ 35 | " at this moment, but you are welcome to use it anyways" 36 | } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. 3 | extend Devise::Models 4 | devise :database_authenticatable, :registerable, 5 | :recoverable, :rememberable, :trackable, 6 | :validatable, :omniauthable, :confirmable 7 | include DeviseTokenAuth::Concerns::User 8 | 9 | include PgSearch::Model 10 | multisearchable :against => [:email, :slug] 11 | 12 | has_many :authorships 13 | has_many :articles, through: :authorships, dependent: :destroy 14 | has_many :comments, dependent: :destroy 15 | has_many :profiles 16 | has_many :roles, through: :profiles, dependent: :destroy 17 | has_many :media, through: :profiles 18 | after_create :init 19 | 20 | has_attached_file :profile_picture, 21 | storage: :s3, 22 | default_url: "" 23 | validates_attachment :profile_picture, 24 | content_type: { content_type: ["image/jpg", "image/jpeg", "image/gif", "image/png"] } 25 | def init 26 | self.update(security_level: 0) 27 | first_name = self.first_name || '' 28 | last_name = self.last_name || '' 29 | slug = (first_name + ' ' + last_name).parameterize 30 | while !User.find_by(slug: slug).nil? 31 | slug += '-' + ([*('0'..'9')]-%w(0 1 I O)).sample(8).join 32 | end 33 | self.update(slug: slug) 34 | end 35 | 36 | def is_editor?(token, client_id) 37 | self.valid_token?(token, client_id) && self.security_level > 0 38 | end 39 | 40 | def is_admin?(token, client_id) 41 | self.valid_token?(token, client_id) && self.security_level > 1 42 | end 43 | 44 | def profile_pic_url 45 | profile_picture.url 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/authorships_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthorshipsController < ApplicationController 2 | before_action :set_authorship, only: [:show, :update, :destroy] 3 | before_action :authenticate_user!, only: [:create, :update, :destroy] 4 | 5 | # GET /authorships 6 | def index 7 | if params[:article_id] 8 | @article = Article.friendly.find(params[:article_id]) 9 | @authorships = Authorship.where("article_id = ?", @article.id) 10 | else 11 | @authorships = Authorship.all 12 | end 13 | render json: @authorships 14 | end 15 | 16 | # GET /authorships/1 17 | def show 18 | render json: @authorship 19 | end 20 | 21 | # POST /authorships 22 | def create 23 | @authorship = Authorship.new(authorship_params) 24 | 25 | if @authorship.save 26 | render json: @authorship, status: :created, location: @authorship 27 | else 28 | render json: @authorship.errors, status: :unprocessable_entity 29 | end 30 | end 31 | 32 | # PATCH/PUT /authorships/1 33 | def update 34 | if @authorship.update(authorship_params) 35 | render json: @authorship 36 | else 37 | render json: @authorship.errors, status: :unprocessable_entity 38 | end 39 | end 40 | 41 | # DELETE /authorships/1 42 | def destroy 43 | @authorship.destroy 44 | end 45 | 46 | private 47 | # Use callbacks to share common setup or constraints between actions. 48 | def set_authorship 49 | @authorship = Authorship.find(params[:id]) 50 | end 51 | 52 | # Only allow a trusted parameter "white list" through. 53 | def authorship_params 54 | params.require(:authorship).permit(:user_id, :article_id) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_user.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::CreateUser < Resolvers::MutationFunction 2 | 3 | argument :first_name, !types.String 4 | argument :last_name, !types.String 5 | argument :email, !types.String 6 | argument :description, !types.String 7 | argument :password, !types.String 8 | argument :password_confirmation, !types.String 9 | argument :role, !types.String 10 | argument :profile_picture_b64, as: :attachment do 11 | type types.String 12 | description 'The base64 encoded version of the profile picture.' 13 | end 14 | 15 | type Types::UserType 16 | 17 | def call(_obj, args, ctx) 18 | if !Authentication::admin_is_valid(ctx) 19 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 20 | end 21 | 22 | email_user = User.find_by(email: args["email"]) 23 | if !email_user.nil? 24 | return GraphQL::ExecutionError.new( 25 | "Email taken by %s %s." % [email_user.first_name, email_user.last_name] 26 | ) 27 | end 28 | 29 | @new_user = User.new( 30 | first_name: args[:first_name], 31 | last_name: args[:last_name], 32 | email: args[:email], 33 | description: args[:description], 34 | password: args[:password], 35 | password_confirmation: args[:password_confirmation], 36 | created_at: Time.now, 37 | profile_picture: args["attachment"] || nil, 38 | ) 39 | role = Role.find_by(title: args["role"]) 40 | if args["role"] and role != nil and !@new_user.roles.include?(role) 41 | @new_user.roles << role 42 | end 43 | Authentication::generate_new_header(ctx) if @new_user.save! 44 | return @new_user 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_featured_articles_by_section_id.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetFeaturedArticlesBySectionID < Resolvers::ArticleQueryFunction 2 | 3 | argument :section_id, !types.ID 4 | # return type from the mutation 5 | type types[Types::ArticleType] 6 | 7 | # the mutation method 8 | # _obj - is parent object, which in this case is nil 9 | # _args - are the arguments passed 10 | # _ctx - is the GraphQL context (which would be discussed later) 11 | def call(_obj, args, _ctx) 12 | @section = Section.find(section_id) 13 | return GraphQL::ExecutionError.new("No such section found") if !@section 14 | 15 | @section_ids = [args['section_id']].concat(@section.subsections.map { |s| s.id }) 16 | 17 | primary_article = 18 | Article 19 | .joins('JOIN media ON articles.id = media.article_id') 20 | .joins('JOIN sections ON articles.section_id = sections.id') 21 | .where("sections.id = ?", @section_ids) 22 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue"\ 23 | " + 192 * articles.volume DESC") 24 | .published 25 | .first 26 | 27 | secondary_articles = 28 | Article 29 | .joins('JOIN sections ON articles.section_id = sections.id') 30 | .where("sections.id = ? AND articles.id != ?", @section_ids, primary_article.id) 31 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue"\ 32 | " + 192 * articles.volume DESC") 33 | .published 34 | 35 | if primary_article.nil? 36 | return secondary_articles.first(3) 37 | end 38 | return [primary_article, secondary_articles.first, secondary_articles.second] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/profiles_controller.rb: -------------------------------------------------------------------------------- 1 | class ProfilesController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :set_profile, only: [:show, :update, :destroy] 4 | 5 | # GET /profiles 6 | def index 7 | if params[:user_id] 8 | @user = User.find(params[:user_id]) 9 | @profiles = Profile.where("user_id = ?", @user.id) 10 | elsif params[:role_id] 11 | @role = Role.find(params[:role_id]) 12 | @profiles = Profile.where("role_id = ?", @role.id) 13 | else 14 | @profiles = Profile.all 15 | end 16 | 17 | render json: @profiles 18 | end 19 | 20 | # GET /profiles/1 21 | def show 22 | render json: @profile 23 | end 24 | 25 | # POST /profiles 26 | def create 27 | @profile = Profile.new(profile_params) 28 | 29 | if @profile.save 30 | render json: @profile, status: :created, location: @profile 31 | else 32 | render json: @profile.errors, status: :unprocessable_entity 33 | end 34 | end 35 | 36 | # PATCH/PUT /profiles/1 37 | def update 38 | if @profile.update(profile_params) 39 | render json: @profile 40 | else 41 | render json: @profile.errors, status: :unprocessable_entity 42 | end 43 | end 44 | 45 | # DELETE /profiles/1 46 | def destroy 47 | @profile.destroy 48 | end 49 | 50 | private 51 | # Use callbacks to share common setup or constraints between actions. 52 | def set_profile 53 | @profile = Profile.find(params[:id]) 54 | end 55 | 56 | # Only allow a trusted parameter "white list" through. 57 | def profile_params 58 | params.require(:profile).permit(:user_id, :role_id) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_top_ranked_articles.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetTopRankedArticles < Resolvers::ArticleQueryFunction 2 | 3 | argument :section_id, types.ID 4 | argument :section_slug, types.String 5 | argument :has_media, types.Boolean 6 | argument :limit, types.Int 7 | argument :include_subsections, types.Boolean 8 | # return type from the mutation 9 | type types[Types::ArticleType] 10 | 11 | # the mutation method 12 | # _obj - is parent object, which in this case is nil 13 | # _args - are the arguments passed 14 | # _ctx - is the GraphQL context (which would be discussed later) 15 | def call(_obj, args, _ctx) 16 | articles = Article.order_by_rank.published # joins Sections as well 17 | 18 | if args['section_id'] || args['section_slug'] 19 | if args['section_id'] 20 | section = Section.find(args['section_id']) 21 | else 22 | section = Section.find_by(slug: args['section_slug']) 23 | end 24 | 25 | return GraphQL::ExecutionError.new("Invalid section id or slug") if section.nil? 26 | 27 | if args['include_subsections'] 28 | section_ids = section.subsections.map do |s| s.id end 29 | section_ids << section.id 30 | else 31 | section_ids = section.id 32 | end 33 | 34 | articles = articles.where(section_id: section_ids) 35 | end 36 | 37 | if args["has_media"] 38 | unless articles.joins(:media).length == 0 39 | # uses SQL GROUP_BY clause to remove repeated articles from media JOIN 40 | articles = articles.joins(:media).group('articles.id, sections.rank') 41 | end 42 | end 43 | 44 | articles = articles.limit(args["limit"]) if args["limit"] 45 | 46 | return articles 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/controllers/outquotes_controller.rb: -------------------------------------------------------------------------------- 1 | class OutquotesController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :authenticate_admin!, only: [:create, :update, :destroy] 4 | before_action :set_outquote, only: [:show, :update, :destroy] 5 | 6 | 7 | # GET /outquotes 8 | def index 9 | if params[:limit] 10 | @outquotes = Outquote.first(params[:limit]) 11 | else 12 | @outquotes = Outquote.all 13 | end 14 | if params[:article_id] 15 | @outquotes = Article.friendly.find(params[:article_id]).outquotes 16 | end 17 | 18 | render json: @outquotes 19 | end 20 | 21 | # GET /outquotes/1 22 | def show 23 | render json: @outquote 24 | end 25 | 26 | # POST /outquotes 27 | def create 28 | @outquote = Outquote.new(outquote_params) 29 | 30 | if @outquote.save 31 | render json: @outquote, status: :created, location: @outquote 32 | else 33 | render json: @outquote.errors, status: :unprocessable_entity 34 | end 35 | end 36 | 37 | # PATCH/PUT /outquotes/1 38 | def update 39 | if @outquote.update(outquote_params) 40 | render json: @outquote 41 | else 42 | render json: @outquote.errors, status: :unprocessable_entity 43 | end 44 | end 45 | 46 | # DELETE /outquotes/1 47 | def destroy 48 | @outquote.destroy 49 | end 50 | 51 | private 52 | # Use callbacks to share common setup or constraints between actions. 53 | def set_outquote 54 | @outquote = Outquote.find(params[:id]) 55 | end 56 | 57 | # Only allow a trusted parameter "white list" through. 58 | def outquote_params 59 | params.require(:outquote).permit(:article_id, :text) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/graphql/resolvers/get_featured_articles_by_section_slug.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::GetFeaturedArticlesBySectionSlug < Resolvers::ArticleQueryFunction 2 | 3 | argument :section_slug, !types.String 4 | # return type from the mutation 5 | type types[Types::ArticleType] 6 | 7 | # the mutation method 8 | # _obj - is parent object, which in this case is nil 9 | # _args - are the arguments passed 10 | # _ctx - is the GraphQL context (which would be discussed later) 11 | def call(_obj, args, _ctx) 12 | @section = Section.find_by(slug: args['section_slug']) 13 | return GraphQL::ExecutionError.new("No such section found") if !@section 14 | 15 | @section_slugs = [args['section_slug']].concat(@section.subsections.map { |s| s.slug }) 16 | 17 | primary_article = 18 | Article 19 | .joins('JOIN media ON articles.id = media.article_id') 20 | .joins('JOIN sections ON articles.section_id = sections.id') 21 | .where("sections.slug IN (?)", @section_slugs) 22 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue"\ 23 | " + 192 * articles.volume DESC") 24 | .published 25 | .first 26 | 27 | primary_article_id = if primary_article.nil? then -1 else primary_article.id end 28 | secondary_articles = 29 | Article 30 | .joins('JOIN sections ON articles.section_id = sections.id') 31 | .where("sections.slug IN (?) AND articles.id != ?", @section_slugs, primary_article_id) 32 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue"\ 33 | " + 192 * articles.volume DESC") 34 | .published 35 | 36 | if primary_article.nil? 37 | return secondary_articles.first(3) 38 | end 39 | return [primary_article, secondary_articles.first, secondary_articles.second] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /db/migrate/20170731175931_devise_token_auth_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table(:users) do |t| 4 | ## Required 5 | t.string :provider, :null => false, :default => "email" 6 | t.string :uid, :null => false, :default => "" 7 | 8 | ## Database authenticatable 9 | t.string :encrypted_password, :null => false, :default => "" 10 | 11 | ## Recoverable 12 | t.string :reset_password_token 13 | t.datetime :reset_password_sent_at 14 | 15 | ## Rememberable 16 | t.datetime :remember_created_at 17 | 18 | ## Trackable 19 | t.integer :sign_in_count, :default => 0, :null => false 20 | t.datetime :current_sign_in_at 21 | t.datetime :last_sign_in_at 22 | t.string :current_sign_in_ip 23 | t.string :last_sign_in_ip 24 | 25 | ## Confirmable 26 | t.string :confirmation_token 27 | t.datetime :confirmed_at 28 | t.datetime :confirmation_sent_at 29 | t.string :unconfirmed_email # Only if using reconfirmable 30 | 31 | ## Lockable 32 | # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts 33 | # t.string :unlock_token # Only if unlock strategy is :email or :both 34 | # t.datetime :locked_at 35 | 36 | ## User Info 37 | t.string :name 38 | t.string :nickname 39 | t.string :image 40 | t.string :email 41 | 42 | ## Tokens 43 | t.json :tokens 44 | 45 | t.timestamps 46 | end 47 | 48 | add_index :users, :email, unique: true 49 | add_index :users, [:uid, :provider], unique: true 50 | add_index :users, :reset_password_token, unique: true 51 | add_index :users, :confirmation_token, unique: true 52 | # add_index :users, :unlock_token, unique: true 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ApplicationRecord 2 | 3 | before_create :init 4 | 5 | include PgSearch::Model 6 | multisearchable :against => [:title, :summary, :content] 7 | 8 | extend FriendlyId 9 | friendly_id :title, use: :slugged 10 | 11 | belongs_to :section, optional: true 12 | 13 | has_many :authorships 14 | has_many :contributors, 15 | through: :authorships, 16 | dependent: :destroy, 17 | source: :user 18 | has_many :comments, dependent: :destroy 19 | has_many :outquotes, dependent: :destroy 20 | 21 | # We want media shown in the order they were uploaded, which is by 22 | # order of ascending created_at. 23 | has_many :media, -> { order(created_at: :asc) }, dependent: :destroy 24 | 25 | def self.order_by_rank 26 | Article 27 | .joins(:section) 28 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue + 192 * articles.volume DESC") 29 | end 30 | 31 | def init 32 | self.preview = self.generate_preview() 33 | end 34 | 35 | def generate_preview 36 | preview = self.summary 37 | 38 | if preview.nil? || preview.empty? 39 | if self.content.nil? || self.content.empty? 40 | return nil 41 | end 42 | # This global replace before HTML tag sanitizing ensures we have spaces 43 | # between paragraphs. 44 | clean_content = self.content.gsub('

', ' ') 45 | preview = ActionView::Base.full_sanitizer.sanitize(clean_content) 46 | .split(' ')[0, 25] 47 | .join(' ') + '...' 48 | else 49 | words = preview.split(' ') 50 | if words.length > 25 51 | preview = words[0, 25].join(' ') + '...' 52 | end 53 | end 54 | 55 | return preview 56 | end 57 | 58 | def self.published 59 | return where(is_published: true) 60 | end 61 | 62 | def self.unpublished 63 | return where(is_published: false) 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 33 | # Tell Action Mailer not to deliver emails to the real world. 34 | # The :test delivery method accumulates sent emails in the 35 | # ActionMailer::Base.deliveries array. 36 | config.action_mailer.delivery_method = :test 37 | 38 | # Print deprecation notices to the stderr. 39 | config.active_support.deprecation = :stderr 40 | 41 | # Raises error for missing translations 42 | # config.action_view.raise_on_missing_translations = true 43 | end 44 | -------------------------------------------------------------------------------- /app/graphql/resolvers/update_user.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::UpdateUser < Resolvers::MutationFunction 2 | # arguments passed as "args" 3 | argument :id, !types.ID 4 | argument :first_name, types.String 5 | argument :last_name, types.String 6 | argument :email, types.String 7 | argument :description, types.String 8 | argument :role, types.String 9 | argument :profile_picture_b64, as: :attachment do 10 | type types.String 11 | description 'The base64 encoded bersion of the attatchment to upload.' 12 | end 13 | 14 | # return type from the mutation 15 | type Types::UserType 16 | 17 | # the mutation method 18 | # _obj - is parent object, which in this case is nil 19 | # args - are the arguments passed 20 | # _ctx - is the GraphQL context (which would be discussed later) 21 | def call(_obj, args, ctx) 22 | if !Authentication::editor_is_valid(ctx) or !Authentication::admin_is_valid(ctx) 23 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 24 | end 25 | 26 | @user = User.find(args["id"]) 27 | 28 | # Transaction so that we don't update a malformed user 29 | User.transaction do 30 | @user.first_name = args["first_name"] if args["first_name"] 31 | @user.last_name = args["last_name"] if args["last_name"] 32 | @user.email = args["email"] if args["email"] 33 | @user.description = args["description"] if args["description"] 34 | @user.profile_picture = args["attachment"] if args["attachment"] 35 | if args["first_name"] or args["last_name"] 36 | save = "-" + @user.slug.split("-")[-1] 37 | save = "" if save =~ /\d/ else save 38 | @user.slug = args["first_name"].downcase + "-" + args["last_name"].downcase + save 39 | end 40 | role = Role.find_by(title: args["role"]) 41 | if args["role"] and role != nil and !@user.roles.include?(role) 42 | @user.roles << role 43 | end 44 | Authentication::generate_new_header(ctx) if @user.save! 45 | end 46 | return @user 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/requests/articles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "Articles", type: :request do 4 | before(:all) do 5 | unless @user = User.where(email: "jkao1@stuy.edu").first 6 | @user = User.create( 7 | email: "jkao1@stuy.edu", 8 | password: "hunter2hunter2", 9 | password_confirmation: "hunter2hunter2", 10 | security_level: 1 11 | ) 12 | end 13 | if @user.tokens 14 | @user.tokens = nil 15 | @user.save 16 | end 17 | end 18 | describe "GET /articles" do 19 | it "returns correct types" do 20 | get articles_path, params: { limit: 10 } 21 | expect_json_types( 22 | '?', 23 | title: :string, 24 | slug: :string, 25 | content: :string 26 | ) 27 | end 28 | end 29 | 30 | describe "POST /articles" do 31 | it "creates new article" do 32 | @user.create_new_auth_token 33 | auth_token = @user.create_new_auth_token 34 | post(articles_path, 35 | params: { 36 | article: { 37 | title: 'My article', 38 | content: 'This is my article', 39 | volume: 1, 40 | issue: 2, 41 | is_published: false, 42 | }, 43 | section_id: 1 44 | }, 45 | headers: auth_token 46 | ) 47 | end 48 | end 49 | 50 | describe "PUT /articles" do 51 | it "updates article" do 52 | auth_token = @user.create_new_auth_token 53 | article = Article.first 54 | put article_path(article), 55 | params: { 56 | article: { 57 | title: 'My article', 58 | content: 'This is my article', 59 | volume: 1, 60 | issue: 2, 61 | is_published: false, 62 | } 63 | }, 64 | headers: auth_token 65 | end 66 | end 67 | 68 | describe "DELETE /articles" do 69 | it "deletes article" do 70 | auth_token = @user.create_new_auth_token 71 | article = Article.first 72 | delete article_path(article), headers: auth_token 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: circleci/node:10.15 7 | # Specify service dependencies here if necessary 8 | # CircleCI maintains a library of pre-built images 9 | # documented at https://circleci.com/docs/2.0/circleci-images/ 10 | # - image: circleci/mongo:3.4.4 11 | working_directory: ~/stuyspec-api 12 | steps: 13 | - checkout 14 | 15 | - run: | 16 | sudo apt-get install python-dev python-pip 17 | pip install awsebcli --upgrade --user 18 | cd 19 | git clone https://github.com/stuyspec/stuyspec.com.git 20 | cd stuyspec.com 21 | git checkout master 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "~/stuyspec.com/package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | - run: | 29 | cd ~/stuyspec.com 30 | npm install 31 | - save_cache: 32 | paths: 33 | - ~/stuyspec.com/node_modules 34 | key: v1-dependencies-{{ checksum "~/stuyspec.com/package.json" }} 35 | - run: | 36 | cd ~/stuyspec.com 37 | CI=false npm run build 38 | - run: | 39 | git checkout master 40 | mkdir public/client-app 41 | cp -af ~/stuyspec.com/build/* public/client-app 42 | cd ~/stuyspec-api 43 | zip -r ~/api.zip ./* 44 | mv ~/api.zip ~/stuyspec-api/ 45 | - deploy: 46 | name: Deploy to AWS S3 47 | command: | 48 | #if [ "${CIRCLE_BRANCH}" == "staging" ]; then 49 | #aws s3 sync ~/client-app/dist/prod s3://staging.stuyspec.com 50 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 51 | cd ~/stuyspec-api 52 | printf "1\n1\n\n" | ~/.local/bin/eb init 53 | ~/.local/bin/eb deploy --staged 54 | else 55 | echo "We do not deploy on the ${CIRCLE_BRANCH} branch." 56 | fi 57 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_medium.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::CreateMedium < Resolvers::MutationFunction 2 | 3 | # arguments passed as "args" 4 | # TODO: make is_featured mandatory 5 | argument :title, !types.String 6 | argument :article_id, types.Int 7 | argument :user_id, !types.Int 8 | argument :is_featured do 9 | type types.Boolean 10 | description 'Whether the medium will be shown at the top of its article.' 11 | end 12 | argument :caption, types.String 13 | argument :media_type, !types.String 14 | argument :attachment_b64, as: :attachment do 15 | type !types.String 16 | description 'The base64 encoded version of the attachment to upload.' 17 | end 18 | 19 | # return type from the mutation 20 | type Types::MediumType 21 | 22 | # the mutation method 23 | # _obj - is parent object, which in this case is nil 24 | # args - are the arguments passed 25 | # _ctx - is the GraphQL context (which would be discussed later) 26 | def call(_obj, args, ctx) 27 | if !Authentication::editor_is_valid(ctx) 28 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 29 | end 30 | 31 | ActiveRecord::Base.transaction do 32 | media_type = args["media_type"] 33 | if media_type != "illustration" && media_type != "photo" 34 | return GraphQL::ExecutionError.new( 35 | "#{media_type} is currently unsupported" 36 | ) 37 | end 38 | 39 | if media_type == "illustration" 40 | roleTitle = "Illustrator" 41 | else 42 | roleTitle = "Photographer" 43 | end 44 | role = Role.find_by(title: roleTitle) 45 | 46 | profile = Profile.find_or_create_by( 47 | role_id: role.id, 48 | user_id: args["user_id"] 49 | ) 50 | 51 | @medium = Medium.new( 52 | title: args["title"], 53 | article_id: args["article_id"], 54 | profile_id: profile.id, 55 | is_featured: args["is_featured"] || false, 56 | caption: args["caption"], 57 | media_type: args["media_type"], 58 | attachment: args["attachment"], 59 | ) 60 | Authentication::generate_new_header(ctx) if @medium.save! 61 | end 62 | return @medium 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | require "rack/throttle" 15 | 16 | # Require the gems listed in Gemfile, including any gems 17 | # you've limited to :test, :development, or :production. 18 | Bundler.require(*Rails.groups) 19 | 20 | module StuySpecApi 21 | class Application < Rails::Application 22 | # Initialize configuration defaults for originally generated Rails version. 23 | config.load_defaults 5.1 24 | 25 | config.middleware.insert_before 0, Rack::Cors do 26 | allow do 27 | origins '*' 28 | resource '*', :headers => :any, 29 | :methods => [ 30 | :get, 31 | :post, 32 | :put, 33 | :delete, 34 | :options 35 | ], 36 | :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'] 37 | end 38 | end 39 | config.middleware.use OliveBranch::Middleware 40 | config.session_store :cookie_store, key: '_interslice_session' 41 | config.middleware.use ActionDispatch::Cookies # Required for all session management 42 | config.middleware.use ActionDispatch::Session::CookieStore, config.session_options 43 | config.middleware.use Rack::Throttle::Minute, :max => 1000 44 | config.time_zone = 'Eastern Time (US & Canada)' 45 | config.active_record.default_timezone = :local # Or :utc 46 | 47 | # Settings in config/environments/* take precedence over those specified here. 48 | # Application configuration should go into files in config/initializers 49 | # -- all .rb files in that directory are automatically loaded. 50 | 51 | # Only loads a smaller set of middleware suitable for API only apps. 52 | # Middleware like session, flash, cookies can be added back manually. 53 | # Skip views, helpers and assets when generating a new resource. 54 | config.api_only = true 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/media_controller.rb: -------------------------------------------------------------------------------- 1 | class MediaController < ApplicationController 2 | # before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | # before_action :authenticate_admin!, only: [:create, :update, :destroy] 4 | before_action :set_medium, only: [:show, :update, :destroy] 5 | 6 | 7 | # GET /media 8 | def index 9 | @media = Medium.all 10 | 11 | render json: @media.to_json(methods: [ 12 | :attachment_url, 13 | :medium_attachment_url, 14 | :thumb_attachment_url 15 | ]) 16 | end 17 | 18 | # GET /media/1 19 | def show 20 | render json: @medium.to_json( 21 | :only => [:id, :title, :caption], 22 | :methods => [:attachment_url, 23 | :medium_attachment_url, 24 | :thumb_attachment_url] 25 | ) 26 | end 27 | 28 | # POST /media 29 | def create 30 | @medium = Medium.new(medium_params) 31 | 32 | if @medium.save 33 | render json: @medium.to_json( 34 | :only => [:id, :title, :caption], 35 | :methods => [ 36 | :attachment_url, 37 | :medium_attachment_url, 38 | :thumb_attachment_url 39 | ] 40 | ), status: :created, location: @medium 41 | else 42 | render json: @medium.errors, status: :unprocessable_entity 43 | end 44 | end 45 | 46 | # PATCH/PUT /media/1 47 | def update 48 | if @medium.update(medium_params) 49 | render json: @medium 50 | else 51 | render json: @medium.errors, status: :unprocessable_entity 52 | end 53 | end 54 | 55 | # DELETE /media/1 56 | def destroy 57 | @medium.destroy 58 | end 59 | 60 | private 61 | # Use callbacks to share common setup or constraints between actions. 62 | def set_medium 63 | @medium = Medium.find(params[:id]) 64 | end 65 | 66 | # Only allow a trusted parameter "white list" through. 67 | def medium_params 68 | params.require(:medium).permit( 69 | :user_id, 70 | :article_id, 71 | :url, 72 | :title, 73 | :caption, 74 | :is_featured, 75 | :media_type, 76 | :attachment 77 | ) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_article.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::CreateArticle < Resolvers::MutationFunction 2 | 3 | # arguments passed as "args" 4 | argument :title, !types.String 5 | argument :section_id, !types.Int 6 | argument :content, !types.String 7 | argument :summary, types.String 8 | argument :created_at, types.String 9 | argument :outquotes, types[!types.String] 10 | argument :volume, !types.Int 11 | argument :issue, !types.Int 12 | argument :contributors, !types[!types.Int] 13 | argument :is_published, types.Boolean 14 | argument :media_ids, types[!types.Int] 15 | 16 | # return type from the mutation 17 | type Types::ArticleType 18 | 19 | # the mutation method 20 | # _obj - is parent object, which in this case is nil 21 | # args - are the arguments passed 22 | # _ctx - is the GraphQL context (which would be discussed later) 23 | def call(_obj, args, ctx) 24 | if !Authentication::editor_is_valid(ctx) 25 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 26 | end 27 | 28 | if !!args["is_published"] && !Authentication::admin_is_valid(ctx) 29 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 30 | end 31 | 32 | ActiveRecord::Base.transaction do 33 | @article = Article.new( 34 | title: args["title"], 35 | section_id: args["section_id"], 36 | content: args["content"], 37 | volume: args["volume"], 38 | issue: args["issue"], 39 | summary: args["summary"], 40 | created_at: args["created_at"], 41 | is_published: !!args["is_published"] 42 | ) 43 | args["contributors"].each do |id| 44 | @article.authorships.build(user_id: id) 45 | 46 | # Adds contributor role to user if not yet present 47 | u = User.find_by(id: id) 48 | u.roles << Role.first unless u.nil? || u.roles.include?(Role.first) 49 | end 50 | if args["outquotes"] 51 | args["outquotes"].each do |text| 52 | @article.outquotes.build(text: text) 53 | end 54 | end 55 | 56 | if args["media_ids"] then 57 | args["media_ids"].each do |medium| 58 | @medium = Medium.find_by(id: medium) 59 | @article.media << @medium if @medium 60 | end 61 | end 62 | Authentication::generate_new_header(ctx) if @article.save 63 | end 64 | return @article 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.7.0' 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | # Nicer IDs 11 | gem 'friendly_id', '~> 5.1.0' 12 | 13 | # For authentication 14 | gem 'devise_token_auth', '~> 1.1.3' 15 | 16 | gem 'omniauth-google' 17 | 18 | # Fixes CORS issues 19 | gem 'rack-cors', :require => 'rack/cors' 20 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 21 | gem 'rails', '~> 6.0.2.1' 22 | # Use postgresql as the database for Active Record 23 | gem 'pg', '~> 0.18' 24 | # Use Puma as the app server 25 | gem 'puma', '~> 3.12' 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | # gem 'jbuilder', '~> 2.5' 28 | # Use Redis adapter to run Action Cable in production 29 | # gem 'redis', '~> 3.0' 30 | # Use ActiveModel has_secure_password 31 | # gem 'bcrypt', '~> 3.1.7' 32 | 33 | # Use Capistrano for deployment 34 | # gem 'capistrano-rails', group: :development 35 | 36 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 37 | # gem 'rack-cors' 38 | 39 | group :development, :test do 40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 41 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 42 | gem 'rspec_junit_formatter' 43 | gem 'rspec-rails', '~> 3.5' 44 | end 45 | 46 | group :development do 47 | gem 'listen', '< 3.2' 48 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 49 | gem 'spring' 50 | gem 'spring-watcher-listen', '~> 2.0.0' 51 | 52 | gem "capistrano", "~> 3.12", require: false 53 | gem "capistrano-rails", "~> 1.4", require: false 54 | gem 'capistrano-rvm', require: false 55 | gem 'capistrano-passenger', require: false 56 | gem 'capistrano-bundler', '~> 1.6', require: false 57 | end 58 | 59 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 60 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 61 | 62 | # Turns snake_case into camelCase 63 | gem "olive_branch" 64 | # OmniAuth 65 | gem 'omniauth-github' 66 | 67 | gem 'paperclip', '5.2.1' 68 | gem 'aws-sdk' 69 | gem 'airborne' 70 | gem 'dotenv-rails', groups: [:development] 71 | gem 'rack-throttle' 72 | 73 | gem 'pg_search' 74 | gem 'seed_dump' 75 | gem 'graphql' 76 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # server-based syntax 2 | # ====================== 3 | # Defines a single server with a list of roles and multiple properties. 4 | # You can define all roles on a single server, or split them: 5 | 6 | # server "example.com", user: "deploy", roles: %w{app db web}, my_property: :my_value 7 | # server "example.com", user: "deploy", roles: %w{app web}, other_property: :other_value 8 | # server "db.example.com", user: "deploy", roles: %w{db} 9 | 10 | 11 | 12 | # role-based syntax 13 | # ================== 14 | 15 | # Defines a role with one or multiple servers. The primary server in each 16 | # group is considered to be the first unless any hosts have the primary 17 | # property set. Specify the username and a domain or IP for the server. 18 | # Don't use `:all`, it's a meta role. 19 | 20 | role :app, %w{ubuntu@stuyspec.com} 21 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value 22 | # role :db, %w{deploy@example.com} 23 | 24 | 25 | 26 | # Configuration 27 | # ============= 28 | # You can set any configuration variable like in config/deploy.rb 29 | # These variables are then only loaded and set in this stage. 30 | # For available Capistrano configuration variables see the documentation page. 31 | # http://capistranorb.com/documentation/getting-started/configuration/ 32 | # Feel free to add new variables to customise your setup. 33 | 34 | 35 | 36 | # Custom SSH Options 37 | # ================== 38 | # You may pass any option but keep in mind that net/ssh understands a 39 | # limited set of options, consult the Net::SSH documentation. 40 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start 41 | # 42 | # Global options 43 | # -------------- 44 | #set :ssh_options, { 45 | # keys: %w("/home/darius/.ssh/LightsailDefaultKey-us-east-1.pem"), 46 | # forward_agent: false, 47 | # auth_methods: %w(password) 48 | #} 49 | # 50 | # The server-based syntax can be used to override options: 51 | # ------------------------------------ 52 | #server "example.com", 53 | # user: "ubuntu", 54 | # roles: %w{web app}, 55 | # ssh_options: { 56 | # user: "user_name", # overrides user setting above 57 | # keys: %w(/home/darius/.ssh/LightsailDefaultKey-us-east-1.pem), 58 | # forward_agent: false, 59 | # auth_methods: %w(publickey password) 60 | # # password: "please use keys" 61 | # } 62 | 63 | set :ssh_options, { :keys => [ "~/.ssh/LightsailDefaultKey-us-east-1.pem" ] } 64 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :authenticate_author!, only: [:update, :destroy] 4 | before_action :set_comment, only: [:show, :update, :destroy] 5 | 6 | 7 | # GET /comments 8 | def index 9 | @comments = Comment.where.not(published_at: nil).all 10 | if params[:article_id] 11 | @comments = Article.friendly.find(params[:article_id]).comments 12 | elsif params[:user_id] 13 | @comments = User.friendly.find(params[:user_id]).comments 14 | end 15 | if params[:limit] 16 | limit = params[:limit] 17 | @comments = @comments.first(limit) 18 | end 19 | render json: @comments 20 | end 21 | 22 | # GET /comments/1 23 | def show 24 | if params[:article_id] 25 | if @comment.article_id != params[:article_id] 26 | render json: @comment.errors, status: :unprocessable_entity 27 | end 28 | elsif params[:user_id] 29 | if @comment.user_id != params[:user_id] 30 | render json: @comment.errors, status: :unprocessable_entity 31 | end 32 | end 33 | render json: @comment 34 | end 35 | 36 | # POST /comments 37 | def create 38 | @comment = Comment.new( 39 | comment_params.merge(user_id: current_user.id) 40 | ) 41 | if @comment.save 42 | render json: @comment, status: :created, location: @comment 43 | else 44 | render json: @comment.errors, status: :unprocessable_entity 45 | end 46 | end 47 | 48 | # PATCH/PUT /comments/1 49 | def update 50 | if @comment.update(comment_params) 51 | render json: @comment 52 | else 53 | render json: @comment.errors, status: :unprocessable_entity 54 | end 55 | end 56 | 57 | # DELETE /comments/1 58 | def destroy 59 | @comment.destroy 60 | end 61 | 62 | private 63 | # Use callbacks to share common setup or constraints between actions. 64 | def set_comment 65 | @comment = Comment.find(params[:id]) 66 | end 67 | 68 | # Only allow a trusted parameter "white list" through. 69 | def comment_params 70 | params.require(:comment).permit(:content, 71 | :text, 72 | :article_id, 73 | :published_at) 74 | end 75 | 76 | def authenticate_author! 77 | return render json: { success: false, 78 | errors: ["You are not the author of this comment"] 79 | }, status: 401 unless comment.is_author? current_user 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /config/initializers/devise_token_auth.rb: -------------------------------------------------------------------------------- 1 | DeviseTokenAuth.setup do |config| 2 | # By default the authorization headers will change after each request. The 3 | # client is responsible for keeping track of the changing tokens. Change 4 | # this to false to prevent the Authorization header from changing after 5 | # each request. 6 | config.change_headers_on_each_request = false 7 | 8 | # By default, users will need to re-authenticate after 2 weeks. This setting 9 | # determines how long tokens will remain valid after they are issued. 10 | # config.token_lifespan = 2.weeks 11 | 12 | # Sets the max number of concurrent devices per user, which is 10 by default. 13 | # After this limit is reached, the oldest tokens will be removed. 14 | # config.max_number_of_devices = 10 15 | 16 | # Sometimes it's necessary to make several requests to the API at the same 17 | # time. In this case, each request in the batch will need to share the same 18 | # auth token. This setting determines how far apart the requests can be while 19 | # still using the same auth token. 20 | # config.batch_request_buffer_throttle = 5.seconds 21 | 22 | # This route will be the prefix for all oauth2 redirect callbacks. For 23 | # example, using the default '/omniauth', the github oauth2 provider will 24 | # redirect successful authentications to '/omniauth/github/callback' 25 | # config.omniauth_prefix = "/omniauth" 26 | 27 | # By default sending current password is not needed for the password update. 28 | # Uncomment to enforce current_password param to be checked before all 29 | # attribute updates. Set it to :password if you want it to be checked only if 30 | # password is updated. 31 | # config.check_current_password_before_update = :attributes 32 | 33 | # By default we will use callbacks for single omniauth. 34 | # It depends on fields like email, provider and uid. 35 | # config.default_callbacks = true 36 | 37 | # Makes it possible to change the headers names 38 | # config.headers_names = {:'access-token' => 'access-token', 39 | # :'client' => 'client', 40 | # :'expiry' => 'expiry', 41 | # :'uid' => 'uid', 42 | # :'token-type' => 'token-type' } 43 | 44 | # By default, only Bearer Token authentication is implemented out of the box. 45 | # If, however, you wish to integrate with legacy Devise authentication, you can 46 | # do so by enabling this flag. NOTE: This feature is highly experimental! 47 | # config.enable_standard_devise_support = false 48 | config.default_confirm_success_url = 'https://stuyspec.com' 49 | end 50 | -------------------------------------------------------------------------------- /app/graphql/resolvers/update_article.rb: -------------------------------------------------------------------------------- 1 | class Resolvers::UpdateArticle < Resolvers::MutationFunction 2 | # arguments passed as "args" 3 | argument :id, !types.ID 4 | argument :title, types.String 5 | argument :section_id, types.Int 6 | argument :content, types.String 7 | argument :summary, types.String 8 | argument :created_at, types.String 9 | argument :outquotes, types[!types.String] 10 | argument :volume, types.Int 11 | argument :issue, types.Int 12 | argument :contributors, types[!types.Int] 13 | argument :is_published, types.Boolean 14 | argument :media_ids, types[!types.Int] 15 | 16 | # return type from the mutation 17 | type Types::ArticleType 18 | 19 | # the mutation method 20 | # _obj - is parent object, which in this case is nil 21 | # args - are the arguments passed 22 | # _ctx - is the GraphQL context (which would be discussed later) 23 | def call(_obj, args, ctx) 24 | if !Authentication::editor_is_valid(ctx) 25 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 26 | end 27 | 28 | if !!args["is_published"] && !Authentication::admin_is_valid(ctx) 29 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 30 | end 31 | 32 | @article = Article.find(args["id"]) 33 | 34 | # Transaction so that we don't update a malformed article 35 | Article.transaction do 36 | @article.title = args["title"] if args["title"] 37 | @article.section_id = args["section_id"] if args["section_id"] 38 | @article.content = args["content"] if args["content"] 39 | @article.preview = args["summary"] if args["summary"] 40 | @article.created_at = args["created_at"] if args["created_at"] 41 | @article.volume = args["volume"] if args["volume"] 42 | @article.issue = args["issue"] if args["issue"] 43 | @article.is_published = args["is_published"] if args["is_published"] 44 | 45 | if args["outquotes"] 46 | @article.outquotes.clear 47 | args["outquotes"].each do |text| 48 | @article.outquotes.build(text: text) 49 | end 50 | end 51 | 52 | if args["contributors"] 53 | @article.contributors.clear 54 | args["contributors"].each do |id| 55 | Authorship.find_or_create_by(user_id: id, article_id: @article.id) 56 | 57 | # Adds contributor role to user if not yet present 58 | u = User.find_by(id: id) 59 | u.roles << Role.first unless u.nil? || u.roles.include?(Role.first) 60 | end 61 | end 62 | 63 | if args["media_ids"] then 64 | args["media_ids"].each do |medium| 65 | @medium = Medium.find_by(id: medium) 66 | @article.media << @medium if @medium 67 | end 68 | end 69 | 70 | Authentication::generate_new_header(ctx) if @article.save 71 | end 72 | return @article 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require File.expand_path('../../config/environment', __FILE__) 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | # 23 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 24 | 25 | # Checks for pending migration and applies them before tests are run. 26 | # If you are not using ActiveRecord, you can remove this line. 27 | ActiveRecord::Migration.maintain_test_schema! 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 31 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 32 | 33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 34 | # examples within a transaction, remove the following line or assign false 35 | # instead of true. 36 | config.use_transactional_fixtures = true 37 | 38 | # RSpec Rails can automatically mix in different behaviours to your tests 39 | # based on their file location, for example enabling you to call `get` and 40 | # `post` in specs under `spec/controllers`. 41 | # 42 | # You can disable this behaviour by removing the line below, and instead 43 | # explicitly tag your specs with their type, e.g.: 44 | # 45 | # RSpec.describe UsersController, :type => :controller do 46 | # # ... 47 | # end 48 | # 49 | # The different available types are documented in the features, such as in 50 | # https://relishapp.com/rspec/rspec-rails/docs 51 | config.infer_spec_type_from_file_location! 52 | 53 | # Filter lines from Rails gems in backtraces. 54 | config.filter_rails_from_backtrace! 55 | # arbitrary gems may also be filtered via: 56 | # config.filter_gems_from_backtrace("gem name") 57 | end 58 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | config.action_mailer.raise_delivery_errors = true 30 | 31 | config.action_mailer.perform_caching = false 32 | 33 | # Print deprecation notices to the Rails logger. 34 | config.active_support.deprecation = :log 35 | 36 | # Raise an error on page load if there are pending migrations. 37 | config.active_record.migration_error = :page_load 38 | 39 | # config.action_mailer.default_url_options = { 40 | # :host => 'localhost:3000' 41 | # } 42 | # config.action_mailer.delivery_method = :smtp 43 | # config.action_mailer.smtp_settings = { 44 | # :address => "localhost", :port => 1025 45 | # } 46 | 47 | config.action_mailer.default_url_options = { :host => 'localhost:3000'} 48 | config.action_mailer.delivery_method = :smtp 49 | config.action_mailer.smtp_settings = {:address => "localhost", :port => 1025} 50 | config.paperclip_defaults = { 51 | :storage => :s3, 52 | :s3_region => "us-east-1", 53 | :url => ":s3_domain_url", 54 | :s3_endpoint => 's3-us-east-1.amazonaws.com', 55 | :s3_protocol => "https", 56 | 57 | # A DEV_PAPERCLIP_BUCKET variable can be used to specify the production 58 | # image bucket (stuyspec-images). This is useful for getting the correct 59 | # URLs on images dumped from prod DB. 60 | :bucket => ENV['DEV_PAPERCLIP_BUCKET'].presence || 'stuyspec-media-testing', 61 | } 62 | # Raises error for missing translations 63 | # config.action_view.raise_on_missing_translations = true 64 | 65 | # Use an evented file watcher to asynchronously detect changes in source code, 66 | # routes, locales, etc. This feature depends on the listen gem. 67 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 68 | 69 | # For local subdomain support 70 | config.action_dispatch.tld_length = 0 71 | end 72 | 73 | Paperclip.options[:command_path] = "/usr/local/bin/" 74 | -------------------------------------------------------------------------------- /app/views/client_app/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 30 | The Stuyvesant Spectator - The Pulse of the Stuyvesant Student Body 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 62 | 63 | 64 | <%=raw render file: "public/client-app/index.html" %> 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/controllers/articles_controller.rb: -------------------------------------------------------------------------------- 1 | class ArticlesController < ApplicationController 2 | before_action :authenticate_user!, only: [:create, :update, :destroy] 3 | before_action :authenticate_admin!, only: [:create, :update, :destroy] 4 | before_action :set_article, only: [:show, :update, :destroy] 5 | 6 | 7 | # GET /articles 8 | def index 9 | if params[:section_id] 10 | @section = Section.friendly.find(params[:section_id]) 11 | @articles = Article 12 | .where("section_id = ?", @section.id) 13 | .joins("LEFT JOIN sections ON articles.section_id = sections.id") 14 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue + 192 * articles.volume DESC") 15 | else 16 | @articles = Article 17 | .joins("LEFT JOIN sections ON articles.section_id = sections.id") 18 | .order("articles.rank + 3 * sections.rank + 12 * articles.issue + 192 * articles.volume DESC") 19 | end 20 | 21 | if params[:query] 22 | @articles = PgSearch.multisearch(params[:query]) 23 | end 24 | 25 | @articles = @articles.order(:created_at).reverse if params[:order_by] == 'date' 26 | 27 | @articles = @articles.first(params[:limit].to_i) if params[:limit] 28 | 29 | @articles = @articles.select( 30 | :id, 31 | :title, 32 | :slug, 33 | :volume, 34 | :issue, 35 | :is_published, 36 | :created_at, 37 | :updated_at, 38 | :section_id, 39 | :rank, 40 | :preview 41 | ) if params[:content] == 'false' 42 | 43 | render json: @articles 44 | end 45 | 46 | # GET /articles/1 47 | def show 48 | render json: @article 49 | end 50 | 51 | # POST /articles 52 | def create 53 | @section = Section.friendly.find(params[:section_id]) 54 | @article = @section.articles.build(article_params) 55 | 56 | if @article.save 57 | render json: @article, status: :created, location: @article 58 | else 59 | render json: @article.errors, status: :unprocessable_entity 60 | end 61 | end 62 | 63 | # PATCH/PUT /articles/1 64 | def update 65 | if @article.update(article_params) 66 | render json: @article 67 | else 68 | render json: @article.errors, status: :unprocessable_entity 69 | end 70 | end 71 | 72 | # DELETE /articles/1 73 | def destroy 74 | @article.destroy 75 | end 76 | 77 | private 78 | # Use callbacks to share common setup or constraints between actions. 79 | def set_article 80 | @article = Article.friendly.find(params[:id]) 81 | end 82 | 83 | # Only allow a trusted parameter "white list" through. 84 | def article_params 85 | params.require(:article).permit( 86 | :title, 87 | :slug, 88 | :content, 89 | :volume, 90 | :issue, 91 | :is_published, 92 | :section_id, 93 | :summary, 94 | :rank, 95 | :created_at 96 | ) 97 | end 98 | def find_combined_rank(article) 99 | return article.rank + Section.friendly.find(article.section_id).rank * 1.5 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | host: <%= ENV["PG_HOST"] %> 21 | # For details on connection pooling, see Rails configuration guide 22 | # http://guides.rubyonrails.org/configuring.html#database-pooling 23 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 24 | 25 | development: 26 | <<: *default 27 | database: stuy-spec-api_development 28 | username: postgres 29 | password: 30 | # The specified database role being used to connect to postgres. 31 | # To create additional roles in postgres see `$ createuser --help`. 32 | # When left blank, postgres will use the default role. This is 33 | # the same name as the operating system user that initialized the database. 34 | #username: stuy-spec-api 35 | 36 | # The password associated with the postgres role (username). 37 | #password: 38 | 39 | # Connect on a TCP socket. Omitted by default since the client uses a 40 | # domain socket that doesn't need configuration. Windows does not have 41 | # domain sockets, so uncomment these lines. 42 | #host: localhost 43 | 44 | # The TCP port the server listens on. Defaults to 5432. 45 | # If your server runs on a different port number, change accordingly. 46 | #port: 5432 47 | 48 | # Schema search path. The server defaults to $user,public 49 | #schema_search_path: myapp,sharedapp,public 50 | 51 | # Minimum log levels, in increasing order: 52 | # debug5, debug4, debug3, debug2, debug1, 53 | # log, notice, warning, error, fatal, and panic 54 | # Defaults to warning. 55 | #min_messages: notice 56 | 57 | # Warning: The database defined as "test" will be erased and 58 | # re-generated from your development database when you run "rake". 59 | # Do not set this db to the same as development or production. 60 | test: 61 | <<: *default 62 | user: postgres 63 | database: stuy-spec-api_test 64 | 65 | # As with config/secrets.yml, you never want to store sensitive information, 66 | # like your database password, in your source code. If your source code is 67 | # ever seen by anyone, they now have access to your database. 68 | # 69 | # Instead, provide the password as a unix environment variable when you boot 70 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 71 | # for a full rundown on how to provide these environment variables in a 72 | # production deployment. 73 | # 74 | # On Heroku and other platform providers, you may have a full connection URL 75 | # available as an environment variable. For example: 76 | # 77 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 78 | # 79 | # You can use this database configuration with: 80 | # 81 | # production: 82 | # url: <%= ENV['DATABASE_URL'] %> 83 | # 84 | production: 85 | <<: *default 86 | database: <%= ENV['RDS_DB_NAME'] %> 87 | username: <%= ENV['RDS_USERNAME'] %> 88 | host: <%= ENV['RDS_HOSTNAME']%> 89 | password: <%= ENV['RDS_PASSWORD'] %> 90 | port: <%= ENV['RDS_PORT'] %> 91 | -------------------------------------------------------------------------------- /config/initializers/friendly_id.rb: -------------------------------------------------------------------------------- 1 | # FriendlyId Global Configuration 2 | # 3 | # Use this to set up shared configuration options for your entire application. 4 | # Any of the configuration options shown here can also be applied to single 5 | # models by passing arguments to the `friendly_id` class method or defining 6 | # methods in your model. 7 | # 8 | # To learn more, check out the guide: 9 | # 10 | # http://norman.github.io/friendly_id/file.Guide.html 11 | 12 | FriendlyId.defaults do |config| 13 | # ## Reserved Words 14 | # 15 | # Some words could conflict with Rails's routes when used as slugs, or are 16 | # undesirable to allow as slugs. Edit this list as needed for your app. 17 | config.use :reserved 18 | 19 | config.reserved_words = %w(new edit index session login logout users admin 20 | stylesheets assets javascripts images) 21 | 22 | # ## Friendly Finders 23 | # 24 | # Uncomment this to use friendly finders in all models. By default, if 25 | # you wish to find a record by its friendly id, you must do: 26 | # 27 | # MyModel.friendly.find('foo') 28 | # 29 | # If you uncomment this, you can do: 30 | # 31 | # MyModel.find('foo') 32 | # 33 | # This is significantly more convenient but may not be appropriate for 34 | # all applications, so you must explicity opt-in to this behavior. You can 35 | # always also configure it on a per-model basis if you prefer. 36 | # 37 | # Something else to consider is that using the :finders addon boosts 38 | # performance because it will avoid Rails-internal code that makes runtime 39 | # calls to `Module.extend`. 40 | # 41 | # config.use :finders 42 | # 43 | # ## Slugs 44 | # 45 | # Most applications will use the :slugged module everywhere. If you wish 46 | # to do so, uncomment the following line. 47 | # 48 | # config.use :slugged 49 | # 50 | # By default, FriendlyId's :slugged addon expects the slug column to be named 51 | # 'slug', but you can change it if you wish. 52 | # 53 | # config.slug_column = 'slug' 54 | # 55 | # When FriendlyId can not generate a unique ID from your base method, it appends 56 | # a UUID, separated by a single dash. You can configure the character used as the 57 | # separator. If you're upgrading from FriendlyId 4, you may wish to replace this 58 | # with two dashes. 59 | # 60 | # config.sequence_separator = '-' 61 | # 62 | # ## Tips and Tricks 63 | # 64 | # ### Controlling when slugs are generated 65 | # 66 | # As of FriendlyId 5.0, new slugs are generated only when the slug field is 67 | # nil, but if you're using a column as your base method can change this 68 | # behavior by overriding the `should_generate_new_friendly_id` method that 69 | # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave 70 | # more like 4.0. 71 | # 72 | # config.use Module.new { 73 | # def should_generate_new_friendly_id? 74 | # slug.blank? || _changed? 75 | # end 76 | # } 77 | # 78 | # FriendlyId uses Rails's `parameterize` method to generate slugs, but for 79 | # languages that don't use the Roman alphabet, that's not usually sufficient. 80 | # Here we use the Babosa library to transliterate Russian Cyrillic slugs to 81 | # ASCII. If you use this, don't forget to add "babosa" to your Gemfile. 82 | # 83 | # config.use Module.new { 84 | # def normalize_friendly_id(text) 85 | # text.to_slug.normalize! :transliterations => [:russian, :latin] 86 | # end 87 | # } 88 | end 89 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`. 18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or 19 | # `config/secrets.yml.key`. 20 | config.read_encrypted_secrets = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | # config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # serve public items for client-app and cms integration 27 | config.serve_static_assets = true 28 | 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "stuy-spec-api_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | 60 | # Ignore bad email addresses and do not raise email delivery errors. 61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 62 | # config.action_mailer.raise_delivery_errors = false 63 | 64 | config.action_mailer.default_url_options = { :host => 'api.stuyspec.com' } 65 | 66 | config.action_mailer.delivery_method = :smtp 67 | config.action_mailer.smtp_settings = { 68 | :address => "smtp.gmail.com", 69 | :port => 587, 70 | :user_name => 'web@stuyspec.com', 71 | :password => ENV['EMAIL_PASSWORD'], 72 | :authentication => 'plain', 73 | :enable_starttls_auto => true 74 | } 75 | 76 | config.paperclip_defaults = { 77 | :storage => :s3, 78 | :s3_region => "us-east-1", 79 | :url => ":s3_domain_url", 80 | :s3_endpoint => 's3-us-east-1.amazonaws.com', 81 | :bucket => 'stuyspec-images', 82 | :s3_protocol => "https" 83 | } 84 | 85 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 86 | # the I18n.default_locale when a translation cannot be found). 87 | config.i18n.fallbacks = true 88 | 89 | # Send deprecation notices to registered listeners. 90 | config.active_support.deprecation = :notify 91 | 92 | # Use default logging formatter so that PID and timestamp are not suppressed. 93 | config.log_formatter = ::Logger::Formatter.new 94 | 95 | # Use a different logger for distributed setups. 96 | # require 'syslog/logger' 97 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 98 | 99 | if ENV["RAILS_LOG_TO_STDOUT"].present? 100 | logger = ActiveSupport::Logger.new(STDOUT) 101 | logger.formatter = config.log_formatter 102 | config.logger = ActiveSupport::TaggedLogging.new(logger) 103 | end 104 | 105 | # Do not dump schema after migrations. 106 | config.active_record.dump_schema_after_migration = false 107 | end 108 | -------------------------------------------------------------------------------- /app/graphql/types/query_type.rb: -------------------------------------------------------------------------------- 1 | Types::QueryType = GraphQL::ObjectType.define do 2 | name "Query" 3 | # Add root-level fields here. 4 | # They will be entry points for queries on your schema. 5 | 6 | field :allSections, !types[Types::SectionType] do 7 | resolve -> (obj, args, ctx) { Section.all } 8 | end 9 | 10 | field :articlesBySectionID do 11 | type !types[Types::ArticleType] 12 | argument :section_id, !types.ID 13 | resolve -> (obj, args, ctx) { 14 | Resolvers::ArticleQueryFunction(Article.where(section_id: args["section_id"])) 15 | } 16 | end 17 | 18 | field :articleByContent do 19 | type Types::ArticleType 20 | argument :content, !types.String 21 | resolve -> (obj, args, ctx) { 22 | article = Article.find_by("content like ?", "%#{args['content']}%") 23 | if article.nil? 24 | return GraphQL::ExecutionError.new("No article found.") 25 | end 26 | return article 27 | } 28 | end 29 | 30 | field :sectionsByParentSectionID do 31 | type !types[Types::SectionType] 32 | argument :section_id, !types.ID 33 | resolve -> (obj, args, ctx) { 34 | Section.where(parent_id: args["section_id"]) 35 | } 36 | end 37 | 38 | field :searchArticles do 39 | type !types[Types::SearchDocumentType] 40 | argument :query, !types.String 41 | resolve -> (obj, args, ctx) { 42 | results = PgSearch.multisearch(args["query"]) 43 | results = results.select{ |r| r.searchable_type == 'Article'} 44 | results.select{ |r| !r.nil? && r.searchable.is_published } 45 | } 46 | end 47 | 48 | field :searchUnpublishedArticles do 49 | type !types[Types::SearchDocumentType] 50 | argument :query, !types.String 51 | resolve -> (obj, args, ctx) { 52 | if !Authentication::editor_is_valid(ctx) 53 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 54 | end 55 | results = PgSearch.multisearch(args["query"]) 56 | results = results.select{ |r| r.searchable_type == 'Article'} 57 | results.select{ |r| !r.nil? && !(r.searchable.is_published) } 58 | } 59 | end 60 | 61 | field :searchUsers do 62 | type !types[Types::UserType] 63 | argument :query, !types.String 64 | description "Find users by slug" 65 | resolve -> (obj, args, ctx) { 66 | if !Authentication::editor_is_valid(ctx) 67 | return GraphQL::ExecutionError.new("Invalid user token. Please log in.") 68 | end 69 | results = PgSearch.multisearch(args["query"]) 70 | results.map{|r| r.searchable if r.searchable_type == 'User'}.compact 71 | } 72 | end 73 | 74 | field :articleByID do 75 | type Types::ArticleType 76 | argument :id, !types.ID 77 | description "Find an article by ID" 78 | resolve -> (obj, args, ctx) { Article.find(args["id"]) } 79 | end 80 | 81 | field :articleBySlug do 82 | type Types::ArticleType 83 | argument :slug, !types.String 84 | description "Find an article by slug" 85 | resolve ->(obj, args, ctx) { Article.friendly.find(args["slug"])} 86 | end 87 | 88 | field :userByUID do 89 | type Types::UserType 90 | argument :uid, !types.String 91 | description "Find user by UID" 92 | resolve -> (obj, args, ctx) { User.find_by(uid: args["uid"]) } 93 | end 94 | 95 | field :userBySlug do 96 | type Types::UserType 97 | argument :slug, !types.String 98 | description "Find an user by slug" 99 | resolve ->(obj, args, ctx) { User.find_by(slug: args["slug"])} 100 | end 101 | 102 | field :userByFirstLastName do 103 | type Types::UserType 104 | argument :first_name, !types.String 105 | argument :last_name, !types.String 106 | description "Find user by first and last names" 107 | resolve -> (obj, args, ctx) { 108 | User.find_by(first_name: args["first_name"], last_name: args["last_name"]) 109 | } 110 | end 111 | 112 | field :allUsersWithRoles, !types[Types::UserType] do 113 | resolve -> (obj, args, ctx) { User.all.select do |user| user.roles.any? end } 114 | end 115 | 116 | field :latestArticles, function: Resolvers::GetLatestArticles.new 117 | 118 | field :latestUnpublishedArticles, function: Resolvers::GetLatestUnpublishedArticles.new 119 | 120 | field :sectionBySlug do 121 | type Types::SectionType 122 | argument :slug, !types.String 123 | description "Find an section by slug" 124 | resolve ->(obj, args, ctx) { Section.friendly.find(args["slug"])} 125 | end 126 | 127 | field :featuredSubsection, function: Resolvers::GetFeaturedSubsection.new 128 | 129 | field :topRankedArticles, function: Resolvers::GetTopRankedArticles.new 130 | 131 | field :featuredSections, !types[Types::SectionType] do 132 | resolve -> (obj, args, ctx) { Section.where(parent_id: nil).order("rank DESC") } 133 | end 134 | 135 | field :featuredArticlesBySectionSlug, function: Resolvers::GetFeaturedArticlesBySectionSlug.new 136 | 137 | field :featuredArticle, function: Resolvers::GetFeaturedArticle.new 138 | 139 | field :columnArticles, function: Resolvers::GetColumnArticles.new 140 | 141 | field :profileByUserAndRole, function: Resolvers::GetProfileByUserAndRole.new 142 | end 143 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | config.before(:suite) do 18 | Rails.application.load_seed # loading seeds 19 | end 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | 44 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 45 | # have no way to turn it off -- the option exists only for backwards 46 | # compatibility in RSpec 3). It causes shared context metadata to be 47 | # inherited by the metadata hash of host groups and examples, rather than 48 | # triggering implicit auto-inclusion in groups with matching metadata. 49 | config.shared_context_metadata_behavior = :apply_to_host_groups 50 | 51 | # The settings below are suggested to provide a good initial experience 52 | # with RSpec, but feel free to customize to your heart's content. 53 | =begin 54 | # This allows you to limit a spec run to individual examples or groups 55 | # you care about by tagging them with `:focus` metadata. When nothing 56 | # is tagged with `:focus`, all examples get run. RSpec also provides 57 | # aliases for `it`, `describe`, and `context` that include `:focus` 58 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 59 | config.filter_run_when_matching :focus 60 | 61 | # Allows RSpec to persist some state between runs in order to support 62 | # the `--only-failures` and `--next-failure` CLI options. We recommend 63 | # you configure your source control system to ignore this file. 64 | config.example_status_persistence_file_path = "spec/examples.txt" 65 | 66 | # Limits the available syntax to the non-monkey patched syntax that is 67 | # recommended. For more details, see: 68 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 69 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 70 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 71 | config.disable_monkey_patching! 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /spec/controllers/roles_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe RolesController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Role. As you add validations to Role, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # RolesController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | role = Role.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | role = Role.create! valid_attributes 55 | get :show, params: {id: role.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Role" do 63 | expect { 64 | post :create, params: {role: valid_attributes}, session: valid_session 65 | }.to change(Role, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new role" do 69 | 70 | post :create, params: {role: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(role_url(Role.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new role" do 79 | 80 | post :create, params: {role: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested role" do 94 | role = Role.create! valid_attributes 95 | put :update, params: {id: role.to_param, role: new_attributes}, session: valid_session 96 | role.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the role" do 101 | role = Role.create! valid_attributes 102 | 103 | put :update, params: {id: role.to_param, role: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the role" do 111 | role = Role.create! valid_attributes 112 | 113 | put :update, params: {id: role.to_param, role: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested role" do 122 | role = Role.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: role.to_param}, session: valid_session 125 | }.to change(Role, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/controllers/media_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe MediaController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Medium. As you add validations to Medium, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # MediaController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | medium = Medium.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | medium = Medium.create! valid_attributes 55 | get :show, params: {id: medium.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Medium" do 63 | expect { 64 | post :create, params: {medium: valid_attributes}, session: valid_session 65 | }.to change(Medium, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new medium" do 69 | 70 | post :create, params: {medium: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(medium_url(Medium.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new medium" do 79 | 80 | post :create, params: {medium: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested medium" do 94 | medium = Medium.create! valid_attributes 95 | put :update, params: {id: medium.to_param, medium: new_attributes}, session: valid_session 96 | medium.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the medium" do 101 | medium = Medium.create! valid_attributes 102 | 103 | put :update, params: {id: medium.to_param, medium: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the medium" do 111 | medium = Medium.create! valid_attributes 112 | 113 | put :update, params: {id: medium.to_param, medium: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested medium" do 122 | medium = Medium.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: medium.to_param}, session: valid_session 125 | }.to change(Medium, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/controllers/articles_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe ArticlesController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Article. As you add validations to Article, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # ArticlesController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | article = Article.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | article = Article.create! valid_attributes 55 | get :show, params: {id: article.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Article" do 63 | expect { 64 | post :create, params: {article: valid_attributes}, session: valid_session 65 | }.to change(Article, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new article" do 69 | 70 | post :create, params: {article: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(article_url(Article.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new article" do 79 | 80 | post :create, params: {article: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested article" do 94 | article = Article.create! valid_attributes 95 | put :update, params: {id: article.to_param, article: new_attributes}, session: valid_session 96 | article.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the article" do 101 | article = Article.create! valid_attributes 102 | 103 | put :update, params: {id: article.to_param, article: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the article" do 111 | article = Article.create! valid_attributes 112 | 113 | put :update, params: {id: article.to_param, article: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested article" do 122 | article = Article.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: article.to_param}, session: valid_session 125 | }.to change(Article, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/controllers/comments_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe CommentsController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Comment. As you add validations to Comment, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # CommentsController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | comment = Comment.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | comment = Comment.create! valid_attributes 55 | get :show, params: {id: comment.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Comment" do 63 | expect { 64 | post :create, params: {comment: valid_attributes}, session: valid_session 65 | }.to change(Comment, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new comment" do 69 | 70 | post :create, params: {comment: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(comment_url(Comment.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new comment" do 79 | 80 | post :create, params: {comment: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested comment" do 94 | comment = Comment.create! valid_attributes 95 | put :update, params: {id: comment.to_param, comment: new_attributes}, session: valid_session 96 | comment.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the comment" do 101 | comment = Comment.create! valid_attributes 102 | 103 | put :update, params: {id: comment.to_param, comment: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the comment" do 111 | comment = Comment.create! valid_attributes 112 | 113 | put :update, params: {id: comment.to_param, comment: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested comment" do 122 | comment = Comment.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: comment.to_param}, session: valid_session 125 | }.to change(Comment, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/controllers/profiles_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe ProfilesController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Profile. As you add validations to Profile, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # ProfilesController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | profile = Profile.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | profile = Profile.create! valid_attributes 55 | get :show, params: {id: profile.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Profile" do 63 | expect { 64 | post :create, params: {profile: valid_attributes}, session: valid_session 65 | }.to change(Profile, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new profile" do 69 | 70 | post :create, params: {profile: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(profile_url(Profile.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new profile" do 79 | 80 | post :create, params: {profile: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested profile" do 94 | profile = Profile.create! valid_attributes 95 | put :update, params: {id: profile.to_param, profile: new_attributes}, session: valid_session 96 | profile.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the profile" do 101 | profile = Profile.create! valid_attributes 102 | 103 | put :update, params: {id: profile.to_param, profile: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the profile" do 111 | profile = Profile.create! valid_attributes 112 | 113 | put :update, params: {id: profile.to_param, profile: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested profile" do 122 | profile = Profile.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: profile.to_param}, session: valid_session 125 | }.to change(Profile, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /spec/controllers/sections_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | # 21 | # Also compared to earlier versions of this generator, there are no longer any 22 | # expectations of assigns and templates rendered. These features have been 23 | # removed from Rails core in Rails 5, but can be added back in via the 24 | # `rails-controller-testing` gem. 25 | 26 | RSpec.describe SectionsController, type: :controller do 27 | 28 | # This should return the minimal set of attributes required to create a valid 29 | # Section. As you add validations to Section, be sure to 30 | # adjust the attributes here as well. 31 | let(:valid_attributes) { 32 | skip("Add a hash of attributes valid for your model") 33 | } 34 | 35 | let(:invalid_attributes) { 36 | skip("Add a hash of attributes invalid for your model") 37 | } 38 | 39 | # This should return the minimal set of values that should be in the session 40 | # in order to pass any filters (e.g. authentication) defined in 41 | # SectionsController. Be sure to keep this updated too. 42 | let(:valid_session) { {} } 43 | 44 | describe "GET #index" do 45 | it "returns a success response" do 46 | section = Section.create! valid_attributes 47 | get :index, params: {}, session: valid_session 48 | expect(response).to be_success 49 | end 50 | end 51 | 52 | describe "GET #show" do 53 | it "returns a success response" do 54 | section = Section.create! valid_attributes 55 | get :show, params: {id: section.to_param}, session: valid_session 56 | expect(response).to be_success 57 | end 58 | end 59 | 60 | describe "POST #create" do 61 | context "with valid params" do 62 | it "creates a new Section" do 63 | expect { 64 | post :create, params: {section: valid_attributes}, session: valid_session 65 | }.to change(Section, :count).by(1) 66 | end 67 | 68 | it "renders a JSON response with the new section" do 69 | 70 | post :create, params: {section: valid_attributes}, session: valid_session 71 | expect(response).to have_http_status(:created) 72 | expect(response.content_type).to eq('application/json') 73 | expect(response.location).to eq(section_url(Section.last)) 74 | end 75 | end 76 | 77 | context "with invalid params" do 78 | it "renders a JSON response with errors for the new section" do 79 | 80 | post :create, params: {section: invalid_attributes}, session: valid_session 81 | expect(response).to have_http_status(:unprocessable_entity) 82 | expect(response.content_type).to eq('application/json') 83 | end 84 | end 85 | end 86 | 87 | describe "PUT #update" do 88 | context "with valid params" do 89 | let(:new_attributes) { 90 | skip("Add a hash of attributes valid for your model") 91 | } 92 | 93 | it "updates the requested section" do 94 | section = Section.create! valid_attributes 95 | put :update, params: {id: section.to_param, section: new_attributes}, session: valid_session 96 | section.reload 97 | skip("Add assertions for updated state") 98 | end 99 | 100 | it "renders a JSON response with the section" do 101 | section = Section.create! valid_attributes 102 | 103 | put :update, params: {id: section.to_param, section: valid_attributes}, session: valid_session 104 | expect(response).to have_http_status(:ok) 105 | expect(response.content_type).to eq('application/json') 106 | end 107 | end 108 | 109 | context "with invalid params" do 110 | it "renders a JSON response with errors for the section" do 111 | section = Section.create! valid_attributes 112 | 113 | put :update, params: {id: section.to_param, section: invalid_attributes}, session: valid_session 114 | expect(response).to have_http_status(:unprocessable_entity) 115 | expect(response.content_type).to eq('application/json') 116 | end 117 | end 118 | end 119 | 120 | describe "DELETE #destroy" do 121 | it "destroys the requested section" do 122 | section = Section.create! valid_attributes 123 | expect { 124 | delete :destroy, params: {id: section.to_param}, session: valid_session 125 | }.to change(Section, :count).by(-1) 126 | end 127 | end 128 | 129 | end 130 | --------------------------------------------------------------------------------