├── 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 |
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 |
2 | ダッシュボード
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
6 |
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 |
2 |
6 |
7 |
8 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/app/javascript/views/mypage/components/MypageCardTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
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 |
2 |
7 |
8 |
9 |
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 |
2 |
3 |
10 | mdi-flask-empty-outline
11 |
12 |
13 |
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 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseTabItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
24 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
24 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeResizer.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
15 |
16 |
23 |
--------------------------------------------------------------------------------
/app/javascript/views/top/components/TopSectionTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
26 |
--------------------------------------------------------------------------------
/app/javascript/components/app/AppContainerAdmin.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
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 |
2 |
3 | 404
4 | Page Not Found
5 | お探しのページは見つかりませんでした。
6 |
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/app/javascript/layout/default/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
23 |
--------------------------------------------------------------------------------
/app/javascript/layout/admin/components/AdminNavbar/components/AdminNavbarLogout.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
13 | mdi-logout
14 |
15 | ログアウト
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
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 |
2 |
9 | mdi-close
10 |
11 |
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 |
2 |
3 | 500
4 | Internal Server Error
5 |
6 | システムエラーが発生しました。
7 | お手数ですが、しばらく時間をおいてから再度お試しください。
8 |
9 |
10 |
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 |
2 |
3 | ログイン
4 |
5 |
6 |
7 |
8 |
21 |
22 |
27 |
--------------------------------------------------------------------------------
/app/javascript/components/app/AppPageHeading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
8 | {{ work.name }}
9 | 全{{ work.totalPractices }}問
10 |
11 |
12 |
13 |
24 |
--------------------------------------------------------------------------------
/app/javascript/components/app/AppIconLock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
14 | mdi-lock
15 |
16 |
17 | 要ログイン
18 |
19 |
20 |
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 |
2 |
10 |
11 |
12 |
13 |
14 |
26 |
27 |
34 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseProgressLinear.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | {{ value }}%
10 |
11 |
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 |
2 |
11 |
12 |
13 |
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 |
2 |
9 |
10 |
11 |
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 |
2 |
9 |
16 | 新しいタブでER図を開く
17 |
18 |
19 |
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 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
36 |
--------------------------------------------------------------------------------
/app/javascript/views/work/components/WorkDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | 概要
8 | {{ work.description }}
9 |
10 | 問題数
11 | 全{{ work.totalPractices }}問
12 |
13 |
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 |
2 |
3 |
10 | {{ item.icon }}
11 |
12 |
13 |
17 | {{ item.tooltip }}
18 |
19 |
20 |
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 |
2 |
8 |
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 |
2 |
6 | ログイン
7 |
8 |
9 |
13 |
14 |
15 |
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 |
2 |
3 | どんなサービス?
4 |
5 |
6 | SQLの練習ができる学習サービスです。
7 | 練習問題が豊富で、環境構築不要!ブラウザ上ですぐにSQLを書いて結果を確認することができます。
8 | SQLabを活用して技術書などで学んだSQLの知識をスキルに変えていきましょう!
9 |
10 |
11 |
12 |
13 |
23 |
24 |
31 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
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 |
2 |
18 |
19 |
20 |
35 |
36 |
43 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeTabItemEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
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 |
2 |
13 |
14 |
15 |
43 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
44 |
--------------------------------------------------------------------------------
/app/javascript/layout/default/components/UserDrawer/components/UserDrawerHeading.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
16 | mdi-account
17 |
18 |
19 | {{ user.name }}
20 | {{ user.email }}
21 |
22 |
23 |
24 |
25 |
36 |
--------------------------------------------------------------------------------
/app/javascript/components/BaseSwitch.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
39 |
40 |
47 |
--------------------------------------------------------------------------------
/app/javascript/components/app/AppLogoLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
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 |
2 |
13 |
14 |
15 |
16 |
17 |
45 |
--------------------------------------------------------------------------------
/app/javascript/views/works/components/WorksCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | {{ work.name }}
9 |
10 | {{ work.description }}
11 |
12 |
19 | 練習問題はこちら
20 |
21 |
22 |
23 |
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 |
2 |
9 | {{ flashMessage.text }}
10 |
11 |
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 |
2 |
7 | 設定
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
{{ concept }}
4 |
5 |
6 |
13 | 問題を解いてみる
14 |
15 |
16 |
17 |
18 |
19 |
30 |
31 |
47 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeQuestion.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 | mdi-pencil
13 |
14 |
15 |
19 |
20 |
21 |
22 |
33 |
34 |
47 |
--------------------------------------------------------------------------------
/app/javascript/views/top/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
7 | アカウント削除
8 |
9 |
10 | 本当に削除しますか?
11 |
12 |
13 |
14 |
19 | 削除
20 |
21 |
26 | キャンセル
27 |
28 |
29 |
30 |
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 |
2 |
7 | アカウント削除
8 |
9 |
10 |
11 | 本当に削除しますか?
12 | アカウントを削除をした場合、アカウントに紐づく全てのデータが失われ、復元することはできません。
15 |
16 |
17 |
18 |
19 |
24 | 削除
25 |
26 |
31 | キャンセル
32 |
33 |
34 |
35 |
36 |
37 |
48 |
--------------------------------------------------------------------------------
/app/javascript/components/app/AppIconBookmark.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ icon }}
17 |
18 |
19 | {{ tooltipText }}
20 |
21 |
22 |
23 |
24 |
46 |
--------------------------------------------------------------------------------
/app/javascript/layout/admin/components/AdminNavbar/components/AdminNavbarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | {{ item.icon }}
14 |
15 |
16 |
17 | {{ item.title }}
18 |
19 |
20 |
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 |
2 |
6 |
7 |
8 | 問題集一覧
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
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 |
2 |
3 |
4 | 練習問題クリア回数
5 |
11 | mdi-information-outline
12 |
13 |
14 |
15 |
20 | 練習問題をクリアする度に回数が増えます
21 |
22 |
23 |
27 | 0問
28 |
29 |
30 |
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 |
2 |
8 |
9 |
10 | |
14 | {{ column }}
15 | |
16 |
17 |
18 |
19 |
23 | |
27 | {{ value }}
28 | |
29 |
30 |
31 |
32 |
33 |
34 |
56 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeModalExampleAnswer.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | 解答例
7 |
8 |
9 |
18 |
19 |
20 |
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 |
2 |
6 |
7 |
8 |
![ER diagram]()
14 |
15 |
16 |
17 |
18 | mdi-image-off-outline
19 |
20 |
21 |
22 |
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 |
2 |
6 | アカウント詳細
7 |
8 |
9 |
10 |
11 |
15 | | {{ $t(key) }} |
16 | {{ value }} |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 | 削除
29 |
30 |
31 |
32 |
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 |
2 |
7 | {{ modalTitle }}
8 |
9 |
10 | 本当に削除しますか?
11 |
12 |
13 |
14 |
19 | 削除
20 |
21 |
26 | キャンセル
27 |
28 |
29 |
30 |
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 |
2 |
3 |
11 |
12 | {{ chapter.name }}
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
46 |
47 |
53 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeTabItemSampleDatabase.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-table-off
6 |
7 |
8 |
9 |
15 |
16 | {{ slotProps.table.name }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
51 |
--------------------------------------------------------------------------------
/app/javascript/views/practice/components/PracticeToolbarEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
15 | mdi-play
16 |
17 |
21 | 実行
22 |
23 |
24 |
30 | mdi-restore
31 |
32 |
36 | リセット
37 |
38 |
39 |
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 |
2 |
3 |
お気に入りの練習問題
4 |
5 |
9 |
10 |
19 |
20 |
24 | お気に入りの練習問題はありません。
25 |
26 |
27 |
28 |
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 |
2 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
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 |
2 |
3 |
11 | 答え合わせ
12 |
13 |
14 |
20 | 解答例
21 |
22 |
23 |
24 |
25 |
29 | mdi-cog
30 |
31 |
35 | 設定
36 |
37 |
38 |
43 | mdi-keyboard-settings-outline
44 |
45 |
49 | ショートカット
50 |
51 |
52 |
53 |
54 |
65 |
--------------------------------------------------------------------------------
/app/javascript/views/work/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | {{ work.name }}
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
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 |
2 |
3 |
4 |
5 | | ID |
6 | 名前 |
7 | 権限 |
8 | |
9 |
10 |
11 |
12 |
16 | | {{ user.id }} |
17 | {{ user.name }} |
18 | {{ user.role }} |
19 |
20 |
25 | 詳細
26 |
27 |
33 | 削除
34 |
35 | |
36 |
37 |
38 |
39 |
40 |
41 |
52 |
53 |
68 |
--------------------------------------------------------------------------------
/app/javascript/views/admin/works/components/WorksDetailModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ modalTitle }}
7 |
8 |
9 |
10 |
11 |
15 | | {{ $t(key) }} |
16 | {{ value }} |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 | 編集
29 |
30 |
35 | 削除
36 |
37 |
38 |
39 |
40 |
41 |
65 |
66 |
71 |
--------------------------------------------------------------------------------
/app/javascript/views/top/components/TopFaq.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
よくあるご質問
4 |
5 |
6 |
11 |
17 |
20 | {{ faqItem.content }}
21 |
22 |
23 |
24 |
25 |
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 |
2 |
34 |
35 |
36 |
56 |
57 |
65 |
--------------------------------------------------------------------------------
/app/javascript/layout/default/components/TheFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
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 |
2 |
3 |
4 |
5 |
57 |
58 |
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sqlab",
3 | "private": true,
4 | "dependencies": {
5 | "@rails/actioncable": "^7.1.0",
6 | "@rails/activestorage": "^7.1.0",
7 | "@rails/ujs": "^7.1.0",
8 | "axios": "^1.7.7",
9 | "codemirror": "^5.65.20",
10 | "dayjs": "^1.11.13",
11 | "lodash.clonedeep": "^4.5.0",
12 | "turbolinks": "^5.2.0",
13 | "vee-validate": "^3.4.13",
14 | "vue": "^2.7.16",
15 | "vue-gtag": "^1.16.1",
16 | "vue-i18n": "^8",
17 | "vue-loader": "^15.9.8",
18 | "vue-router": "^3.5.3",
19 | "vue-template-compiler": "^2.7.16",
20 | "vuedraggable": "^2.24.3",
21 | "vuetify": "^2.6.7",
22 | "vuex": "^3.6.2"
23 | },
24 | "version": "0.1.0",
25 | "devDependencies": {
26 | "@mdi/font": "^6.9.96",
27 | "@types/codemirror": "^5.60.16",
28 | "@types/lodash.clonedeep": "^4.5.9",
29 | "@vitejs/plugin-vue2": "^2.3.4",
30 | "@vue/test-utils": "^1.2.2",
31 | "babel-core": "^7.0.0-bridge.0",
32 | "babel-jest": "^27.5.1",
33 | "babel-preset-env": "^1.7.0",
34 | "eslint": "^8.57.1",
35 | "eslint-config-prettier": "^8.10.2",
36 | "eslint-plugin-vue": "^8.7.1",
37 | "jest": "^27.5.1",
38 | "postcss-flexbugs-fixes": "^5.0.2",
39 | "postcss-import": "^16.1.1",
40 | "postcss-preset-env": "^10.4.0",
41 | "prettier": "^2.8.8",
42 | "sass-embedded": "^1.93.2",
43 | "vite": "^7.1.0",
44 | "vite-plugin-full-reload": "^1.2.0",
45 | "vite-plugin-ruby": "^5.1.0",
46 | "vue-jest": "^3.0.7"
47 | },
48 | "scripts": {
49 | "lint": "eslint --ext .js,.vue app/javascript",
50 | "lint-fix": "eslint --fix --ext .js,.vue app/javascript",
51 | "format": "prettier --write app/javascript",
52 | "fix": "yarn format && yarn lint-fix",
53 | "test": "jest"
54 | },
55 | "engines": {
56 | "node": ">=24.0.0",
57 | "yarn": "1.x"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/javascript/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------