├── log └── .keep ├── storage └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ ├── rebuild_database.rake │ └── rebuild_sample_database.rake ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── er_diagrams │ │ │ └── book_stores.png │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ └── application.css ├── models │ ├── concerns │ │ ├── .keep │ │ ├── samples │ │ │ ├── temporary_tableable.rb │ │ │ └── base.rb │ │ └── sortable.rb │ ├── authentication.rb │ ├── bookmark.rb │ ├── application_record.rb │ ├── sample_table.rb │ ├── samples │ │ ├── book_stores │ │ │ ├── book_author.rb │ │ │ ├── book_category.rb │ │ │ ├── category.rb │ │ │ ├── event.rb │ │ │ ├── store.rb │ │ │ ├── author.rb │ │ │ ├── book_sale.rb │ │ │ └── book.rb │ │ ├── book_stores_record.rb │ │ ├── temporary_migration.rb │ │ ├── query_handler.rb │ │ ├── query_parser.rb │ │ ├── temporary_table_creator.rb │ │ ├── query_validator.rb │ │ ├── bulk_insert.rb │ │ ├── temporary_migrator.rb │ │ └── sorting.rb │ ├── chapter.rb │ ├── work.rb │ ├── user.rb │ ├── sample_database_definition.rb │ ├── practice.rb │ └── sample_table_definition.rb ├── controllers │ ├── concerns │ │ ├── .keep │ │ └── api │ │ │ └── exception_handler.rb │ ├── home_controller.rb │ ├── api │ │ ├── base_controller.rb │ │ ├── samples │ │ │ ├── base_controller.rb │ │ │ └── queries_controller.rb │ │ ├── user_sessions_controller.rb │ │ ├── admin │ │ │ ├── sample_databases_controller.rb │ │ │ ├── base_controller.rb │ │ │ ├── users_controller.rb │ │ │ ├── works_controller.rb │ │ │ └── chapters_controller.rb │ │ ├── works_controller.rb │ │ ├── bookmarks_controller.rb │ │ ├── auth_users_controller.rb │ │ └── oauths_controller.rb │ ├── application_controller.rb │ └── development │ │ └── user_sessions_controller.rb ├── views │ ├── home │ │ └── index.html.erb │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── _google_analytics.html.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ └── api │ │ ├── admin │ │ ├── works │ │ │ ├── create.json.jbuilder │ │ │ ├── update.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── chapters │ │ │ ├── create.json.jbuilder │ │ │ ├── update.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── practices │ │ │ ├── create.json.jbuilder │ │ │ ├── update.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── users │ │ │ └── index.json.jbuilder │ │ └── sample_databases │ │ │ └── index.json.jbuilder │ │ ├── auth_users │ │ ├── show.json.jbuilder │ │ └── update.json.jbuilder │ │ ├── samples │ │ └── queries │ │ │ └── execute.json.jbuilder │ │ ├── works │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ │ ├── bookmarks │ │ └── index.json.jbuilder │ │ └── practices │ │ └── show.json.jbuilder ├── javascript │ ├── assets │ │ ├── main-visual.png │ │ ├── main-visual@2x.png │ │ ├── man2.svg │ │ ├── man.svg │ │ ├── woman.svg │ │ └── logo.svg │ ├── plugins │ │ ├── vue-draggable.js │ │ ├── vuetify.js │ │ ├── axios.js │ │ ├── vee-validate.js │ │ └── vuetify │ │ │ └── theme.js │ ├── utils │ │ ├── format-date.js │ │ ├── tabindex.js │ │ ├── exception.js │ │ └── helpers.js │ ├── views │ │ ├── admin │ │ │ ├── dashboard │ │ │ │ └── index.vue │ │ │ ├── works │ │ │ │ └── components │ │ │ │ │ ├── WorksFooterButton.vue │ │ │ │ │ ├── WorksDeleteModal.vue │ │ │ │ │ ├── WorksDetailModal.vue │ │ │ │ │ └── WorksFooter.vue │ │ │ └── users │ │ │ │ └── components │ │ │ │ ├── UsersDeleteModal.vue │ │ │ │ ├── UsersDetailModal.vue │ │ │ │ └── UsersTable.vue │ │ ├── mypage │ │ │ └── components │ │ │ │ ├── MypageCardTitle.vue │ │ │ │ ├── MypageAccountDeleteDialog.vue │ │ │ │ ├── MypageClearCount.vue │ │ │ │ └── MypageBookmark.vue │ │ ├── practice │ │ │ └── components │ │ │ │ ├── PracticeSheet.vue │ │ │ │ ├── PracticeResizer.vue │ │ │ │ ├── PracticeMenuErDiagram.vue │ │ │ │ ├── PracticeTabItemEditor.vue │ │ │ │ ├── PracticeModalPreferenceList.vue │ │ │ │ ├── PracticeQuestion.vue │ │ │ │ ├── PracticeTable.vue │ │ │ │ ├── PracticeModalExampleAnswer.vue │ │ │ │ ├── PracticeTabItemErDiagram.vue │ │ │ │ ├── PracticeTabItemSampleDatabase.vue │ │ │ │ ├── PracticeToolbarEditor.vue │ │ │ │ ├── PracticeFooter.vue │ │ │ │ └── PracticeEditor.vue │ │ ├── top │ │ │ ├── components │ │ │ │ ├── TopSectionTitle.vue │ │ │ │ ├── TopAbout.vue │ │ │ │ ├── TopCatch.vue │ │ │ │ └── TopFaq.vue │ │ │ └── index.vue │ │ ├── errors │ │ │ ├── 404.vue │ │ │ └── 500.vue │ │ ├── login │ │ │ └── index.vue │ │ ├── work │ │ │ ├── components │ │ │ │ ├── WorkDetail.vue │ │ │ │ └── WorkList.vue │ │ │ └── index.vue │ │ └── works │ │ │ ├── components │ │ │ └── WorksCard.vue │ │ │ └── index.vue │ ├── app.vue │ ├── channels │ │ ├── index.js │ │ └── consumer.js │ ├── components │ │ ├── BaseImage.vue │ │ ├── BaseDivider.vue │ │ ├── BaseAlert.vue │ │ ├── BaseIcon.vue │ │ ├── BaseTabItem.vue │ │ ├── app │ │ │ ├── AppContainer.vue │ │ │ ├── AppContainerAdmin.vue │ │ │ ├── AppPageHeading.vue │ │ │ ├── AppIconLock.vue │ │ │ ├── AppLogo.vue │ │ │ ├── AppLogoLink.vue │ │ │ ├── AppFlashMessage.vue │ │ │ └── AppIconBookmark.vue │ │ ├── BaseAvatar.vue │ │ ├── BaseTab.vue │ │ ├── globals.js │ │ ├── BaseButtonClose.vue │ │ ├── BaseButtonIcon.vue │ │ ├── BaseProgressLinear.vue │ │ ├── BaseButtonText.vue │ │ ├── BaseButton.vue │ │ ├── BaseTooltip.vue │ │ ├── BaseTable.vue │ │ ├── BaseDrawer.vue │ │ ├── BaseSwitch.vue │ │ ├── BaseMenu.vue │ │ └── BaseModal.vue │ ├── entrypoints │ │ └── application.js │ ├── layout │ │ ├── admin │ │ │ ├── components │ │ │ │ ├── TheView.vue │ │ │ │ ├── AdminNavbar │ │ │ │ │ └── components │ │ │ │ │ │ ├── AdminNavbarLogout.vue │ │ │ │ │ │ └── AdminNavbarItem.vue │ │ │ │ └── TheSidebar.vue │ │ │ └── index.vue │ │ └── default │ │ │ ├── components │ │ │ ├── Navbar │ │ │ │ └── components │ │ │ │ │ ├── NavbarLogo.vue │ │ │ │ │ └── NavbarItem.vue │ │ │ ├── TheView.vue │ │ │ ├── WorkDrawer │ │ │ │ └── components │ │ │ │ │ └── WorkDrawerHeading.vue │ │ │ ├── LoginModal.vue │ │ │ ├── UserDrawer │ │ │ │ └── components │ │ │ │ │ └── UserDrawerHeading.vue │ │ │ └── TheFooter.vue │ │ │ └── index.vue │ ├── i18n │ │ ├── index.js │ │ └── messages │ │ │ └── ja.json │ ├── data │ │ ├── judgement.json │ │ ├── features.json │ │ ├── preferences.json │ │ ├── nav.json │ │ ├── faq.json │ │ ├── flash-messages.json │ │ └── keyboard-events.json │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── practices.js │ │ │ ├── users.js │ │ │ ├── works.js │ │ │ └── app.js │ └── styles │ │ ├── typography.scss │ │ └── index.scss ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ └── application_mailer.rb ├── jobs │ └── application_job.rb └── helpers │ └── application_helper.rb ├── config ├── settings.yml ├── locales │ ├── enums.yml │ └── en.yml ├── spring.rb ├── initializers │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── permissions_policy.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── meta_tags.rb │ └── content_security_policy.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── settings │ ├── production.yml │ ├── test.yml │ ├── development.yml │ └── staging.yml ├── vite.json ├── credentials.yml.enc ├── storage.yml ├── routes.rb ├── application.rb └── puma.rb ├── .browserslistrc ├── Procfile.dev ├── spec ├── support │ ├── simplecov.rb │ ├── factory_bot.rb │ ├── shoulda_matchers.rb │ ├── capybara.rb │ ├── response_helper.rb │ ├── bullet.rb │ ├── database_cleaner.rb │ └── user_sessions_helper.rb ├── factories │ ├── sample_tables.rb │ ├── chapters.rb │ ├── users.rb │ ├── works.rb │ └── practices.rb ├── lib │ └── tasks │ │ └── import_sample_csv_data_rake_spec.rb ├── requests │ └── api │ │ ├── user_sessions_spec.rb │ │ └── admin │ │ ├── admin_authentication_spec.rb │ │ ├── sample_databases_spec.rb │ │ └── users_spec.rb └── models │ ├── chapter_spec.rb │ └── sample_table_spec.rb ├── public ├── icon.png ├── ogp.png ├── favicon.ico ├── apple-touch-icon.png ├── robots.txt ├── apple-touch-icon-precomposed.png └── icon.svg ├── db ├── csv │ └── book_stores │ │ ├── events.csv │ │ ├── stores.csv │ │ ├── categories.csv │ │ ├── book_categories.csv │ │ ├── book_authors.csv │ │ ├── authors.csv │ │ ├── books.csv │ │ └── book_sales.csv ├── book_stores_migrate │ ├── 20211116095204_create_categories.rb │ ├── 20211116094526_create_authors.rb │ ├── 20211116095233_create_stores.rb │ ├── 20211116090723_create_books.rb │ ├── 20211116095116_create_book_authors.rb │ ├── 20211116095212_create_book_categories.rb │ └── 20211116095247_create_book_sales.rb ├── book_stores_migrate_temporary │ └── create_events.rb └── migrate │ ├── 20211116065257_create_chapters.rb │ ├── 20220818094349_create_bookmarks.rb │ ├── 20211116065620_create_sample_tables.rb │ ├── 20251002204605_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── 20220809123932_sorcery_external.rb │ ├── 20220731181706_sorcery_core.rb │ ├── 20211116065231_create_works.rb │ ├── 20211116065548_create_practices.rb │ ├── 20250921025156_drop_model_databases_and_tables.rb │ ├── 20251002204603_add_service_name_to_active_storage_blobs.active_storage.rb │ └── 20251002204604_create_active_storage_variant_records.active_storage.rb ├── bin ├── rake ├── rails ├── rspec ├── rubocop ├── spring ├── yarn ├── vite └── setup ├── .prettierrc ├── config.ru ├── .babelrc ├── .eslintrc ├── Rakefile ├── postcss.config.js ├── .gitattributes ├── .rubocop_todo.yml ├── jest.config.js ├── vite.config.mts └── package.json /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.6 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | 2 | vite: bin/vite dev 3 | web: bin/rails s 4 | -------------------------------------------------------------------------------- /spec/support/simplecov.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | -------------------------------------------------------------------------------- /app/views/api/admin/works/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @work.attributes 2 | -------------------------------------------------------------------------------- /app/views/api/admin/works/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @work.attributes 2 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/public/ogp.png -------------------------------------------------------------------------------- /app/views/api/admin/chapters/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @chapter.attributes 2 | -------------------------------------------------------------------------------- /app/views/api/admin/chapters/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @chapter.attributes 2 | -------------------------------------------------------------------------------- /app/views/api/auth_users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! current_user&.attributes 2 | -------------------------------------------------------------------------------- /app/views/api/auth_users/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! current_user&.attributes 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/views/api/samples/queries/execute.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @result, :status, :values 2 | -------------------------------------------------------------------------------- /db/csv/book_stores/events.csv: -------------------------------------------------------------------------------- 1 | id,name,max_num 2 | 1,私達はなぜ世界を旅するのか,30 3 | 2,最新の英語学習法を学ぼう,15 4 | -------------------------------------------------------------------------------- /db/csv/book_stores/stores.csv: -------------------------------------------------------------------------------- 1 | id,name,holiday 2 | 1,A店,月曜日 3 | 2,B店,年中無休 4 | 3,オンライン,ー 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /app/views/api/works/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @works, :id, :name, :slug, :description, :enabled 2 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/factories/sample_tables.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :sample_table do 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/views/api/admin/works/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @works do |work| 2 | json.merge! work.attributes 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/javascript/assets/main-visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/app/javascript/assets/main-visual.png -------------------------------------------------------------------------------- /config/locales/enums.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | enums: 3 | user: 4 | role: 5 | admin: '管理者' 6 | general: '一般' 7 | -------------------------------------------------------------------------------- /app/javascript/assets/main-visual@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/app/javascript/assets/main-visual@2x.png -------------------------------------------------------------------------------- /app/views/api/admin/chapters/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @chapters do |chapter| 2 | json.merge! chapter.attributes 3 | end 4 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /app/assets/images/er_diagrams/book_stores.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/take-paolo/sqlab/HEAD/app/assets/images/er_diagrams/book_stores.png -------------------------------------------------------------------------------- /app/models/authentication.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Authentication < ApplicationRecord 4 | belongs_to :user 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < ApplicationController 4 | def index; end 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 | -------------------------------------------------------------------------------- /app/views/api/admin/practices/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @practice.attributes 2 | json.sample_table_ids @practice.sample_tables.pluck(:uid) 3 | -------------------------------------------------------------------------------- /app/views/api/admin/practices/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.merge! @practice.attributes 2 | json.sample_table_ids @practice.sample_tables.pluck(:uid) 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "semi": false, 5 | "arrowParens": "avoid", 6 | "singleAttributePerLine": true 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/plugins/vue-draggable.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueDraggable from 'vuedraggable' 3 | 4 | Vue.component('VueDraggable', VueDraggable) 5 | -------------------------------------------------------------------------------- /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/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | config.before :all do 4 | FactoryBot.reload 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /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 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/javascript/utils/format-date.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | dayjs.locale('ja') 4 | 5 | export function formatDate(date) { 6 | return dayjs(date).format('YYYY/MM/DD, HH:mm') 7 | } 8 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class BaseController < ApplicationController 5 | include Api::ExceptionHandler 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/views/admin/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/views/api/admin/practices/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @practices do |practice| 2 | json.merge! practice.attributes 3 | json.sample_table_ids practice.sample_tables.pluck(:uid) 4 | end 5 | -------------------------------------------------------------------------------- /spec/factories/chapters.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :chapter do 3 | work 4 | sequence(:name) { |n| "chapter_name#{n}" } 5 | sequence(:order_number) { |n| n } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each, type: :system) do 3 | driven_by :selenium, using: :headless_chrome, screen_size: [1920, 1080] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "modules": false }]], 3 | "env": { 4 | "test": { 5 | "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | import.meta.glob('./**/*_channel.js', { eager: true }) 4 | -------------------------------------------------------------------------------- /spec/support/response_helper.rb: -------------------------------------------------------------------------------- 1 | module ResponseHelper 2 | def body 3 | JSON.parse(response.body) 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include ResponseHelper 9 | end 10 | -------------------------------------------------------------------------------- /app/views/api/admin/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @users do |user| 2 | json.extract! user, :id, :name, :email, :role, :created_at, :updated_at 3 | json.role I18n.t("enums.user.role.#{user.role}") 4 | end 5 | -------------------------------------------------------------------------------- /app/javascript/components/BaseImage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /app/models/bookmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Bookmark < ApplicationRecord 4 | belongs_to :user 5 | belongs_to :practice 6 | 7 | validates :user_id, uniqueness: { scope: :practice_id } 8 | end 9 | -------------------------------------------------------------------------------- /app/javascript/components/BaseDivider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /db/csv/book_stores/categories.csv: -------------------------------------------------------------------------------- 1 | id,name 2 | 1,文芸・評論 3 | 2,サイエンス・テクノロジー 4 | 3,旅行 5 | 4,歴史・地理 6 | 5,ビジネス・経済 7 | 6,語学 8 | 7,教育・自己啓発 9 | 8,暮らし・健康・料理 10 | 9,政治・社会 11 | 10,アート・建築・デザイン 12 | 11,絵本・児童書 13 | 12,未分類 14 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | 6 | connects_to database: { writing: :primary, reading: :primary } 7 | end 8 | -------------------------------------------------------------------------------- /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 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /app/views/api/bookmarks/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @bookmark_practices do |practice| 2 | json.work_slug practice.chapter.work.slug 3 | json.extract! practice, :id, :name, :enabled, :requires_auth 4 | json.bookmarked true 5 | end 6 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116095204_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :categories do |t| 4 | t.string :name, null: false 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require 'bundler/setup' 8 | load Gem.bin_path('rspec-core', 'rspec') 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:vue/recommended", 11 | "prettier" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/controllers/api/samples/base_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Samples 5 | class BaseController < ApplicationController 6 | include Api::ExceptionHandler 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/javascript/entrypoints/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Vite 2 | import Rails from '@rails/ujs' 3 | import * as ActiveStorage from '@rails/activestorage' 4 | import '../channels' 5 | 6 | Rails.start() 7 | ActiveStorage.start() 8 | -------------------------------------------------------------------------------- /app/models/sample_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SampleTable < ApplicationRecord 4 | belongs_to :practice 5 | 6 | validates :uid, presence: true, numericality: { only_integer: true }, uniqueness: { scope: :practice_id } 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/utils/tabindex.js: -------------------------------------------------------------------------------- 1 | export function addTabIndexTo(selectors, node) { 2 | const elements = node.querySelectorAll(selectors) 3 | elements.forEach(el => { 4 | if (!el.hasAttribute('tabIndex')) { 5 | el.tabIndex = 0 6 | } 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116094526_create_authors.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthors < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :authors do |t| 4 | t.string :name, null: false 5 | t.string :gender, null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116095233_create_stores.rb: -------------------------------------------------------------------------------- 1 | class CreateStores < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :stores do |t| 4 | t.string :name, null: false 5 | t.string :holiday, null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/javascript/layout/admin/components/TheView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /app/controllers/api/user_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class UserSessionsController < BaseController 5 | before_action :require_login 6 | 7 | def destroy 8 | logout 9 | head :ok 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/javascript/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | // messages 5 | import ja from './messages/ja' 6 | 7 | Vue.use(VueI18n) 8 | 9 | const i18n = new VueI18n({ 10 | locale: 'ja', 11 | messages: { ja }, 12 | }) 13 | 14 | export default i18n 15 | -------------------------------------------------------------------------------- /app/views/api/practices/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @practice, :id, :name, :question, :answer, :sample_database_id 2 | if @with_sample_data 3 | # ER diagram URL 4 | json.er_diagram_url @er_diagram_url 5 | 6 | # Sample table records 7 | json.sample_table_data @sample_table_data 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/javascript/components/BaseAlert.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /app/javascript/views/mypage/components/MypageCardTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/views/api/admin/sample_databases/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @sample_databases do |sample_database| 2 | json.extract! sample_database, :id, :name 3 | 4 | # Sample table list 5 | json.tables sample_database.available_tables do |table| 6 | json.extract! table, :id, :name 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /db/book_stores_migrate_temporary/create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < Samples::TemporaryMigration 2 | def create_temporary_table 3 | create_table :events, temporary: true, force: true do |t| 4 | t.string :name, null: false 5 | t.integer :max_num, null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116090723_create_books.rb: -------------------------------------------------------------------------------- 1 | class CreateBooks < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :books do |t| 4 | t.string :name, null: false 5 | t.integer :release_year 6 | t.integer :total_page, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/javascript/data/judgement.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": { 3 | "title": "正解", 4 | "text": "おめでとうございます!\n次の問題にも挑戦してみましょう!", 5 | "isSuccess": true 6 | }, 7 | "fail": { 8 | "title": "不正解", 9 | "text": "想定結果に合わせて解答してください。\n実行結果と想定結果を比較してみましょう。", 10 | "isSuccess": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/support/bullet.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | if Bullet.enable? 3 | config.before do 4 | Bullet.start_request 5 | end 6 | config.after do 7 | Bullet.perform_out_of_channel_notifications if Bullet.notification? 8 | Bullet.end_request 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/layouts/_google_analytics.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. 3 | 4 | import { createConsumer } from '@rails/actioncable' 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | 6 | before_action :snakeize_params 7 | 8 | def snakeize_params 9 | params.deep_transform_keys!(&:underscore) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/development/user_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Development 4 | class UserSessionsController < ApplicationController 5 | def login_as 6 | user = User.find(params[:user_id]) 7 | auto_login(user) 8 | 9 | head :ok 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/book_author.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class BookAuthor < BookStoresRecord 6 | belongs_to :book 7 | belongs_to :author 8 | 9 | validates :book_id, uniqueness: { scope: :author_id } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/book_category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class BookCategory < BookStoresRecord 6 | belongs_to :book 7 | belongs_to :category 8 | 9 | validates :book_id, uniqueness: { scope: :category_id } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | 7 | # Configure Jbuilder to use camelCase for JSON keys (for frontend compatibility) 8 | Jbuilder.key_format camelize: :lower 9 | Jbuilder.deep_format_keys true 10 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | require 'database_cleaner/active_record' 2 | 3 | Rails.application.load_tasks 4 | 5 | RSpec.configure do |config| 6 | config.before(:suite) do 7 | Rake::Task['import:sample_csv_data'].execute 8 | end 9 | 10 | # No cleanup needed since import task already clears existing records every time 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | sequence(:name) { |n| "user#{n}" } 4 | sequence(:email) { |n| "email#{n}@test.com" } 5 | role { 0 } 6 | password { 'password' } 7 | password_confirmation { 'password' } 8 | 9 | trait :admin do 10 | role { 10 } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/javascript/data/features.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "豊富な問題量", 4 | "text": "練習問題が豊富で基礎から応用まで幅広く学習することができます。\n練習を積み上げてSQLを自由自在に操れるようになりましょう。", 5 | "icon": "practice-list.svg" 6 | }, 7 | { 8 | "title": "環境構築不要", 9 | "text": "面倒な環境構築やデータの用意は不要です。\nブラウザ上ですぐに実践できます。\n今すぐ挑戦してみましょう!", 10 | "icon": "laptop.svg" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /app/javascript/data/preferences.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoSlide": { 3 | "label": "実行結果タブへのオートスライド", 4 | "description": "ユーザーが「実行」または「答え合わせ」ボタンを押した時に「実行結果」タブへ自動的にスライドするかどうかを設定します。", 5 | "enabled": true 6 | }, 7 | "shortcut": { 8 | "label": "ショートカット", 9 | "description": "ショートカットを有効にするかどうかを設定します。", 10 | "enabled": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class Category < BookStoresRecord 6 | has_many :book_categories, dependent: :destroy 7 | has_many :books, through: :book_categories 8 | 9 | validates :name, presence: true 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20211116065257_create_chapters.rb: -------------------------------------------------------------------------------- 1 | class CreateChapters < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :chapters do |t| 4 | t.references :work, null: false, foreign_key: true 5 | t.string :name, null: false 6 | t.integer :order_number, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/admin/sample_databases_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Admin 5 | class SampleDatabasesController < BaseController 6 | def index 7 | @sample_databases = SampleDatabaseDefinition.order(:id) 8 | 9 | render 'index', formats: :json 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class Event < BookStoresRecord 6 | include Samples::TemporaryTableable 7 | 8 | validates :name, presence: true 9 | validates :max_num, presence: true, numericality: { only_integer: true } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark the yarn lockfile as having been generated. 7 | yarn.lock linguist-generated 8 | 9 | # Mark any vendored files as having been vendored. 10 | vendor/* linguist-vendored 11 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class Store < BookStoresRecord 6 | has_many :book_sales, dependent: :destroy 7 | has_many :books, through: :book_sales 8 | 9 | validates :name, presence: true 10 | validates :holiday, presence: true 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20220818094349_create_bookmarks.rb: -------------------------------------------------------------------------------- 1 | class CreateBookmarks < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :bookmarks do |t| 4 | t.references :user, foreign_key: true 5 | t.references :practice, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :bookmarks, [:user_id, :practice_id], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: sqlab_production 11 | 12 | staging: 13 | adapter: redis 14 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 15 | channel_prefix: sqlab_staging 16 | -------------------------------------------------------------------------------- /app/javascript/layout/admin/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /app/models/concerns/samples/temporary_tableable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module TemporaryTableable 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | delegate :migrate_temporary, to: :temporary_migrator 9 | 10 | def temporary_migrator 11 | TemporaryMigrator.new(self) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/settings/production.yml: -------------------------------------------------------------------------------- 1 | sorcery: 2 | google: 3 | callback_url: 'https://sqlab.net/api/oauth/callback?provider=google' 4 | github: 5 | client_id: <%= Rails.application.credentials[:github][:production][:client_id] %> 6 | client_secret: <%= Rails.application.credentials[:github][:production][:client_secret] %> 7 | callback_url: 'https://sqlab.net/api/oauth/callback?provider=github' 8 | -------------------------------------------------------------------------------- /config/settings/test.yml: -------------------------------------------------------------------------------- 1 | sorcery: 2 | google: 3 | callback_url: 'http://127.0.0.1:3000/api/oauth/callback?provider=google' 4 | github: 5 | client_id: <%= Rails.application.credentials[:github][:development][:client_id] %> 6 | client_secret: <%= Rails.application.credentials[:github][:development][:client_secret] %> 7 | callback_url: 'http://127.0.0.1:3000/api/oauth/callback?provider=github' 8 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116095116_create_book_authors.rb: -------------------------------------------------------------------------------- 1 | class CreateBookAuthors < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :book_authors do |t| 4 | t.references :book, null: false, foreign_key: true 5 | t.references :author, null:false, foreign_key: true 6 | end 7 | 8 | add_index :book_authors, [:book_id, :author_id], unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20211116065620_create_sample_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateSampleTables < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :sample_tables do |t| 4 | t.references :practice, null:false, foreign_key: true 5 | t.integer :uid, null:false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :sample_tables, [:practice_id, :uid], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20251002204605_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "sourceCodeDir": "app/javascript", 4 | "entrypointsDir": "entrypoints", 5 | "watchAdditionalPaths": [] 6 | }, 7 | "development": { 8 | "autoBuild": true, 9 | "publicOutputDir": "vite-dev", 10 | "port": 3036 11 | }, 12 | "test": { 13 | "autoBuild": true, 14 | "publicOutputDir": "vite-test", 15 | "port": 3037 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/settings/development.yml: -------------------------------------------------------------------------------- 1 | sorcery: 2 | google: 3 | callback_url: 'http://127.0.0.1:3000/api/oauth/callback?provider=google' 4 | github: 5 | client_id: <%= Rails.application.credentials[:github][:development][:client_id] %> 6 | client_secret: <%= Rails.application.credentials[:github][:development][:client_secret] %> 7 | callback_url: 'http://127.0.0.1:3000/api/oauth/callback?provider=github' 8 | -------------------------------------------------------------------------------- /app/javascript/components/BaseIcon.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /app/javascript/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/dist/vuetify.min.css' 4 | import light from './vuetify/theme' 5 | 6 | Vue.use(Vuetify) 7 | 8 | const options = { 9 | theme: { 10 | themes: { 11 | light, 12 | }, 13 | options: { 14 | customProperties: true, 15 | }, 16 | }, 17 | } 18 | 19 | export default new Vuetify(options) 20 | -------------------------------------------------------------------------------- /app/models/samples/book_stores_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class BookStoresRecord < ApplicationRecord 5 | self.abstract_class = true 6 | 7 | DATABASE_NAME = SampleDatabaseDefinition.find_by(name: 'book_stores').name.to_sym 8 | 9 | connects_to database: { writing: DATABASE_NAME, reading: DATABASE_NAME } 10 | 11 | include Samples::Base 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116095212_create_book_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateBookCategories < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :book_categories do |t| 4 | t.references :book, null: false, foreign_key: true 5 | t.references :category, null:false, foreign_key: true 6 | end 7 | 8 | add_index :book_categories, [:book_id, :category_id], unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/tasks/import_sample_csv_data_rake_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe 'rake task import:sample_csv_data', type: :task do 6 | let(:task) { Rake::Task['import:sample_csv_data'] } 7 | 8 | describe 'successful execution' do 9 | it 'completes successfully' do 10 | expect { task.invoke }.not_to raise_error 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/author.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class Author < BookStoresRecord 6 | has_many :book_authors, dependent: :destroy 7 | has_many :books, through: :book_authors 8 | 9 | validates :name, presence: true 10 | validates :gender, presence: true, inclusion: { in: %w[男性 女性 不明] } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/admin/base_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Admin 5 | class BaseController < ApplicationController 6 | include Api::ExceptionHandler 7 | 8 | before_action :require_login 9 | before_action :check_admin 10 | 11 | def check_admin 12 | head :unauthorized unless current_user.admin? 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20220809123932_sorcery_external.rb: -------------------------------------------------------------------------------- 1 | class SorceryExternal < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :authentications do |t| 4 | t.integer :user_id, null: false 5 | t.string :provider, :uid, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | add_index :authentications, [:provider, :uid] 11 | add_index :authentications, :user_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/settings/staging.yml: -------------------------------------------------------------------------------- 1 | sorcery: 2 | google: 3 | callback_url: 'https://sqlabapp-staging.herokuapp.com/api/oauth/callback?provider=google' 4 | github: 5 | client_id: <%= Rails.application.credentials[:github][:staging][:client_id] %> 6 | client_secret: <%= Rails.application.credentials[:github][:staging][:client_secret] %> 7 | callback_url: 'https://sqlabapp-staging.herokuapp.com/api/oauth/callback?provider=github' 8 | -------------------------------------------------------------------------------- /app/controllers/api/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Admin 5 | class UsersController < BaseController 6 | def index 7 | @users = User.all 8 | 9 | render 'index', formats: :json 10 | end 11 | 12 | def destroy 13 | user = User.find(params[:id]) 14 | user.destroy! 15 | 16 | head :ok 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/Navbar/components/NavbarLogo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /app/javascript/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import app from './modules/app' 5 | import works from './modules/works' 6 | import practices from './modules/practices' 7 | import users from './modules/users' 8 | 9 | Vue.use(Vuex) 10 | 11 | const store = new Vuex.Store({ 12 | modules: { 13 | app, 14 | works, 15 | practices, 16 | users, 17 | }, 18 | }) 19 | 20 | export default store 21 | -------------------------------------------------------------------------------- /spec/support/user_sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module UserSessionsHelper 2 | def login_as(user) 3 | get "/login_as/#{user.id}" 4 | expect(logged_in?).to be_truthy 5 | end 6 | 7 | def logged_in? 8 | !!current_user 9 | end 10 | 11 | def current_user 12 | @current_user ||= User.find(session[:user_id]) if session[:user_id] 13 | end 14 | end 15 | 16 | RSpec.configure do |config| 17 | config.include UserSessionsHelper 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeSheet.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /app/javascript/components/BaseTabItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /db/migrate/20220731181706_sorcery_core.rb: -------------------------------------------------------------------------------- 1 | class SorceryCore < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :name, null: false 5 | t.string :email, null: false 6 | t.integer :role, null:false, default: 0 7 | t.string :crypted_password 8 | t.string :salt 9 | 10 | t.timestamps null: false 11 | end 12 | 13 | add_index :users, :email, unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppContainer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /app/javascript/components/BaseAvatar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeResizer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /app/javascript/views/top/components/TopSectionTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /app/javascript/components/BaseTab.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppContainerAdmin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /app/javascript/utils/exception.js: -------------------------------------------------------------------------------- 1 | import router from '@/router/index' 2 | 3 | export function handleException(err, path = '') { 4 | path = path || router.history._startLocation 5 | 6 | if (err.response?.status === 404) { 7 | router.push({ name: 'PageNotFound', params: { 0: path } }) 8 | } else if (err.response?.status === 401) { 9 | router.push({ name: 'Login' }) 10 | } else { 11 | router.push({ name: 'InternalServerError', params: { 0: path } }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/csv/book_stores/book_categories.csv: -------------------------------------------------------------------------------- 1 | id,book_id,category_id 2 | 1,1,2 3 | 2,2,2 4 | 3,3,1 5 | 4,4,2 6 | 5,5,8 7 | 6,6,3 8 | 7,7,1 9 | 8,8,12 10 | 9,9,5 11 | 10,10,2 12 | 11,11,2 13 | 12,12,2 14 | 13,13,6 15 | 14,14,12 16 | 15,15,8 17 | 16,16,8 18 | 17,17,5 19 | 18,18,5 20 | 19,19,8 21 | 20,20,9 22 | 21,21,2 23 | 22,22,7 24 | 23,23,10 25 | 24,24,2 26 | 25,25,4 27 | 26,25,7 28 | 27,26,11 29 | 28,27,2 30 | 29,28,2 31 | 30,29,1 32 | 31,30,4 33 | 32,30,7 34 | -------------------------------------------------------------------------------- /app/javascript/views/errors/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /app/javascript/layout/default/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /app/javascript/layout/admin/components/AdminNavbar/components/AdminNavbarLogout.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /db/csv/book_stores/book_authors.csv: -------------------------------------------------------------------------------- 1 | id,book_id,author_id 2 | 1,1,1 3 | 2,1,18 4 | 3,2,7 5 | 4,3,16 6 | 5,4,2 7 | 6,5,15 8 | 7,5,17 9 | 8,6,3 10 | 9,7,10 11 | 10,8,19 12 | 11,9,5 13 | 12,10,14 14 | 13,11,14 15 | 14,12,9 16 | 15,13,4 17 | 16,14,28 18 | 17,15,20 19 | 18,16,13 20 | 19,17,6 21 | 20,18,8 22 | 21,19,11 23 | 22,20,12 24 | 23,21,21 25 | 24,22,22 26 | 25,23,23 27 | 26,24,2 28 | 27,24,24 29 | 28,25,25 30 | 29,26,26 31 | 30,27,27 32 | 31,28,2 33 | 32,29,28 34 | 33,30,25 35 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-09-14 02:23:37 UTC using RuboCop version 1.20.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | Rails/FindEach: 10 | Exclude: 11 | - 'lib/tasks/import_sample_csv_data.rake' -------------------------------------------------------------------------------- /app/javascript/components/globals.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const modules = import.meta.glob(['./**/(Base|App)[A-Z]*.vue', './**/(Base|App)[A-Z]*.js'], { eager: true }) 4 | 5 | // For each matching file... 6 | Object.entries(modules).forEach(([path, module]) => { 7 | // Get the component name from the file path 8 | const componentName = path.replace(/^.+\//, '').replace(/\.\w+$/, '') 9 | 10 | // Globally register the component 11 | Vue.component(componentName, module.default || module) 12 | }) 13 | -------------------------------------------------------------------------------- /db/book_stores_migrate/20211116095247_create_book_sales.rb: -------------------------------------------------------------------------------- 1 | class CreateBookSales < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :book_sales do |t| 4 | t.references :book, null: false, foreign_key: true 5 | t.references :store, null:false, foreign_key: true 6 | t.integer :price, null: false 7 | t.integer :stock, null: false 8 | t.integer :figure, null: false 9 | end 10 | 11 | add_index :book_sales, [:book_id, :store_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /app/javascript/data/nav.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "works", 4 | "icon": "mdi-database", 5 | "tooltip": "問題集一覧", 6 | "to": { "name": "Works" }, 7 | "disabled": false 8 | }, 9 | { 10 | "id": "help", 11 | "icon": "mdi-help-circle-outline", 12 | "tooltip": "ヘルプ", 13 | "to": null, 14 | "disabled": true 15 | }, 16 | { 17 | "id": "roadmap", 18 | "icon": "mdi-chart-timeline-variant", 19 | "tooltip": "学習ロードマップ", 20 | "to": null, 21 | "disabled": true 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/TheView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /lib/tasks/rebuild_database.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :rebuild do 4 | desc 'rebuild all database' 5 | task :database, [] => :environment do 6 | raise 'Not allowed to run on production' if Rails.env.production? 7 | 8 | Rails.application.eager_load! 9 | 10 | Rake::Task['db:drop'].execute 11 | Rake::Task['db:create'].execute 12 | Rake::Task['db:migrate'].execute 13 | Rake::Task['import:sample_csv_data'].execute 14 | Rake::Task['db:seed'].execute 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/api/samples/queries_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Samples 5 | class QueriesController < BaseController 6 | def execute 7 | target_database = SampleDatabaseDefinition.find_by(id: params[:sample_database_id]) 8 | return render_500 unless target_database 9 | 10 | @result = ::Samples::QueryHandler.new(target_database).execute(params[:query]) 11 | 12 | render 'execute', formats: :json 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/samples/temporary_migration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class TemporaryMigration < ActiveRecord::Migration[6.1] 5 | attr_reader :table_class 6 | 7 | def initialize(table_class) 8 | super 9 | @table_class = table_class 10 | end 11 | 12 | def create_temporary_table 13 | raise NotImplementedError 14 | end 15 | 16 | def create_table(table_name, **options) 17 | @connection = table_class.connection 18 | super 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20211116065231_create_works.rb: -------------------------------------------------------------------------------- 1 | class CreateWorks < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :works do |t| 4 | t.string :name, null: false 5 | t.string :slug, null: false 6 | t.text :description, null: false 7 | t.boolean :enabled, null: false, default: false 8 | t.boolean :published, null: false, default: false 9 | t.integer :order_number, null: false 10 | 11 | t.timestamps 12 | end 13 | 14 | add_index :works, :slug, unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/chapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Chapter < ApplicationRecord 4 | include Sortable 5 | 6 | has_many :practices, dependent: :destroy 7 | belongs_to :work 8 | 9 | validates :name, presence: true 10 | validates :order_number, presence: true, numericality: { only_integer: true } 11 | 12 | scope :with_practice, lambda { 13 | includes(practices: :bookmarks) 14 | .references(:practices) 15 | .merge(Practice.published) 16 | .merge(Practice.sort_by_order_number) 17 | } 18 | end 19 | -------------------------------------------------------------------------------- /app/views/api/works/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @work, :id, :name, :slug, :description 2 | 3 | json.chapters do 4 | json.array! @chapters do |chapter| 5 | if chapter.practices.present? 6 | json.extract! chapter, :id, :name 7 | json.practices do 8 | json.array! chapter.practices do |practice| 9 | json.extract! practice, :id, :name, :enabled, :requires_auth 10 | json.bookmarked practice.bookmarked_by?(current_user) if current_user 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | "js", 4 | "json", 5 | "vue" 6 | ], 7 | transform: { 8 | ".*\\.(vue)$": "vue-jest", 9 | "^.+\\.js$": "/node_modules/babel-jest" 10 | }, 11 | transformIgnorePatterns: [ 12 | "node_modules/" 13 | ], 14 | moduleNameMapper: { 15 | "^@/(.*)$": "/app/javascript/$1" 16 | }, 17 | testMatch: [ 18 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)", "**/__tests__/**/*.(js|jsx|ts|tsx)" 19 | ], 20 | testEnvironment: "jsdom" 21 | } 22 | -------------------------------------------------------------------------------- /app/javascript/components/BaseButtonClose.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /app/controllers/api/works_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class WorksController < BaseController 5 | def index 6 | @works = Work.published.sort_by_order_number 7 | render 'index', formats: :json 8 | end 9 | 10 | def show 11 | @work = Work.find_by!(slug: params[:slug]) 12 | 13 | return render_404 unless @work.enabled && @work.published 14 | 15 | @chapters = @work.chapters.with_practice.sort_by_order_number 16 | 17 | render 'show', formats: :json 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/book_sale.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class BookSale < BookStoresRecord 6 | belongs_to :book 7 | belongs_to :store 8 | 9 | validates :book_id, uniqueness: { scope: :store_id } 10 | 11 | validates :price, presence: true, numericality: { only_integer: true } 12 | validates :stock, presence: true, numericality: { only_integer: true } 13 | validates :figure, presence: true, numericality: { only_integer: true } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /app/javascript/views/errors/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w[ admin.js admin.css ] 13 | -------------------------------------------------------------------------------- /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/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| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /db/csv/book_stores/authors.csv: -------------------------------------------------------------------------------- 1 | id,name,gender 2 | 1,たなか あきら,男性 3 | 2,いきしま まこと,男性 4 | 3,にしのみや ひなた,男性 5 | 4,しのだ あつし,男性 6 | 5,あきえだ たつお,男性 7 | 6,たけだ かづや,男性 8 | 7,ひとすぎ りたか,男性 9 | 8,たかやま まさたか,男性 10 | 9,かわい しゅう,男性 11 | 10,こもり えりさ,女性 12 | 11,あさの みなみ,女性 13 | 12,はら さくら,女性 14 | 13,おくだ ゆあ,女性 15 | 14,ほりうち かなめ,女性 16 | 15,いわもと そらな,女性 17 | 16,はしもと さく,女性 18 | 17,かたふち はる,女性 19 | 18,ウォレス・コー,男性 20 | 19,ホケードル・ブランク,男性 21 | 20,メボバン・ミランダ,女性 22 | 21,いわかべ ただし,男性 23 | 22,きたむら しょうご,男性 24 | 23,さのう ひかな,女性 25 | 24,そらやま はる,男性 26 | 25,かくなみ あきのり,男性 27 | 26,もろみ りさ,女性 28 | 27,おざき せんか,女性 29 | 28,不明,不明 30 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/javascript/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppPageHeading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /app/javascript/i18n/messages/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ID", 3 | "name": "名前", 4 | "description": "説明", 5 | "slug": "スラッグ", 6 | "enabled": "利用可能", 7 | "published": "公開済み", 8 | "orderNumber": "並び順", 9 | "createdAt": "作成日", 10 | "updatedAt": "更新日", 11 | "workId": "問題集ID", 12 | "chapterId": "チャプターID", 13 | "question": "問題文", 14 | "answer": "解答例", 15 | "sampleDatabase": "演習用DB", 16 | "sampleTables": "演習用テーブル", 17 | "displayErDiagram": "ER図の表示", 18 | "requiresAuth": "ログイン要求", 19 | "email": "メールアドレス", 20 | "role": "権限", 21 | "password": "パスワード", 22 | "passwordConfirmation": "パスワード確認用" 23 | } 24 | -------------------------------------------------------------------------------- /spec/requests/api/user_sessions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Api::UserSessions', type: :request do 4 | let!(:headers) { { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' } } 5 | 6 | describe 'DELETE /api/logout' do 7 | let!(:user) { create(:user) } 8 | let(:http_request) { delete api_logout_path, headers: headers } 9 | 10 | it 'returns 200' do 11 | login_as(user) 12 | http_request 13 | expect(logged_in?).to be_falsey 14 | expect(response).to be_successful 15 | expect(response).to have_http_status(:ok) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/WorkDrawer/components/WorkDrawerHeading.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppIconLock.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /app/controllers/api/bookmarks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class BookmarksController < BaseController 5 | before_action :require_login 6 | 7 | def index 8 | @bookmark_practices = current_user.bookmark_practices.includes(chapter: :work) 9 | 10 | render 'index', formats: :json 11 | end 12 | 13 | def create 14 | practice = Practice.find(params[:practice_id]) 15 | current_user.bookmark(practice) 16 | end 17 | 18 | def destroy 19 | practice = Practice.find(params[:id]) 20 | current_user.unbookmark(practice) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/javascript/components/BaseButtonIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 34 | -------------------------------------------------------------------------------- /app/javascript/components/BaseProgressLinear.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /app/models/work.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Work < ApplicationRecord 4 | include Sortable 5 | 6 | has_many :chapters, dependent: :destroy 7 | 8 | validates :name, presence: true 9 | validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z_]+\z/, message: 'は半角英字、アンダースコアのみが使えます' } 10 | validates :description, presence: true 11 | validates :enabled, inclusion: { in: [true, false] } 12 | validates :published, inclusion: { in: [true, false] } 13 | validates :order_number, presence: true, numericality: { only_integer: true } 14 | 15 | scope :published, -> { where(published: true) } 16 | end 17 | -------------------------------------------------------------------------------- /app/javascript/components/BaseButtonText.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 35 | -------------------------------------------------------------------------------- /app/javascript/data/faq.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "header": "SQLの知識が全くないのですが、どうすれば良いですか?", 4 | "content": "まずは、技術書などでSQLの知識を体系的に学ぶことをおすすめします。\n次にSQLabを活用して実際にSQLを書いてみましょう。\n動作や感覚を掴むことでSQLの理解がより深まるはずです。" 5 | }, 6 | { 7 | "header": "推奨のブラウザはありますか?", 8 | "content": "PC版の「Google Chrome」の最新版をご利用ください。\n他ブラウザでの動作については保証致しかねますのでご了承ください。" 9 | }, 10 | { 11 | "header": "スマホやタブレットの対応はしてますか?", 12 | "content": "対応していません。PCからご利用ください。" 13 | }, 14 | { 15 | "header": "演習で使用しているデータベースの情報を教えてください。", 16 | "content": "PostgreSQL ver14.4です。" 17 | }, 18 | { 19 | "header": "利用料金はありますか?", 20 | "content": "無料でご利用いただけます。" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /app/javascript/plugins/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const host = { 4 | development: 'http://127.0.0.1:3000', 5 | staging: 'https://sqlabapp-staging.herokuapp.com', 6 | production: 'https://sqlab.net', 7 | } 8 | 9 | const axiosInstance = axios.create({ 10 | baseURL: `${host[process.env.NODE_ENV]}/api`, 11 | credentials: true, 12 | }) 13 | 14 | axiosInstance.interceptors.request.use(config => { 15 | if (['post', 'put', 'patch', 'delete'].includes(config.method)) { 16 | config.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content') 17 | } 18 | return config 19 | }) 20 | 21 | export default axiosInstance 22 | -------------------------------------------------------------------------------- /app/javascript/data/flash-messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "loginSuccess": { 3 | "type": "success", 4 | "icon": "mdi-check-circle", 5 | "text": "ログインしました。" 6 | }, 7 | "loginWarning": { 8 | "type": "warning", 9 | "icon": "mdi-alert-outline", 10 | "text": "ログインが必要です。" 11 | }, 12 | "loginFail": { 13 | "type": "error", 14 | "icon": "mdi-information-outline", 15 | "text": "ログインに失敗しました。" 16 | }, 17 | "logoutSuccess": { 18 | "type": "success", 19 | "icon": "mdi-check-circle", 20 | "text": "ログアウトしました。" 21 | }, 22 | "deleteUserSuccess": { 23 | "type": "success", 24 | "icon": "mdi-check-circle", 25 | "text": "アカウントを削除しました。" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/javascript/components/BaseButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /app/models/concerns/sortable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Sortable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_validation :set_order_number, on: :create 8 | 9 | scope :sort_by_order_number, -> { order(:order_number) } 10 | 11 | def set_order_number 12 | self.order_number = self.class.maximum(:order_number).to_i + 1 13 | end 14 | 15 | class << self 16 | def update_order(ids_arr) 17 | ids_arr.each do |ids| 18 | ids.each_with_index do |id, index| 19 | record = find(id) 20 | record.update!(order_number: index) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/javascript/styles/typography.scss: -------------------------------------------------------------------------------- 1 | .v-application { 2 | .text-x-large { 3 | font-size: 1.375rem !important; 4 | line-height: 2.25rem !important; 5 | } 6 | .text-large { 7 | font-size: 1.25rem !important; 8 | line-height: 2.25rem !important; 9 | } 10 | .text-medium { 11 | font-size: 1.125rem !important; 12 | line-height: 2rem !important; 13 | } 14 | .text-default { 15 | font-size: 1rem !important; 16 | line-height: 1.75rem !important; 17 | } 18 | .text-small { 19 | font-size: 0.875rem !important; 20 | line-height: 1.5rem !important; 21 | } 22 | .text-x-small { 23 | font-size: 0.75rem !important; 24 | line-height: 1.25rem !important; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/models/chapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Chapter, type: :model do 4 | describe 'validation' do 5 | let!(:chapter) { build(:chapter) } 6 | 7 | it 'is valid all with attributes' do 8 | expect(chapter).to be_valid 9 | expect(chapter.errors).to be_empty 10 | end 11 | 12 | it 'is invalid without work_id' do 13 | chapter.work = nil 14 | expect(chapter).to be_invalid 15 | expect(chapter.errors[:work]).to include('を入力してください') 16 | end 17 | 18 | it 'is invalid without name' do 19 | chapter.name = '' 20 | expect(chapter).to be_invalid 21 | expect(chapter.errors[:name]).to include('を入力してください') 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeMenuErDiagram.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 36 | -------------------------------------------------------------------------------- /db/migrate/20211116065548_create_practices.rb: -------------------------------------------------------------------------------- 1 | class CreatePractices < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :practices do |t| 4 | t.references :chapter, null:false, foreign_key: true 5 | t.string :name, null: false 6 | t.text :question, null: false 7 | t.text :answer, null: false 8 | t.integer :sample_database_id, null:false 9 | t.boolean :display_er_diagram, null: false, default: true 10 | t.boolean :enabled, null: false, default: false 11 | t.boolean :published, null: false, default: false 12 | t.boolean :requires_auth, null: false, default: true 13 | t.integer :order_number, null: false 14 | 15 | t.timestamps 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/components/BaseTooltip.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | -------------------------------------------------------------------------------- /app/javascript/views/work/components/WorkDetail.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /app/models/samples/book_stores/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module BookStores 5 | class Book < BookStoresRecord 6 | has_many :book_authors, dependent: :destroy 7 | has_many :authors, through: :book_authors 8 | 9 | has_many :book_categories, dependent: :destroy 10 | has_many :categories, through: :book_categories 11 | 12 | has_many :book_sales, dependent: :destroy 13 | has_many :stores, through: :book_sales 14 | 15 | validates :name, presence: true 16 | validates :release_year, numericality: { only_integer: true }, allow_blank: true 17 | validates :total_page, presence: true, numericality: { only_integer: true } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/Navbar/components/NavbarItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /spec/factories/works.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :work do 3 | sequence(:name) { |n| "work_name#{n}" } 4 | sequence(:slug, 'a') { |n| "work_slug#{n}" } 5 | sequence(:description) { |n| "work_description#{n}" } 6 | enabled { false } 7 | published { false } 8 | sequence(:order_number) { |n| n } 9 | 10 | trait :enabled do 11 | enabled { true } 12 | end 13 | 14 | trait :published do 15 | published { true } 16 | end 17 | 18 | trait :with_chapters do 19 | transient do 20 | chapters_count { 5 } 21 | end 22 | 23 | after(:create) do |work, evaluator| 24 | create_list(:chapter, evaluator.chapters_count, work: work) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/samples/query_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class QueryHandler 5 | attr_reader :target_database 6 | 7 | def initialize(target_database) 8 | @target_database = target_database 9 | end 10 | 11 | def all_records(tables) 12 | TemporaryTableCreator.new(target_database).create(tables) 13 | QueryExecutor.new(target_database).all_records(tables) 14 | end 15 | 16 | def execute(query) 17 | queries = QueryParser.call(query) 18 | temporary_tables = target_database.available_temporary_tables 19 | TemporaryTableCreator.new(target_database).create(temporary_tables) 20 | QueryExecutor.new(target_database).execute(queries) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/api/auth_users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class AuthUsersController < BaseController 5 | before_action :require_login, only: %i[update destroy] 6 | 7 | def show 8 | render 'show', formats: :json 9 | end 10 | 11 | def update 12 | if current_user.update(auth_user_params) 13 | render 'update', formats: :json 14 | else 15 | render json: current_user.errors.messages, status: :bad_request 16 | end 17 | end 18 | 19 | def destroy 20 | current_user.destroy! 21 | 22 | head :ok 23 | end 24 | 25 | private 26 | 27 | def auth_user_params 28 | params.require(:auth_user).permit(:name, :email) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import RubyPlugin from 'vite-plugin-ruby' 3 | import vue from '@vitejs/plugin-vue2' 4 | import FullReload from 'vite-plugin-full-reload' 5 | import path from 'path' 6 | 7 | export default defineConfig({ 8 | base: '/vite-dev/', 9 | plugins: [ 10 | RubyPlugin(), 11 | vue(), 12 | FullReload(['config/routes.rb', 'app/views/**/*'], { delay: 200 }), 13 | ], 14 | resolve: { 15 | alias: { 16 | '@': path.resolve(__dirname, './app/javascript'), 17 | vue: 'vue/dist/vue.esm.js', // Use full build with template compiler 18 | }, 19 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], 20 | }, 21 | server: { 22 | hmr: { 23 | host: 'localhost', 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/javascript/store/modules/practices.js: -------------------------------------------------------------------------------- 1 | import axios from '@/plugins/axios' 2 | import { handleException } from '@/utils/exception' 3 | 4 | const practices = { 5 | namespaced: true, 6 | state: { 7 | practice: null, 8 | }, 9 | getters: { 10 | practice: state => state.practice, 11 | }, 12 | mutations: { 13 | setPractice(state, practice) { 14 | state.practice = practice 15 | }, 16 | }, 17 | actions: { 18 | async fetchPractice({ commit }, { id, withSampleData = false }) { 19 | await axios 20 | .get(`practices/${id}`, { 21 | params: { withSampleData }, 22 | }) 23 | .then(res => commit('setPractice', res.data)) 24 | .catch(err => handleException(err)) 25 | }, 26 | }, 27 | } 28 | 29 | export default practices 30 | -------------------------------------------------------------------------------- /app/javascript/layout/admin/components/TheSidebar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /app/models/samples/query_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class QueryParser 5 | class << self 6 | def call(queries) 7 | queries = remove_comments(queries) 8 | queries = replace_newline_with_space(queries) 9 | queries.split(';').compact_blank 10 | end 11 | 12 | private 13 | 14 | SINGLE_LINE_COMMENT_REGEX = /--.*(?:\n|$)/ 15 | MULTI_LINE_COMMENT_REGEX = %r{/\*(?:.|\n)*?\*/} 16 | 17 | COMMENT_REGEX = Regexp.union(SINGLE_LINE_COMMENT_REGEX, MULTI_LINE_COMMENT_REGEX) 18 | 19 | def remove_comments(queries) 20 | queries.gsub(COMMENT_REGEX, '') 21 | end 22 | 23 | def replace_newline_with_space(queries) 24 | queries.tr("\n/", ' ') 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/LoginModal.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | -------------------------------------------------------------------------------- /app/javascript/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './overrides'; 2 | @use './typography'; 3 | 4 | :root { 5 | --shadow-color: rgb(23, 49, 128, 0.33); 6 | --shadow-color-low: rgb(23, 49, 128, 0.13); 7 | --shadow: 0px 3px 6px -2px var(--shadow-color); 8 | --shadow-low: 0px 1px 7px -3px var(--shadow-color); 9 | --bg-color: #f1f5fb; 10 | } 11 | *, 12 | *:focus { 13 | outline: none; 14 | } 15 | a { 16 | text-decoration: none; 17 | } 18 | .v-application { 19 | font-family: Roboto, 'Noto Sans JP', sans-serif; 20 | } 21 | .v-application--wrap { 22 | background: var(--bg-color); 23 | } 24 | .form-control { 25 | &:not(:first-child) { 26 | margin-top: 1.5rem; 27 | } 28 | } 29 | .shadow { 30 | box-shadow: var(--shadow) !important; 31 | } 32 | .shadow-low { 33 | box-shadow: var(--shadow-low) !important; 34 | } 35 | -------------------------------------------------------------------------------- /app/javascript/views/top/components/TopAbout.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /app/javascript/components/BaseTable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 42 | -------------------------------------------------------------------------------- /db/csv/book_stores/books.csv: -------------------------------------------------------------------------------- 1 | id,name,release_year,total_page 2 | 1,コードと回路,2017,245 3 | 2,宇宙の歴史,2019,314 4 | 3,星の記憶,,181 5 | 4,リファクタリング,2004,315 6 | 5,時短レシピ100,2008,298 7 | 6,世界一周マップ,2005,187 8 | 7,砂,,244 9 | 8,ドン・スターク,2006,272 10 | 9,投資のススメ,2018,350 11 | 10,青毛のアン,2009,338 12 | 11,ロザリンのおくりもの,2019,187 13 | 12,かもめ飛行,2006,307 14 | 13,英単語1000,2010,185 15 | 14,預言者モネ,2014,334 16 | 15,人生の歩み方,2008,274 17 | 16,子育て日記,2009,241 18 | 17,ためになるお金の知識,2004,326 19 | 18,SNSマーケティングの基本,2018,205 20 | 19,一軒家かマンションか,2005,323 21 | 20,SDGsとは何か,2020,197 22 | 21,覚えておきたいExcel手法50選,2011,189 23 | 22,マネジメントの教本,2009,220 24 | 23,デザインの心得,2005,322 25 | 24,コンピューターサイエンス基礎,2021,216 26 | 25,マンガで学ぶ日本史,2020,308 27 | 26,カエルのぴょこ,2016,54 28 | 27,マンガで学ぶデータベース,2019,324 29 | 28,達人Ruby on Rails,2008,295 30 | 29,噂のトンネル,2010,325 31 | 30,マンガで学ぶ世界史,2020,262 32 | -------------------------------------------------------------------------------- /db/migrate/20250921025156_drop_model_databases_and_tables.rb: -------------------------------------------------------------------------------- 1 | class DropModelDatabasesAndTables < ActiveRecord::Migration[6.1] 2 | def up 3 | # Drop model_tables first (has foreign key to model_databases) 4 | drop_table :model_tables, if_exists: true 5 | 6 | # Drop model_databases table 7 | drop_table :model_databases, if_exists: true 8 | end 9 | 10 | def down 11 | # Recreate model_databases table first 12 | create_table :model_databases do |t| 13 | t.string :name, null: false 14 | t.timestamps 15 | end 16 | 17 | # Recreate model_tables table (with foreign key to model_databases) 18 | create_table :model_tables do |t| 19 | t.references :model_database, null: false, foreign_key: true 20 | t.string :name, null: false 21 | t.timestamps 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/samples/temporary_table_creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class TemporaryTableCreator 5 | attr_reader :target_database 6 | 7 | def initialize(target_database) 8 | @target_database = target_database 9 | end 10 | 11 | def create(tables) 12 | # Filter only temporary tables first 13 | temp_tables = tables.where(temporary: true) 14 | 15 | return if temp_tables.empty? 16 | 17 | # Convert temporary tables to classes 18 | temp_table_classes = temp_tables.map(&:table_class) 19 | 20 | sorted_temp_table_classes = Sorting.sort_by_dependencies(temp_table_classes) 21 | 22 | sorted_temp_table_classes.each do |temp_table| 23 | temp_table.migrate_temporary 24 | temp_table.import_csv 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/factories/practices.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :practice do 3 | chapter 4 | sequence(:name) { |n| "practice_name#{n}" } 5 | sequence(:question) { |n| "practice_question#{n}" } 6 | sequence(:answer) { |n| "practice_answer#{n}" } 7 | sequence(:sample_database_id) { SampleDatabaseDefinition.pluck(:id).sample } 8 | display_er_diagram { true } 9 | enabled { false } 10 | published { false } 11 | requires_auth { true } 12 | sequence(:order_number) { |n| n } 13 | 14 | trait :hidden_er_diagram do 15 | display_er_diagram { false } 16 | end 17 | 18 | trait :enabled do 19 | enabled { true } 20 | end 21 | 22 | trait :published do 23 | published { true } 24 | end 25 | 26 | trait :not_requires_auth do 27 | requires_auth { false } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /bin/vite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'vite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("vite_ruby", "vite") 28 | -------------------------------------------------------------------------------- /app/javascript/views/admin/works/components/WorksFooterButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeTabItemEditor.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | -------------------------------------------------------------------------------- /db/migrate/20251002204603_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if configured_service = ActiveStorage::Blob.service.name 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/concerns/samples/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | module Base 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | delegate :import_csv, to: :bulk_insert 9 | 10 | def database_name 11 | self::DATABASE_NAME.to_s 12 | end 13 | 14 | def bulk_insert 15 | BulkInsert.new(self) 16 | end 17 | 18 | def temporary_table? 19 | SampleTableDefinition.find_by(name: table_name).temporary? 20 | end 21 | 22 | def current_connection_db_config 23 | connection_db_config.configuration_hash 24 | end 25 | 26 | def current_connection_db_name 27 | current_connection_db_config[:database] 28 | end 29 | 30 | def current_connection_username 31 | current_connection_db_config[:username] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/requests/api/admin/admin_authentication_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Api::Admin::AdminAuthentication', type: :request do 4 | describe '#check_admin' do 5 | subject do 6 | get api_admin_works_path, headers: headers 7 | response 8 | end 9 | 10 | let(:headers) { { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' } } 11 | 12 | context 'when request from admin user' do 13 | let!(:user) { create(:user, :admin) } 14 | 15 | before do 16 | login_as(user) 17 | end 18 | 19 | it { is_expected.to have_http_status(:ok) } 20 | end 21 | 22 | context 'when request from general user' do 23 | let!(:user) { create(:user) } 24 | 25 | before do 26 | login_as(user) 27 | end 28 | 29 | it { is_expected.to have_http_status(:unauthorized) } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppLogo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | -------------------------------------------------------------------------------- /app/javascript/components/BaseDrawer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/UserDrawer/components/UserDrawerHeading.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /app/javascript/components/BaseSwitch.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppLogoLink.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | 37 | 45 | -------------------------------------------------------------------------------- /app/models/samples/query_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class QueryValidator 5 | VALID_QUERY_PATTERN = /\A\s*(select(?!.*\binto\b)|insert|update|delete)\b/i 6 | private_constant :VALID_QUERY_PATTERN 7 | 8 | SYSTEM_INFO_PATTERN = /\b(pg_|inet_|current_|has_|txid_|information_schema\.|set_config\()/i 9 | private_constant :SYSTEM_INFO_PATTERN 10 | 11 | class << self 12 | def validate(query) 13 | return if query.blank? 14 | return unless invalid_query?(query) || system_info_pattern?(query) 15 | 16 | raise "ERROR: invalid keywords or syntax error, at \"#{query}\"" 17 | end 18 | 19 | private 20 | 21 | def invalid_query?(query) 22 | !VALID_QUERY_PATTERN.match?(query) 23 | end 24 | 25 | def system_info_pattern?(query) 26 | SYSTEM_INFO_PATTERN.match?(query) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/javascript/components/BaseMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /app/javascript/views/works/components/WorksCard.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /spec/requests/api/admin/sample_databases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Api::Admin::SampleDatabases', type: :request do 4 | let!(:headers) { { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' } } 5 | 6 | before do 7 | allow_any_instance_of(Api::Admin::SampleDatabasesController).to receive(:require_login) 8 | allow_any_instance_of(Api::Admin::SampleDatabasesController).to receive(:check_admin) 9 | end 10 | 11 | describe 'GET api/admin/sample_databases' do 12 | let(:http_request) { get api_admin_sample_databases_path, headers: headers, as: :json } 13 | 14 | it 'returns list of sample databases' do 15 | http_request 16 | expect(body[0]['name']).to eq SampleDatabaseDefinition.first.name 17 | expect(body[0]['tables'].pluck('name')).to eq SampleDatabaseDefinition.first.available_tables.pluck(:name) 18 | expect(response).to be_successful 19 | expect(response).to have_http_status(:ok) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/api/oauths_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class OauthsController < BaseController 5 | def oauth 6 | login_at(auth_params[:provider]) 7 | end 8 | 9 | def callback 10 | provider = auth_params[:provider] 11 | 12 | return redirect_to root_path if access_denied? 13 | 14 | if (@user = login_from(provider)) 15 | redirect_to root_path 16 | else 17 | begin 18 | @user = create_from(provider) 19 | reset_session 20 | auto_login(@user) 21 | redirect_to root_path 22 | rescue StandardError 23 | redirect_to root_path 24 | end 25 | end 26 | end 27 | 28 | private 29 | 30 | def auth_params 31 | params.permit(:code, :provider, :error) 32 | end 33 | 34 | def access_denied? 35 | return false unless auth_params[:error] 36 | 37 | auth_params[:error].match?('access_denied') 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /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 https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /db/csv/book_stores/book_sales.csv: -------------------------------------------------------------------------------- 1 | id,book_id,store_id,price,stock,figure 2 | 1,1,1,3500,1000,200 3 | 2,1,3,3000,2000,150 4 | 3,2,3,1500,400,50 5 | 4,3,1,1100,700,1500 6 | 5,4,2,4000,0,500 7 | 6,5,1,1800,9000,3000 8 | 7,6,2,1300,10,300 9 | 8,7,3,800,0,1000 10 | 9,8,1,2200,300,100 11 | 10,9,3,1500,1000,1200 12 | 11,10,1,1100,5000,10000 13 | 12,11,3,1300,10000,8000 14 | 13,12,1,1300,450,5000 15 | 14,13,2,1800,800,2000 16 | 15,13,3,1500,350,2100 17 | 16,14,1,1600,15000,4300 18 | 17,15,2,1800,2700,1700 19 | 18,16,1,1400,1,3400 20 | 19,17,1,1600,650,5600 21 | 20,18,1,2500,0,1300 22 | 21,18,3,2200,10,2700 23 | 22,19,1,2100,6,800 24 | 23,20,3,1300,100000,1000 25 | 24,21,1,2500,600,3000 26 | 25,22,1,2000,40,2800 27 | 26,23,1,2400,8,2900 28 | 27,24,2,2400,9000,7000 29 | 28,24,3,1900,3200,10000 30 | 29,25,3,2000,690,5000 31 | 30,26,1,1000,200,4100 32 | 31,27,1,2500,580,1000 33 | 32,27,3,2700,360,14000 34 | 33,28,1,3900,230,6400 35 | 34,29,2,1500,3900,9000 36 | 35,30,3,2000,2100,4000 37 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppFlashMessage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 42 | -------------------------------------------------------------------------------- /app/javascript/store/modules/users.js: -------------------------------------------------------------------------------- 1 | import axios from '@/plugins/axios' 2 | 3 | const users = { 4 | namespaced: true, 5 | state: { 6 | authUser: null, 7 | }, 8 | getters: { 9 | authUser: state => state.authUser, 10 | }, 11 | mutations: { 12 | setAuthUser(state, authUser) { 13 | state.authUser = authUser 14 | }, 15 | }, 16 | actions: { 17 | async logoutUser({ commit }) { 18 | await axios.delete('logout') 19 | commit('setAuthUser', null) 20 | }, 21 | resetAuthUser({ commit }) { 22 | commit('setAuthUser', null) 23 | }, 24 | getAuthUser({ state, dispatch }) { 25 | if (state.authUser) return state.authUser 26 | 27 | return dispatch('fetchAuthUser') 28 | }, 29 | async fetchAuthUser({ commit }) { 30 | try { 31 | const response = await axios.get('auth_user') 32 | commit('setAuthUser', response.data) 33 | return response.data 34 | } catch { 35 | return null 36 | } 37 | }, 38 | }, 39 | } 40 | 41 | export default users 42 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeModalPreferenceList.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 40 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | MwlCEoDdbV/xRbRBPF97L4Zeo1Jze5qLav5yWO9shvYVUf6MKc4VNYuKmj9DS6TepLQhuhtiBz7zRWw6pKtIiEVi0LktUum6QqS7PmAoLt0XSA5OeLCvU4xBXJ7HKP4X7S9r3jF349DC8nCZm/8ieERGITN6N60MiabkvYxOo7EuMu4uoI96IibpGcsNBFoQlfyC2NO9rv91Yq85Wdh8omsn291LUoCaR9tn8xRNvPPoFWLHdd/qeEi06G6R0JrvIxcUfQrQcnzHYodktuWbSjem69qK/WqYSVPTPSuLfZ5zTToJXaLNN7Y8JCDsqaiGTPRyY7dNydbtS120boxRER7an8pYA7abEECPv7vzGkeacHtTyKrIcH3Z9wCrM35cEx/eTEn2akKMlSqJl7f55cO+mbiakpszK3jAcIOGC8soxMbBMUOrdzdiljjiiDdg2CGhdgfmOugXUZ5Bkrp10IsXoJz1M8O7ZPNrePDd5GWvBo+9P5aUURxt2Sic5sz8ogMUideH0tFZO1DDhxAcaNcPSvyqVCMuZYw8Z6OsfgKNUJSPeMkIwBj6HSE6p3z7wm6bXR1eRreld8NxpvSLQiWZqsBF2pYXtCOqcyV3pUvd8higD/OIuhjiEoiEjVicgFDOEyT0b4RIM0duVsEnc2dIBJhYuJxdWscamZmBFb5DeSqPDYDpY7YiWHirXK4+CV5S7D8vZNrSUkYgAgCbZt3IkiwM/dMq0Q7bFwfH01qh9N6TjJAcT2ZokBsbNDbmFHGTZo0NHlaAv4zSLH0gEIx151e8iWwYl+3+7ZIPwFyfRUfDjSJt/pS+EzMJJsXjSvDr6QnO3kJJvxGO6bn+1LWnhcivPj2KnAOZhpT6MKvr1Pyq4+D8XyGUVLBrNukhIRq0fTOH37eyQLjKiGOfluJiQMSsLF00Q6SIjfoT4u5Jy0eBQfbFsqN94YZ7bUX8r8x7qGxYZ2uq4D7W7pUopMvZ1WG8MYn8gBtVlu6oTuWM9VvofLsfWZW0gy0=--YkcqOXj+Imbnlhu5--4jSsi1ZrLEjrRubqRpXVTg== -------------------------------------------------------------------------------- /app/controllers/concerns/api/exception_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module ExceptionHandler 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | rescue_from StandardError, with: :render_500 9 | rescue_from ActiveRecord::RecordNotFound, with: :render_404 10 | end 11 | 12 | private 13 | 14 | def render_400(exception = nil, messages = nil) 15 | render_error(400, 'Bad Request', exception&.message, *messages) 16 | end 17 | 18 | def render_404(exception = nil, messages = nil) 19 | render_error(404, 'Record Not Found', exception&.message, *messages) 20 | end 21 | 22 | def render_500(exception = nil, messages = nil) 23 | render_error(500, 'Internal Server Error', exception&.message, *messages) 24 | end 25 | 26 | def render_error(code, message, *error_messages) 27 | response = { 28 | message: message, 29 | errors: error_messages.compact 30 | } 31 | 32 | render json: response, status: code 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/javascript/views/top/components/TopCatch.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 47 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeQuestion.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /app/javascript/views/top/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | -------------------------------------------------------------------------------- /spec/models/sample_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SampleTable, type: :model do 4 | describe 'validation' do 5 | let!(:practice) { create(:practice, sample_database_id: 1) } 6 | let!(:sample_table) { build(:sample_table, practice: practice, uid: 1) } 7 | 8 | it 'is valid all with attributes' do 9 | expect(sample_table).to be_valid 10 | expect(sample_table.errors).to be_empty 11 | end 12 | 13 | it 'is invalid without uid' do 14 | sample_table.uid = nil 15 | 16 | expect(sample_table).to be_invalid 17 | expect(sample_table.errors[:uid]).to include('を入力してください') 18 | end 19 | 20 | it 'is invalid with duplicate uid and practice_id combination' do 21 | sample_table = create(:sample_table, practice: practice, uid: 1) 22 | sample_table_with_duplicate_uid = build(:sample_table, practice: practice, uid: sample_table.uid) 23 | 24 | expect(sample_table_with_duplicate_uid).to be_invalid 25 | expect(sample_table_with_duplicate_uid.errors[:uid]).to include('はすでに存在します') 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | authenticates_with_sorcery! 5 | 6 | has_many :authentications, dependent: :destroy 7 | has_many :bookmarks, dependent: :destroy 8 | has_many :bookmark_practices, through: :bookmarks, source: :practice 9 | accepts_nested_attributes_for :authentications 10 | 11 | enum :role, { 12 | general: 0, 13 | admin: 10 14 | } 15 | 16 | validates :name, presence: true, length: { maximum: 32 } 17 | validates :email, presence: true, uniqueness: true 18 | validates :role, presence: true 19 | validates :password, length: { minimum: 8 }, confirmation: true, if: :new_record_or_changes_password 20 | validates :password_confirmation, presence: true, if: :new_record_or_changes_password 21 | 22 | def bookmark(practice) 23 | bookmark_practices << practice 24 | end 25 | 26 | def unbookmark(practice) 27 | bookmark_practices.destroy(practice) 28 | end 29 | 30 | private 31 | 32 | def new_record_or_changes_password 33 | new_record? || changes[:crypted_password] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/javascript/data/keyboard-events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "ctrl+Enter", 4 | "command": "handleTestQuery" 5 | }, 6 | { 7 | "key": "ctrl+k", 8 | "command": "executeQuery" 9 | }, 10 | { 11 | "key": "ctrl+j", 12 | "command": "focusEditor" 13 | }, 14 | { 15 | "key": "ctrl+r", 16 | "command": "resetEditor" 17 | }, 18 | { 19 | "key": "ctrl+o", 20 | "command": "openErDiagramInNewTab" 21 | }, 22 | { 23 | "key": "ctrl+i", 24 | "command": "openExampleAnswerModal" 25 | }, 26 | { 27 | "key": "ctrl+.", 28 | "command": "moveOutputTabTo" 29 | }, 30 | { 31 | "key": "ctrl+,", 32 | "command": "moveOutputTabTo", 33 | "args": [false] 34 | }, 35 | { 36 | "key": "ctrl+/", 37 | "command": "moveOutputTableTabTo" 38 | }, 39 | { 40 | "key": "ctrl+m", 41 | "command": "moveOutputTableTabTo", 42 | "args": [false] 43 | }, 44 | { 45 | "key": "ctrl+meta+.", 46 | "command": "moveInputTabTo" 47 | }, 48 | { 49 | "key": "ctrl+meta+,", 50 | "command": "moveInputTabTo", 51 | "args": [false] 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | <%= display_meta_tags default_meta_tags %> 8 | <%= stylesheet_link_tag 'application', media: 'all' %> 9 | <%= vite_client_tag %> 10 | <%= vite_javascript_tag 'application' %> 11 | <%= vite_javascript_tag 'app' %> 12 | 13 | 14 | 15 | 16 | 17 | <% if Rails.env.production? %> 18 | <%= render 'layouts/google_analytics' %> 19 | <% end %> 20 | 21 | 22 | 23 | <%= yield %> 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/javascript/views/admin/users/components/UsersDeleteModal.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /lib/tasks/rebuild_sample_database.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # IMPORTANT: Always enable maintenance mode before running this task 4 | # This ensures that users cannot access the system during the rebuild process. 5 | namespace :rebuild do 6 | desc 'Rebuild sample databases' 7 | task :sample_database, [] => :environment do 8 | # Environment safety check 9 | if Rails.env.production? 10 | puts 'WARNING: This will destroy production data! Continue? (y/N)' 11 | input = $stdin.gets.chomp.downcase 12 | exit unless input == 'y' 13 | end 14 | 15 | steps = [ 16 | { message: 'Dropping book_stores database...', task: 'db:drop:book_stores' }, 17 | { message: 'Creating book_stores database...', task: 'db:create:book_stores' }, 18 | { message: 'Running migrations for book_stores...', task: 'db:migrate:book_stores' }, 19 | { message: 'Importing CSV data...', task: 'import:sample_csv_data' } 20 | ] 21 | 22 | steps.each do |step| 23 | puts step[:message] 24 | Rake::Task[step[:task]].execute 25 | end 26 | 27 | puts 'Rebuild completed successfully!' 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/javascript/views/mypage/components/MypageAccountDeleteDialog.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /app/javascript/components/app/AppIconBookmark.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 46 | -------------------------------------------------------------------------------- /app/javascript/layout/admin/components/AdminNavbar/components/AdminNavbarItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 53 | -------------------------------------------------------------------------------- /app/models/sample_database_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SampleDatabaseDefinition < ActiveHash::Base 4 | include ActiveHash::Enum 5 | 6 | # Sample database definitions 7 | self.data = [ 8 | { id: 1, name: 'book_stores' } 9 | ] 10 | 11 | enum_accessor :name 12 | 13 | # Get all tables for this database 14 | def available_tables 15 | SampleTableDefinition.for_database(name) 16 | end 17 | 18 | # Get all temporary tables for this database 19 | def available_temporary_tables 20 | available_tables.where(temporary: true) 21 | end 22 | 23 | # Get all permanent tables for this database 24 | def available_permanent_tables 25 | available_tables.where(temporary: false) 26 | end 27 | 28 | # Get specific tables by IDs 29 | def table_definitions_by_ids(table_ids) 30 | SampleTableDefinition.where(id: table_ids).order(:id) 31 | end 32 | 33 | # Establish database connection 34 | def establish_connection 35 | record_class.connection 36 | end 37 | 38 | # Get the base record class for this database 39 | def record_class 40 | "samples/#{name}_record".classify.constantize 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/javascript/views/works/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | 51 | 57 | -------------------------------------------------------------------------------- /app/javascript/plugins/vee-validate.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { ValidationProvider, ValidationObserver, extend, localize } from 'vee-validate' 3 | import ja from 'vee-validate/dist/locale/ja.json' 4 | import { required, regex, email, min, max } from 'vee-validate/dist/rules' 5 | 6 | localize('ja', ja) 7 | 8 | extend('required', { 9 | ...required, 10 | }) 11 | extend('regex', { 12 | ...regex, 13 | }) 14 | extend('email', { 15 | ...email, 16 | message: '{_field_}の形式で入力してください', 17 | }) 18 | extend('min', { 19 | ...min, 20 | validate(value, { length }) { 21 | return value.length >= length 22 | }, 23 | params: ['length'], 24 | message: '{_field_}は{length}文字以上で入力してください', 25 | }) 26 | extend('max', { 27 | ...max, 28 | validate(value, { length }) { 29 | return value.length <= length 30 | }, 31 | params: ['length'], 32 | message: '{_field_}は{length}文字以下で入力してください', 33 | }) 34 | extend('password_confirmed', { 35 | params: ['target'], 36 | validate(value, { target }) { 37 | return value === target 38 | }, 39 | message: 'パスワードと一致しません', 40 | }) 41 | 42 | Vue.component('ValidationProvider', ValidationProvider) 43 | Vue.component('ValidationObserver', ValidationObserver) 44 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def default_meta_tags 3 | { 4 | site: 'SQLab', 5 | title: 'SQLの練習ができる学習サービス', 6 | description: 'SQLの練習ができるSQL特化の学習サービスです。環境構築不要で豊富な練習問題に取り組むことができます。SQLabを活用してSQLの知識をスキルに変えていきましょう。', 7 | keywords: 'sqlab, sql, sql 練習, sql 演習, sql 問題, sql 勉強', 8 | charset: 'utf-8', 9 | separator: '|', 10 | reverse: true, 11 | canonical: request.original_url, 12 | icon: [ 13 | { href: image_url('/favicon.ico') }, 14 | { href: image_url('/apple-touch-icon-precomposed.png'), rel: 'apple-touch-icon-precomposed', sizes: '180x180', type: 'image/png' } 15 | ], 16 | og: { 17 | site_name: :site, 18 | title: 'SQLの練習ができる学習サービス | SQLab', 19 | description: :description, 20 | type: 'website', 21 | url: request.original_url, 22 | image: image_url('/ogp.png'), 23 | locale: 'ja_JP' 24 | }, 25 | twitter: { 26 | title: 'SQLの練習ができる学習サービス | SQLab', 27 | description: :description, 28 | image: image_url('/ogp.png'), 29 | card: 'summary_large_image', 30 | site: '@sqlab_app' 31 | } 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/javascript/plugins/vuetify/theme.js: -------------------------------------------------------------------------------- 1 | const font = Object.freeze({ 2 | base: '#5A6977', 3 | lighten1: '#757680', 4 | darken1: '#252545', 5 | }) 6 | const primary = Object.freeze({ 7 | base: '#7F96FC', 8 | lighten5: '#F5F6FC', 9 | lighten4: '#E8ECFC', 10 | lighten3: '#CFD7FC', 11 | lighten2: '#B4C6FF', 12 | lighten1: '#A3B7FF', 13 | darken1: '#617BED', 14 | darken2: '#4968C8', 15 | darken3: '#24439E', 16 | darken4: '#173180', 17 | }) 18 | const accent = Object.freeze({ 19 | base: '#FBC02D', 20 | lighten1: '#FFF263', 21 | darken1: '#C49000', 22 | }) 23 | 24 | export default { 25 | font: { 26 | base: font.base, 27 | lighten1: font.lighten1, 28 | darken1: font.darken1, 29 | }, 30 | primary: { 31 | base: primary.base, 32 | lighten5: primary.lighten5, 33 | lighten4: primary.lighten4, 34 | lighten3: primary.lighten3, 35 | lighten2: primary.lighten2, 36 | lighten1: primary.lighten1, 37 | darken1: primary.darken1, 38 | darken2: primary.darken2, 39 | darken3: primary.darken3, 40 | darken4: primary.darken4, 41 | }, 42 | accent: { 43 | base: accent.base, 44 | lighten1: accent.lighten1, 45 | darken1: accent.darken1, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /spec/requests/api/admin/users_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Api::Admin::Users', type: :request do 4 | let!(:headers) { { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' } } 5 | 6 | before do 7 | allow_any_instance_of(Api::Admin::UsersController).to receive(:require_login) 8 | allow_any_instance_of(Api::Admin::UsersController).to receive(:check_admin) 9 | end 10 | 11 | describe 'GET /api/admin/users' do 12 | let(:http_request) { get api_admin_users_path, headers: headers, as: :json } 13 | let!(:users_num) { 3 } 14 | 15 | it 'returns list of users' do 16 | create_list(:user, users_num) 17 | http_request 18 | expect(body.count).to eq users_num 19 | expect(response).to be_successful 20 | expect(response).to have_http_status(:ok) 21 | end 22 | end 23 | 24 | describe 'DELETE /api/admin/users/:id' do 25 | let(:http_request) { delete api_admin_user_path(user.id), headers: headers, as: :json } 26 | let!(:user) { create(:user) } 27 | 28 | it 'return 200 status' do 29 | http_request 30 | expect(response).to be_successful 31 | expect(response).to have_http_status(:ok) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/javascript/views/mypage/components/MypageClearCount.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeTable.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 56 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeModalExampleAnswer.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /db/migrate/20251002204604_create_active_storage_variant_records.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20191206030411) 2 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | # Use Active Record's configured type for primary key 7 | create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| 8 | t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type 9 | t.string :variation_digest, null: false 10 | 11 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 12 | t.foreign_key :active_storage_blobs, column: :blob_id 13 | end 14 | end 15 | 16 | private 17 | def primary_key_type 18 | config = Rails.configuration.generators 19 | config.options[config.orm][:primary_key_type] || :primary_key 20 | end 21 | 22 | def blobs_primary_key_type 23 | pkey_name = connection.primary_key(:active_storage_blobs) 24 | pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } 25 | pkey_column.bigint? ? :bigint : pkey_column.type 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | APP_NAME = "sqlab" 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | 34 | # puts "\n== Configuring puma-dev ==" 35 | # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" 36 | # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" 37 | end 38 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeTabItemErDiagram.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 49 | 50 | 57 | -------------------------------------------------------------------------------- /app/models/samples/bulk_insert.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class BulkInsert 5 | require 'csv' 6 | 7 | attr_reader :table_class, :csv_file_path 8 | 9 | def initialize(table_class) 10 | @table_class = table_class 11 | @csv_file_path = locate_csv_file 12 | end 13 | 14 | def import_csv 15 | csv_records = [] 16 | clear_existing_records 17 | 18 | CSV.foreach(csv_file_path, headers: true) do |row| 19 | csv_records << table_class.new(row.to_hash) 20 | end 21 | 22 | table_class.import!(csv_records) 23 | end 24 | 25 | private 26 | 27 | CSV_DATA_DIR = 'db/csv' 28 | 29 | def locate_csv_file 30 | csv_file_path = build_csv_file_path 31 | ensure_csv_file_exists(csv_file_path) 32 | csv_file_path 33 | end 34 | 35 | def build_csv_file_path 36 | Rails.root.join("#{CSV_DATA_DIR}/#{table_class.database_name}/#{table_class.table_name}.csv") 37 | end 38 | 39 | def ensure_csv_file_exists(file_path) 40 | return if File.exist?(file_path) 41 | 42 | raise LoadError, "CSV file not found: #{file_path}" 43 | end 44 | 45 | def clear_existing_records 46 | table_class.destroy_all if table_class.exists? 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/javascript/views/admin/users/components/UsersDetailModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /app/models/samples/temporary_migrator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class TemporaryMigrator 5 | attr_reader :table_class 6 | 7 | def initialize(table_class) 8 | @table_class = table_class 9 | end 10 | 11 | def migrate_temporary 12 | load_migration_file 13 | execute_migration 14 | end 15 | 16 | private 17 | 18 | def migration_filename 19 | "create_#{table_class.table_name}" 20 | end 21 | 22 | def temporary_migration_dir 23 | "#{table_class.database_name}_migrate_temporary" 24 | end 25 | 26 | def load_migration_file 27 | migration_file_path = build_migration_file_path 28 | ensure_migration_file_exists(migration_file_path) 29 | require migration_file_path 30 | end 31 | 32 | def build_migration_file_path 33 | Rails.root.join("db/#{temporary_migration_dir}/#{migration_filename}.rb") 34 | end 35 | 36 | def ensure_migration_file_exists(file_path) 37 | return if File.exist?(file_path) 38 | 39 | raise LoadError, "Migration file not found: #{file_path}" 40 | end 41 | 42 | def execute_migration 43 | migration_class = migration_filename.camelize.constantize 44 | migration_class.new(table_class).create_temporary_table 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/javascript/views/admin/works/components/WorksDeleteModal.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 52 | 53 | 58 | -------------------------------------------------------------------------------- /app/controllers/api/admin/works_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Admin 5 | class WorksController < BaseController 6 | before_action :set_work, only: %i[update destroy] 7 | 8 | def index 9 | @works = Work.sort_by_order_number 10 | 11 | render 'index', formats: :json 12 | end 13 | 14 | def create 15 | @work = Work.new(work_params) 16 | 17 | if @work.save 18 | render 'create', formats: :json 19 | else 20 | render json: @work.errors.messages, status: :bad_request 21 | end 22 | end 23 | 24 | def update 25 | if @work.update(work_params) 26 | render 'update', formats: :json 27 | else 28 | render json: @work.errors.messages, status: :bad_request 29 | end 30 | end 31 | 32 | def update_order 33 | Work.update_order(params[:ids]) 34 | head :ok 35 | end 36 | 37 | def destroy 38 | @work.destroy! 39 | 40 | head :ok 41 | end 42 | 43 | private 44 | 45 | def set_work 46 | @work = Work.find(params[:id]) 47 | end 48 | 49 | def work_params 50 | params.require(:work).permit(:name, :slug, :description, :enabled, :published) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/javascript/views/work/components/WorkList.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 46 | 47 | 53 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeTabItemSampleDatabase.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 51 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeToolbarEditor.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 52 | 53 | 60 | -------------------------------------------------------------------------------- /app/controllers/api/admin/chapters_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module Admin 5 | class ChaptersController < BaseController 6 | before_action :set_chapter, only: %i[update destroy] 7 | 8 | def index 9 | @chapters = Chapter.sort_by_order_number 10 | 11 | render 'index', formats: :json 12 | end 13 | 14 | def create 15 | @chapter = Chapter.new(chapter_params) 16 | 17 | if @chapter.save 18 | render 'create', formats: :json 19 | else 20 | render json: @chapter.errors.messages, status: :bad_request 21 | end 22 | end 23 | 24 | def update 25 | if @chapter.update(chapter_params) 26 | render 'update', formats: :json 27 | else 28 | render json: @chapter.errors.messages, status: :bad_request 29 | end 30 | end 31 | 32 | def update_order 33 | Chapter.update_order(params[:ids]) 34 | head :ok 35 | end 36 | 37 | def destroy 38 | @chapter.destroy! 39 | 40 | head :ok 41 | end 42 | 43 | private 44 | 45 | def set_chapter 46 | @chapter = Chapter.find(params[:id]) 47 | end 48 | 49 | def chapter_params 50 | params.require(:chapter).permit(:work_id, :name) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/javascript/views/mypage/components/MypageBookmark.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /app/models/samples/sorting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Samples 4 | class Sorting 5 | # Sorts database tables by their dependency relationships to ensure proper creation order 6 | # Tables with foreign key constraints will be ordered after their referenced tables 7 | def self.sort_by_dependencies(tables, result = []) 8 | tables_copy ||= Array.new(tables) 9 | return result if tables_copy.blank? 10 | 11 | # Process the next table in queue 12 | current_table = tables_copy.shift 13 | # Get all belongs_to associations (foreign key dependencies) 14 | belongs_to_relations = current_table.reflect_on_all_associations(:belongs_to) 15 | # Find dependencies that haven't been processed yet 16 | unresolved_dependencies = belongs_to_relations.map(&:table_name) & tables_copy.map(&:table_name) 17 | 18 | if belongs_to_relations.present? && unresolved_dependencies.present? 19 | # Table has unresolved dependencies - move to end of queue for later processing 20 | tables_copy << current_table 21 | current_table = nil 22 | end 23 | 24 | # Add table to result if it can be processed now (no unresolved dependencies) 25 | result << current_table if current_table.present? 26 | # Recursively process remaining tables 27 | sort_by_dependencies(tables_copy, result) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/models/practice.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Practice < ApplicationRecord 4 | include Sortable 5 | 6 | has_many :sample_tables, dependent: :destroy 7 | has_many :bookmarks, dependent: :destroy 8 | belongs_to :chapter 9 | 10 | validates :name, presence: true 11 | validates :question, presence: true 12 | validates :answer, presence: true 13 | validates :sample_database_id, presence: true, numericality: { only_integer: true } 14 | validates :display_er_diagram, inclusion: { in: [true, false] } 15 | validates :enabled, inclusion: { in: [true, false] } 16 | validates :published, inclusion: { in: [true, false] } 17 | validates :requires_auth, inclusion: { in: [true, false] } 18 | validates :order_number, presence: true, numericality: { only_integer: true } 19 | 20 | scope :published, -> { where(published: true) } 21 | 22 | def update_with_sample_tables!(practice_params, new_sample_table_ids) 23 | update!(practice_params) 24 | 25 | old_sample_table_ids = sample_tables.map(&:uid) 26 | 27 | (old_sample_table_ids - new_sample_table_ids).each do |id| 28 | sample_tables.find_by(uid: id).destroy! 29 | end 30 | (new_sample_table_ids - old_sample_table_ids).each do |id| 31 | sample_tables.create!(uid: id) 32 | end 33 | end 34 | 35 | def bookmarked_by?(user) 36 | bookmarks.pluck(:user_id).include?(user.id) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/javascript/components/BaseModal.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 64 | -------------------------------------------------------------------------------- /app/models/sample_table_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SampleTableDefinition < ActiveHash::Base 4 | # Sample table definitions with database association 5 | self.data = [ 6 | # book_stores database tables 7 | { id: 1, name: 'authors', temporary: false, database_id: 1 }, 8 | { id: 2, name: 'books', temporary: false, database_id: 1 }, 9 | { id: 3, name: 'stores', temporary: false, database_id: 1 }, 10 | { id: 4, name: 'book_sales', temporary: false, database_id: 1 }, 11 | { id: 5, name: 'events', temporary: true, database_id: 1 }, 12 | { id: 6, name: 'categories', temporary: false, database_id: 1 }, 13 | { id: 7, name: 'book_authors', temporary: false, database_id: 1 }, 14 | { id: 8, name: 'book_categories', temporary: false, database_id: 1 } 15 | ] 16 | 17 | # Check if table is temporary 18 | def temporary? 19 | temporary 20 | end 21 | 22 | # Get the database this table belongs to 23 | def database 24 | @database ||= SampleDatabaseDefinition.find_by(id: database_id) 25 | end 26 | 27 | # Get the ActiveRecord class for this table 28 | def table_class 29 | "samples/#{database.name}/#{name}".classify.constantize 30 | end 31 | 32 | class << self 33 | # Get tables for a specific database by name 34 | def for_database(database_name) 35 | database = SampleDatabaseDefinition.find_by(name: database_name) 36 | return [] unless database 37 | 38 | where(database_id: database.id).order(:id) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/javascript/assets/man2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/store/modules/works.js: -------------------------------------------------------------------------------- 1 | import axios from '@/plugins/axios' 2 | import { handleException } from '@/utils/exception' 3 | 4 | function calcWorkChapters(work) { 5 | return work.chapters.length 6 | } 7 | function calcWorkPractices(work) { 8 | let totalPractices = 0 9 | work.chapters.forEach(chapter => (totalPractices += chapter.practices.length)) 10 | return totalPractices 11 | } 12 | 13 | const works = { 14 | namespaced: true, 15 | state: { 16 | work: null, 17 | }, 18 | getters: { 19 | work: state => state.work, 20 | }, 21 | mutations: { 22 | setWork(state, work) { 23 | state.work = work 24 | state.work.totalChapters = calcWorkChapters(work) 25 | state.work.totalPractices = calcWorkPractices(work) 26 | }, 27 | updateBookmark(state, id) { 28 | state.work.chapters.some(chapter => { 29 | let practice = chapter.practices.find(practice => practice.id === id) 30 | if (practice) { 31 | practice.bookmarked = !practice.bookmarked 32 | return true 33 | } 34 | }) 35 | }, 36 | }, 37 | actions: { 38 | async fetchWork({ state, commit }, slug) { 39 | if (state.work === null || state.work.slug != slug) { 40 | axios 41 | .get(`works/${slug}`) 42 | .then(res => commit('setWork', res.data)) 43 | .catch(err => handleException(err)) 44 | } 45 | }, 46 | toggleBookmark({ commit }, id) { 47 | commit('updateBookmark', id) 48 | }, 49 | }, 50 | } 51 | 52 | export default works 53 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root 'home#index' 3 | namespace :api do 4 | resources :works, param: :slug, only: %i[index show] 5 | resources :practices, only: %i[show] 6 | resources :bookmarks, only: %i[index create destroy] 7 | resource :auth_user, only: %i[show update destroy] 8 | 9 | post 'oauth/callback', to: 'oauths#callback' 10 | get 'oauth/callback', to: 'oauths#callback' 11 | get 'oauth/:provider', to: 'oauths#oauth', as: :auth_at_provider 12 | delete 'logout', to: 'user_sessions#destroy' 13 | 14 | namespace :samples do 15 | post 'query', to: "queries#execute" 16 | end 17 | 18 | namespace :admin do 19 | resources :users, only: %i[index destroy] 20 | 21 | resources :works, only: %i[index create update destroy] do 22 | patch 'order', to: 'works#update_order', on: :collection 23 | end 24 | resources :chapters, only: %i[index create update destroy] do 25 | patch 'order', to: 'chapters#update_order', on: :collection 26 | end 27 | resources :practices, only: %i[index create update destroy] do 28 | patch 'order', to: 'practices#update_order', on: :collection 29 | end 30 | resources :sample_databases, only: %i[index] 31 | end 32 | end 33 | 34 | # for development/test login 35 | get '/login_as/:user_id', to: 'development/user_sessions#login_as' unless Rails.env.production? 36 | 37 | get '*path', to: 'home#index', constraints: lambda { |req| req.path.exclude? 'rails/active_storage' } 38 | end 39 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeFooter.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 65 | -------------------------------------------------------------------------------- /app/javascript/views/work/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 59 | 60 | 66 | -------------------------------------------------------------------------------- /app/javascript/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export function zeroPadding(num) { 2 | if (!isNumber(num)) return num 3 | return num.toString().padStart(2, '0') 4 | } 5 | 6 | export function toPixel(val) { 7 | return parseInt(val) + 'px' 8 | } 9 | 10 | export function isNumber(val) { 11 | return typeof val === 'number' 12 | } 13 | 14 | export function isString(val) { 15 | return typeof val === 'string' 16 | } 17 | 18 | export function isEmpty(obj) { 19 | return [Object, Array].includes((obj || {}).constructor) && !Object.entries(obj || {}).length 20 | } 21 | 22 | export function anchorTag(val) { 23 | return '#' + val 24 | } 25 | 26 | export function camelCase(str) { 27 | if (!isString) return str 28 | 29 | const strs = str.split(/[-_ ]+/) 30 | const length = strs.length 31 | 32 | if (length <= 1) return str 33 | 34 | str = strs[0].toLowerCase() 35 | 36 | for (let i = 1; i < length; i++) { 37 | str += strs[i].toLowerCase().replace(/^[a-z]/, function (val) { 38 | return val.toUpperCase() 39 | }) 40 | } 41 | 42 | return str 43 | } 44 | 45 | export function intersectionBy(array, values, key = null) { 46 | return array.filter(el => values.includes(el[key] || el)) 47 | } 48 | 49 | export function pullObjectFrom(arr, obj, key = 'id') { 50 | return arr.filter(el => el[key] !== obj[key]) 51 | } 52 | 53 | export function replaceObjFrom(arr, obj, key = 'id') { 54 | const item = arr.findIndex(el => el[key] === obj[key]) 55 | arr.splice(item, 1, obj) 56 | } 57 | 58 | export function removeTrailingSlash(item) { 59 | return item.replace(/\/$/, '') 60 | } 61 | -------------------------------------------------------------------------------- /config/initializers/meta_tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Use this setup block to configure all options available in MetaTags. 4 | MetaTags.configure do |config| 5 | # How many characters should the title meta tag have at most. Default is 70. 6 | # Set to nil or 0 to remove limits. 7 | # config.title_limit = 70 8 | 9 | # When true, site title will be truncated instead of title. Default is false. 10 | # config.truncate_site_title_first = false 11 | 12 | # Maximum length of the page description. Default is 300. 13 | # Set to nil or 0 to remove limits. 14 | # config.description_limit = 300 15 | 16 | # Maximum length of the keywords meta tag. Default is 255. 17 | # config.keywords_limit = 255 18 | 19 | # Default separator for keywords meta tag (used when an Array passed with 20 | # the list of keywords). Default is ", ". 21 | # config.keywords_separator = ', ' 22 | 23 | # When true, keywords will be converted to lowercase, otherwise they will 24 | # appear on the page as is. Default is true. 25 | # config.keywords_lowercase = true 26 | 27 | # When true, the output will not include new line characters between meta tags. 28 | # Default is false. 29 | # config.minify_output = false 30 | 31 | # When false, generated meta tags will be self-closing () instead 32 | # of open (``). Default is true. 33 | # config.open_meta_tags = true 34 | 35 | # List of additional meta tags that should use "property" attribute instead 36 | # of "name" attribute in tags. 37 | # config.property_tags.push( 38 | # 'x-hearthstone:deck', 39 | # ) 40 | end 41 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Sqlab 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 7.2 13 | 14 | # Please, add to the `ignore` list any other `lib` subdirectories that do 15 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 16 | # Common ones are `templates`, `generators`, or `middleware`, for example. 17 | config.autoload_lib(ignore: %w[assets tasks]) 18 | 19 | # Configuration for the application, engines, and railties goes here. 20 | # 21 | # These settings can be overridden in specific environments using the files 22 | # in config/environments, which are processed later. 23 | # 24 | # config.time_zone = "Central Time (US & Canada)" 25 | # config.eager_load_paths << Rails.root.join("extras") 26 | config.time_zone = 'Tokyo' 27 | config.active_record.default_timezone = :local 28 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] 29 | config.i18n.available_locales = %i[ja] 30 | config.i18n.default_locale = :ja 31 | 32 | config.generators do |g| 33 | g.stylesheets false 34 | g.javascripts false 35 | g.helper false 36 | g.test_framework nil 37 | g.system_tests nil 38 | g.factory_bot dir: 'spec/factories' 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/javascript/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import router from '@/router/index' 2 | 3 | const app = { 4 | namespaced: true, 5 | state: { 6 | sidebarWidth: 72, 7 | adminSidebarWidth: 256, 8 | footerHeight: 0, 9 | flashMessageActive: false, 10 | flashMessageType: '', 11 | isVisibleLoginModal: false, 12 | }, 13 | getters: { 14 | sidebarWidth: state => state.sidebarWidth, 15 | adminSidebarWidth: state => state.adminSidebarWidth, 16 | isPracticePage: () => router.app.$route.name === 'Practice', 17 | footerHeight: state => state.footerHeight, 18 | flashMessageActive: state => state.flashMessageActive, 19 | flashMessageType: state => state.flashMessageType, 20 | isVisibleLoginModal: state => state.isVisibleLoginModal, 21 | }, 22 | mutations: { 23 | setFooterHeight(state, height) { 24 | state.footerHeight = height 25 | }, 26 | setFlashMessage(state, flashMessageType) { 27 | state.flashMessageType = flashMessageType 28 | state.flashMessageActive = true 29 | setTimeout(() => { 30 | state.flashMessageActive = false 31 | }, 3000) 32 | }, 33 | setLoginModal(state, isVisible) { 34 | state.isVisibleLoginModal = isVisible 35 | }, 36 | }, 37 | actions: { 38 | updateFooterHeight({ commit }, height) { 39 | commit('setFooterHeight', height) 40 | }, 41 | openFlashMessage({ commit }, flashMessageType) { 42 | commit('setFlashMessage', flashMessageType) 43 | }, 44 | switchLoginModal({ commit }, isVisible) { 45 | commit('setLoginModal', isVisible) 46 | }, 47 | }, 48 | } 49 | 50 | export default app 51 | -------------------------------------------------------------------------------- /app/javascript/assets/man.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/views/admin/users/components/UsersTable.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 52 | 53 | 68 | -------------------------------------------------------------------------------- /app/javascript/views/admin/works/components/WorksDetailModal.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 65 | 66 | 71 | -------------------------------------------------------------------------------- /app/javascript/views/top/components/TopFaq.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 64 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # Allow @vite/client to hot reload javascript changes in development 15 | # policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? 16 | 17 | # You may need to enable this in production as well depending on your setup. 18 | # policy.script_src *policy.script_src, :blob if Rails.env.test? 19 | 20 | # policy.style_src :self, :https 21 | # Allow @vite/client to hot reload style changes in development 22 | # policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? 23 | 24 | # # Specify URI for violation reports 25 | # # policy.report_uri "/csp-violation-report-endpoint" 26 | # end 27 | # 28 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 29 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 30 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 31 | # 32 | # # Report violations without enforcing the policy. 33 | # # config.content_security_policy_report_only = true 34 | # end 35 | -------------------------------------------------------------------------------- /app/javascript/assets/woman.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/views/admin/works/components/WorksFooter.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 56 | 57 | 65 | -------------------------------------------------------------------------------- /app/javascript/layout/default/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 62 | 63 | 68 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # The ideal number of threads per worker depends both on how much time the 9 | # application spends waiting for IO operations and on how much you wish to 10 | # to prioritize throughput over latency. 11 | # 12 | # As a rule of thumb, increasing the number of threads will increase how much 13 | # traffic a given process can handle (throughput), but due to CRuby's 14 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 15 | # response time (latency) of the application. 16 | # 17 | # The default is set to 3 threads as it's deemed a decent compromise between 18 | # throughput and latency for the average Rails application. 19 | # 20 | # Any libraries that use a connection pool or another resource pool should 21 | # be configured to provide at least as many connections as the number of 22 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 23 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 24 | threads threads_count, threads_count 25 | 26 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 27 | port ENV.fetch("PORT", 3000) 28 | 29 | # Allow puma to be restarted by `bin/rails restart` command. 30 | plugin :tmp_restart 31 | 32 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 33 | # In other environments, only set the PID file if requested. 34 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 35 | -------------------------------------------------------------------------------- /app/javascript/views/practice/components/PracticeEditor.vue: -------------------------------------------------------------------------------- 1 |