├── public ├── script.js └── style.css ├── database ├── create_database.sql ├── create_topics.sql ├── create_posts.sql └── seeds.sql ├── Gemfile ├── models ├── application_entity.rb ├── db.rb ├── post.rb └── topic.rb ├── setup.sh ├── console.rb ├── Gemfile.lock ├── app.rb ├── views ├── index.slim └── show.slim └── README.md /public/script.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/create_database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS 0ch; 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gem "sinatra" 5 | gem "slim" 6 | gem "mysql2" 7 | -------------------------------------------------------------------------------- /models/application_entity.rb: -------------------------------------------------------------------------------- 1 | require_relative 'db' 2 | 3 | class ApplicationEntity 4 | class Invalid < StandardError 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mysql -u root < ./database/create_database.sql 4 | mysql -u root 0ch < ./database/create_topics.sql 5 | mysql -u root 0ch < ./database/create_posts.sql 6 | mysql -u root 0ch < ./database/seeds.sql 7 | -------------------------------------------------------------------------------- /console.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | 5 | require_relative 'models/db' 6 | require_relative 'models/topic' 7 | require_relative 'models/post' 8 | 9 | require "irb" 10 | IRB.start(__FILE__) 11 | -------------------------------------------------------------------------------- /database/create_topics.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS topics ( 2 | id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL, 3 | title VARCHAR(255) NOT NULL, 4 | created_at DATETIME NOT NULL, 5 | updated_at DATETIME NOT NULL, 6 | PRIMARY KEY (id) 7 | ); 8 | -------------------------------------------------------------------------------- /models/db.rb: -------------------------------------------------------------------------------- 1 | require 'mysql2' 2 | 3 | class DB 4 | def self.query(sql) 5 | client.query(sql, symbolize_keys: true) 6 | end 7 | 8 | private 9 | 10 | def self.client 11 | @client ||= Mysql2::Client.new(host: 'localhost', username: 'root', database: '0ch') 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /database/create_posts.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS posts ( 2 | id INTEGER UNSIGNED AUTO_INCREMENT NOT NULL, 3 | topic_id INTEGER UNSIGNED NOT NULL, 4 | name VARCHAR(255), 5 | email VARCHAR(255), 6 | body TEXT NOT NULL, 7 | created_at DATETIME NOT NULL, 8 | PRIMARY KEY (id) 9 | ); 10 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px; 3 | } 4 | 5 | .posts .post { 6 | margin-bottom: 20px; 7 | } 8 | 9 | .posts .post span { 10 | margin-right: 10px; 11 | } 12 | 13 | .posts .post .body { 14 | padding: 10px 0; 15 | } 16 | 17 | .new-post label { 18 | margin-right: 10px; 19 | } 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | mustermann (1.0.0) 5 | mysql2 (0.4.7) 6 | rack (2.0.3) 7 | rack-protection (2.0.0) 8 | rack 9 | sinatra (2.0.0) 10 | mustermann (~> 1.0) 11 | rack (~> 2.0) 12 | rack-protection (= 2.0.0) 13 | tilt (~> 2.0) 14 | slim (3.0.8) 15 | temple (>= 0.7.6, < 0.9) 16 | tilt (>= 1.3.3, < 2.1) 17 | temple (0.8.0) 18 | tilt (2.0.8) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | mysql2 25 | sinatra 26 | slim 27 | 28 | BUNDLED WITH 29 | 1.15.1 30 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'slim' 3 | 4 | require_relative 'models/topic' 5 | require_relative 'models/post' 6 | 7 | get '/' do 8 | @topics = Topic.all 9 | slim :index 10 | end 11 | 12 | post '/topics' do 13 | topic = Topic.new(title: params[:title]) 14 | topic.save 15 | redirect '/' 16 | end 17 | 18 | get '/topics/:id' do 19 | @topic = Topic.find(params[:id]) 20 | @posts = Post.of_topic(@topic.id) 21 | slim :show 22 | end 23 | 24 | post '/topics/:topic_id/posts' do 25 | name = params[:name].empty? ? '名無し' : params[:name] 26 | post = Post.new(name: name, email: params[:email], body: params[:body], topic_id: params[:topic_id]) 27 | post.save 28 | redirect back 29 | end 30 | -------------------------------------------------------------------------------- /database/seeds.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO topics (title, created_at, updated_at) VALUES ('スレタイ1', NOW(), NOW()); 2 | INSERT INTO topics (title, created_at, updated_at) VALUES ('スレタイ2', NOW(), NOW()); 3 | 4 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス1', 1, NOW()); 5 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス2', 1, NOW()); 6 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス3', 1, NOW()); 7 | 8 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス4', 2, NOW()); 9 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス5', 2, NOW()); 10 | INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('名無し', '', 'レス6', 2, NOW()); 11 | -------------------------------------------------------------------------------- /views/index.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html lang="ja" 3 | head 4 | meta charset="utf-8" 5 | title ぜろちゃんねる 6 | link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" rel="stylesheet" 7 | link href="/style.css?#{Time.now.to_i}" rel="stylesheet" 8 | script src="/script.js?#{Time.now.to_i}" 9 | body 10 | h1 ぜろちゃんねる 11 | p sinatraで掲示板を作ろう 12 | p 13 | a href="https://github.com/ttanimichi/0ch" https://github.com/ttanimichi/0ch 14 | main 15 | h2 スレッド一覧 16 | ul 17 | - @topics.each do |topic| 18 | li 19 | a href="/topics/#{topic.id}" 20 | = topic.title 21 | h2 新規スレッド作成 22 | form action="/topics" method="post" 23 | div 24 | input name="title" type="text" 25 | div 26 | input type="submit" value="作成する" 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0ch 2 | 3 | 「sinatraで掲示板を作ろう」の解答例です。 4 | 5 | ## sinatraで掲示板を作ろう 6 | 7 | - Rails 使用禁止です。sinatra ( https://github.com/sinatra/sinatra ) を使ってください 8 | - 2ch のような掲示板を作ってください。ただし 2ch 全体ではなく、ひとつの板だけ作ればいいです 9 | - 板 has_many スレ 10 | - スレ has_many 投稿 11 | - 自分で Gemfile を作成して bundle install してください 12 | - app.rb 1ファイルにすべて書くのではなく、適宜ファイルを分割して require するようにしてください 13 | - ActiveRecord 使用禁止です。生 SQL を書いてください 14 | - SELECT とか INSERT とか 15 | - DB アダプターには `gem 'mysql2'` を使用してください 16 | - ただし SQL インジェクション対策はしなくても可とします 17 | - テーブルの作成などマイグレーションも SQL を書いて手動でやりましょう 18 | - ActiveSupport も使用禁止です。素の Ruby で実装しましょう 19 | - CSS は生 CSS で良いです。Sass は使わなくても良いです 20 | - haml を使っても erb を使っても slim を使っても良いです 21 | - ここまで出来た人は以下の課題にもチャレンジしてみましょう 22 | - 指定したスレの投稿をすべて消す rake task を作成してください 23 | - Rakefile も自分で作成してください 24 | - sage 機能を実装してください 25 | - メールアドレス欄に sage と書くとスレが上に来ないようにできる機能のことです 26 | -------------------------------------------------------------------------------- /models/post.rb: -------------------------------------------------------------------------------- 1 | require_relative 'application_entity' 2 | 3 | class Post < ApplicationEntity 4 | MIN_SIZE = 1 5 | MAX_SIZE = 140 6 | 7 | attr_reader :name, :email, :body, :topic_id, :id, :created_at 8 | 9 | def initialize(name: nil, email: nil, body:, topic_id:, id: nil, created_at: nil) 10 | @name, @email, @body, @topic_id, @id, @created_at = name, email, body, topic_id, id, created_at 11 | end 12 | 13 | def self.of_topic(topic_id) 14 | DB.query("SELECT * FROM posts WHERE topic_id = #{topic_id}").map { |hash| new(hash) } 15 | end 16 | 17 | def save 18 | validate 19 | DB.query("INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('#{@name}', '#{@email}', '#{@body}', '#{@topic_id}', NOW())") 20 | end 21 | 22 | def validate 23 | unless @body.size >= MIN_SIZE && @body.size <= MAX_SIZE 24 | raise Invalid, "本文は#{MIN_SIZE}文字以上、#{MAX_SIZE}文字以下で指定してください" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /models/topic.rb: -------------------------------------------------------------------------------- 1 | require_relative 'application_entity' 2 | 3 | class Topic < ApplicationEntity 4 | MIN_SIZE = 1 5 | MAX_SIZE = 50 6 | 7 | attr_reader :title, :id, :created_at, :updated_at 8 | 9 | def initialize(id: nil, title:, created_at: nil, updated_at: nil) 10 | @id, @title, @created_at, @updated_at = id, title, created_at, updated_at 11 | end 12 | 13 | def self.all 14 | DB.query('SELECT * FROM topics ORDER BY updated_at DESC').map { |hash| new(hash) } 15 | end 16 | 17 | def self.find(id) 18 | result = DB.query("SELECT * FROM topics WHERE id = #{id} LIMIT 1").first 19 | new(result) 20 | end 21 | 22 | def save 23 | validate 24 | DB.query("INSERT INTO topics (title, created_at, updated_at) VALUES ('#{@title}', NOW(), NOW())") 25 | end 26 | 27 | def validate 28 | unless @title.size >= MIN_SIZE && @title.size <= MAX_SIZE 29 | raise Invalid, "タイトルは#{MIN_SIZE}文字以上、#{MAX_SIZE}文字以下で指定してください" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /views/show.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html lang="ja" 3 | head 4 | meta charset="utf-8" 5 | title ぜろちゃんねる 6 | link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" rel="stylesheet" 7 | link href="/style.css?#{Time.now.to_i}" rel="stylesheet" 8 | script src="/script.js?#{Time.now.to_i}" 9 | body.container 10 | main 11 | h1= @topic.title 12 | 13 | .posts 14 | - @posts.each do |post| 15 | .post 16 | .meta 17 | span= post.name 18 | span= post.created_at.strftime('%Y年%m月%d日 %H:%M:%S') 19 | .body 20 | span= post.body 21 | 22 | .new-post 23 | form action="/topics/#{@topic.id}/posts" method="post" 24 | .meta 25 | label 26 | | 名前: 27 | input name="name" type="text" 28 | label 29 | | Email: 30 | input name="email" type="text" 31 | .body 32 | textarea cols="50" name="body" rows="4" 33 | div 34 | input type="submit" value="書き込む" 35 | 36 | a href="/" トップページへ 37 | --------------------------------------------------------------------------------