カレーの恩返し

おいしいのでオススメ。

`rails new my_app`を解読してみた

Railsアプリを作成するときに毎回使う rails new my_appで何が起きているか知りたかったのでソースを読み解いてみました。

初学者なので解説が間違っている, 解説の粒度がバラバラで読みにくい可能性があります。
何かありましたらコメントください。
コードは時系列で見ていくのでファイルを行ったり来たりしています。

設定

  • .railsrcは作成していない
  • 開発用PCはmac

概要

(1) bundle install railsRailsをインストールする
(2) bundle exec rails new my_appがユーザーによって実行される。
(3) Rails::AppRailsLoader#exec_app_railsが実行される。
(4) Rails::CommandsTasks#newを実行する。
(5) Rails::Generators::AppGenerator#startを実行する。
(6)Rails::Generators::AppGenerator#invoke_allを実行する。
(7) Thor::Command#runを実行する。
(8) Rails::Generators::AppBase#buildを実行する。

コマンドの実行

まずはユーザーがbundle install railsRailsをインストールする。

次にbundle exec rails new my_appを実行する。
bundlerがどんな挙動をしてるのかよく分からないけど今回はrails newの解読なのでとりあえず放置。

railsコマンドはrailties/bin/railsにあった。

#railties/bin/rails

#!/usr/bin/env ruby

git_path = File.expand_path('../../../.git', __FILE__)

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"
  • $:$LOAD_PATHのショートカットらしい。
  • railties/lib$LOAD_PATHに追加してrails/cliをrequireしている。

 

require "rails/cli"を追いかける。

#railties/lib/rails/cli.rb

require 'rails/app_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppRailsLoader.exec_app_rails

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin'
else
  require 'rails/commands/application'
end
  • #exec_app_railsの内部でexecが実行されるため実行に成功すると処理が戻ってこないため以降は実行されない。

 

Rails::AppRailsLoader.exec_app_railsを追いかける。

require 'pathname'

module Rails
  module AppRailsLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ['bin/rails', 'script/rails']
    BUNDLER_WARNING = <<EOS
Looks like your app's ./bin/rails is a stub that was generated by Bundler.

In Rails 4, your app's bin/ directory contains executables that are versioned
like any other source code, rather than stubs that are generated on demand.

Here's how to upgrade:

  bundle config --delete bin    # Turn off Bundler's stub generator
  rake rails:update:bin         # Use the new Rails 4 executables
  git add bin                   # Add bin/ to source control

You may need to remove bin/ from your .gitignore as well.

When you install a gem whose executable you want to use in your app,
generate it and add it to source control:

  bundle binstubs some-gem-name
  git add bin/new-executable

EOS

    def exec_app_rails
      original_cwd = Dir.pwd
      # original_cwd => カレントディレクトリの絶対パス

      loop do
        if exe = find_executable
          # exe => 'bin/rails'
          contents = File.read(exe)
          # contents =>
          #   APP_PATH = File.expand_path('../../config/application', __FILE__)
          #   require_relative '../config/boot'
          #   require 'rails/commands'

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
            require File.expand_path('../boot', APP_PATH)
            require 'rails/commands'
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end
  • contentsにはrailties/lib/rails/generators/rails/app/templates/bin/railsのソースが読み込まれている。
  • #exec_app_railsではrailties/lib/rails/generators/rails/app/templates/bin/railsARGVを引数として実行している。

 

exec RUBY, exe, *ARGVを追いかける。
exec RUBY, exe, *ARGV => % ruby railties/lib/rails/generators/rails/app/templates/bin/rails new my_app

#railties/lib/rails/generators/rails/app/templates/bin/rails

APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
  • ../config/bootrails/commandsをrequireしている。

 

require_relative '../config/boot'を追いかける。

#railties/lib/rails/generators/rails/app/templates/config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.
  • デフォルトでGemfileに記述されているrubygemをrequireしている。

 

require 'rails/commands'を追いかける。

#railties/lib/rails/commands.rb

# ARGV => ['new', 'my_app']
ARGV << '--help' if ARGV.empty?  

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}

command = ARGV.shift
command = aliases[command] || command
# command => 'new'

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)
  • railsコマンドの引数を渡して生成したRails::CommandsTasksインスタンス'new'を引数にrun_command!メソッドを実行している。
  • 引数を与えなかったらhelpが表示される理由が分かった。

 

Rails::CommandsTasks.new(ARGV).run_command!(command)を追いかける。

#railties/lib/rails/commands/commands_tasks.rb

module Rails
  class CommandsTasks # :nodoc:
    attr_reader :argv

    # ...   

    COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)

    def initialize(argv)
      @argv = argv
    end

    def run_command!(command)
      command = parse_command(command)
      # command => 'new'
      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        write_error_message(command)
      end
    end
    
    # ...

    def new
      # argv.first => 'new'
      if %w(-h --help).include?(argv.first)
        require_command!("application")
      else
        exit_with_initialization_warning!
      end
    end

    private

      # ...

      def require_command!(command)
        require "rails/commands/#{command}"
      end

      # ...

      def parse_command(command)
        case command
        when '--version', '-v'
          'version'
        when '--help', '-h'
          'help'
        else
          command
        end
      end
end
  • #run_command!ではcommand#parse_commandを通した後COMMAND_WHITELISTに存在するか確かめて#newを実行している。
  • #newでは'new','-h''--help'がに含まれていないので#exit_with_initialization_warning!が実行されるのかと思ったけど#require_command!が実行されている。分からない。
  • #require_command!ではrails/commands/applicationをrequireしている。

 

require "rails/commands/#{command}"を追いかける。

#railties/lib/rails/commands/application.rb

require 'rails/generators'
require 'rails/generators/rails/app/app_generator'

module Rails
  module Generators
    class AppGenerator # :nodoc:
      # We want to exit on failure to be kind to other libraries
      # This is only when accessing via CLI
      def self.exit_on_failure?
        true
      end
    end
  end
end

# ARGV => ['new', 'my_app']
args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
# args => ['my_app']
Rails::Generators::AppGenerator.start args
  • ARGVについている各オプションを元にargsに整形する。今回はオプションをつけていないのであまり関係ない。
  • argsを渡してRailsの雛形ファイル群を生成すると思われる(コードが追えなくなったので確証はない)

 

args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!を追いかける。

#railties/lib/rails/generater/rails/app/app_generator.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators
    # We need to store the RAILS_DEV_PATH in a constant, otherwise the path
    # can change in Ruby 1.8.7 when we FileUtils.cd.
    RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__))
    RESERVED_NAMES = %w[application destroy plugin runner test]

    # ...

    class ARGVScrubber # :nodoc:
      def initialize(argv = ARGV)
        @argv = argv
        # @argv => ['new', 'my_app']
      end

      def prepare!
        # @argv => ['new', 'my_app']
        handle_version_request!(@argv.first)
        handle_invalid_command!(@argv.first, @argv) do
          handle_rails_rc!(@argv.drop(1))
        end
      end

      def self.default_rc_file
        File.expand_path('~/.railsrc')
      end

      private

        def handle_version_request!(argument)
          # argument => 'new'
          if ['--version', '-v'].include?(argument)
            require 'rails/version'
            puts "Rails #{Rails::VERSION::STRING}"
            exit(0)
          end
        end

        def handle_invalid_command!(argument, argv)
          # argument => 'new'
          # argv => ['new', 'my_app']
          if argument == "new"
            yield
          else
            ['--help'] + argv.drop(1)
          end
        end

        def handle_rails_rc!(argv)
          # argv => ['my_app']
          if argv.find { |arg| arg == '--no-rc' }
            argv.reject { |arg| arg == '--no-rc' }
          else
            railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
          end
        end

        def railsrc(argv)
          #argv => ['my_app']
          if (customrc = argv.index{ |x| x.include?("--rc=") })
            fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
            yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
          else
            yield argv, self.class.default_rc_file
          end
        end

        # ...

        def insert_railsrc_into_argv!(argv, railsrc)
          # argv => ['my_app']
          # railsrc => '~/.railsrcの絶対パス'
          return argv unless File.exist?(railsrc)
          extra_args = read_rc_file railsrc
          argv.take(1) + extra_args + argv.drop(1)
        end
    end
  end
end
  • #handle_version_request!ではargument'--version','-v'が含まれていないので関係ない。
  • #handle_invalid_command!ではargument'new'なのでブロックにそのままargument, argvを渡す。
  • #handle_rails_rc!ではargv'--no--rc'が含まれていないので#railsrcを実行する。
  • #railsrcではargvの各要素に'--rc='が含まれていないのでブロックにargv, self.class.default_rc_fileを渡す。
  • #self.default_rc_fileでは~/.railsrc絶対パスを取得する。なんでこのメソッドはprivateメソッドじゃなくて特異メソッドなんだろう。
  • #insert_railsrc_into_argv!では~/.railsrcが存在しないのでargvを返す。

 

Rails::Generators::AppGenerator#startを探す旅が始まる。
Rails::Generators::AppGenerator.start argsを追いかける。

#railties/lib/rails/generators/rails/app/app_generator.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators
    # ...

    class AppGenerator < AppBase # :nodoc:
      # ...

    end
  end
end
  • Rails::Generators::AppGeneratorでは#startは定義されていなかった。
  • AppBaseを継承していた。

 

rails/generators/app_baseをチェック。

#railties/lib/rails/generators/app_base.rb

require 'digest/md5'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
require 'open-uri'
require 'uri'
require 'rails/generators'
require 'active_support/core_ext/array/extract_options'

module Rails
  module Generators
    class AppBase < Base # :nodoc:
      # ...

    end
  end
end
  • Rails::Generators::AppBaseでは#startは定義されていなかった。
  • Baseを継承していた。どこでrequireしてるのか分からなかった。

 

rails/generators/baseをチェック。

#railties/lib/rails/generators/base.rb

begin
  require 'thor/group'
rescue LoadError
  puts "Thor is not available.\nIf you ran this command from a git checkout " \
       "of Rails, please make sure thor is installed,\nand run this command " \
       "as `ruby #{$0} #{(ARGV | ['--dev']).join(" ")}`"
  exit
end

module Rails
  module Generators
    # ...

    class Base < Thor::Group
      # ...

    end
  end
end
  • Rails::Generators::Baseでは#startは定義されていなかった。
  • Thor::Groupを継承していた。外部のgemに行ってしまった。

 

thor/groupをチェック。

#thor/lib/thor/group.rb

require "thor/base"

class Thor::Group
  class << self
    # ...

    include Thor::Base
end
  • Thor::Groupでは#startは定義されていなかった。
  • Thor::Baseをインクルードしていた。

 

thor/baseをチェック。

#thor/lib/thor/base.rb

require "thor/command"
require "thor/core_ext/hash_with_indifferent_access"
require "thor/core_ext/ordered_hash"
require "thor/error"
require "thor/invocation"
require "thor/parser"
require "thor/shell"
require "thor/line_editor"
require "thor/util"

class Thor
  autoload :Actions,    "thor/actions"
  autoload :RakeCompat, "thor/rake_compat"
  autoload :Group,      "thor/group"

 # ...

  module Base
    # ...

    class << self
      def included(base) #:nodoc:
        base.extend ClassMethods
        base.send :include, Invocation
        base.send :include, Shell
      end

      # ...

    end

    module ClassMethods
      # ...

      def start(given_args = ARGV, config = {})
        # given_args => ['my_app']
        # config => {}
        config[:shell] ||= Thor::Base.shell.new
        # config { shell: Thor::Shell::Colorオブジェクト }
        dispatch(nil, given_args.dup, nil, config)
      rescue Thor::Error => e
        config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
        exit(1) if exit_on_failure?
      rescue Errno::EPIPE
        # This happens if a thor command is piped to something like `head`,
        # which closes the pipe when it's done reading. This will also
        # mean that if the pipe is closed, further unnecessary
        # computation will not occur.
        exit(0)
      end

      # ...
    end
  end
  • Thor::Baseがインクルードされたときフックメソッドのincluded()が実行される。included()Thor::Base::ClassMethodsをextendしてThor::Base::ClassMethodsのメソッドがThor::Baseのクラスメソッドとして使えるようにしている。
  • Thor::Base::ClassMethod#startでは設定や開発環境に合わせてshellでの出力を指定してRailsの雛形ファイル群を生成している。

 

config[:shell] ||= Thor::Base.shell.newを追いかける。

#thor/lib/thor/shell.rb

require "rbconfig"

class Thor
  module Base
    class << self
      attr_writer :shell

      # Returns the shell used in all Thor classes. If you are in a Unix platform
      # it will use a colored log, otherwise it will use a basic one without color.
      #
      def shell
        @shell ||= if ENV["THOR_SHELL"] && ENV["THOR_SHELL"].size > 0
          Thor::Shell.const_get(ENV["THOR_SHELL"])
        # macの場合 RbConfig::CONFIG["host_os"] => 'darwin'
        # windows系の場合 RbConfig::CONFIG["host_os"] => 'mswin' or 'mingw'
        elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"]
          Thor::Shell::Basic
        else
          Thor::Shell::Color
        end
      end
    end
  end

  # ...

end
  • 環境変数を設定してなくてPCはmacだとしたとき、Thor::Base#shellThor::Shell::Colorを返す。
  • rails new my_appを実行したときに出力に色がついていたのはこの辺の設定だったのか。

 

dispatch(nil, given_args.dup, nil, config)を追いかける。

#thor/lib/thor/group.rb

require "thor/base"

class Thor::Group # rubocop:disable ClassLength
  class << self
    # ...

  protected

    # The method responsible for dispatching given the args.
    def dispatch(command, given_args, given_opts, config) #:nodoc:
      # command => nil
      # given_args => ['my_app']
      # given_opts => nil
      # config => { shell: Thor::Shell::Colorのインスタンス }

      # Thor::HELP_MAPPINGS => ['-h', '-?', '--help', '-D']
      if Thor::HELP_MAPPINGS.include?(given_args.first)
        help(config[:shell])
        return
      end

      args, opts = Thor::Options.split(given_args)
      # args => ['my_app']
      # opts => []
      opts = given_opts || opts
      # opts => []

      instance = new(args, opts, config)
      yield instance if block_given?

      if command
        instance.invoke_command(all_commands[command])
      else
        instance.invoke_all
      end
    end
  • instance = new(args, opts, config)で突如出現する#newが何をnewしているのか分からなかったけどデバッグするとRails::Generators::AppGeneratorだと判明した。
  • commandnilなので#invoke_allを実行している。

 

instance.invoke_allを追いかける。

#thor/lib/thor/invocation.rb

class Thor
  module Invocation

    # Make initializer aware of invocations and the initialization args.
    def initialize(args = [], options = {}, config = {}, &block) #:nodoc:
      @_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] }
      @_initializer = [args, options, config]
      super
    end

    # ...

    # Invoke the given command if the given args.
    def invoke_command(command, *args) #:nodoc:
      # self.class => Rails::Generators::AppGenerator
      current = @_invocations[self.class]

      unless current.include?(command.name)
        current << command.name
        command.run(self, *args)
      end
    end

    # Invoke all commands for the current instance.
    def invoke_all #:nodoc:
      self.class.all_commands.map { |_, command| invoke_command(command) }
    end

    # ...
  end
end
  • #invoke_allでデフォルトの雛形ファイル群を全部生成している。
  • #invoke_all内のself.class.all_commandsでは以下のようなキーがコマンド名で値がThor::Commandオブジェクトのハッシュを生成している。
{
  "set_default_accessors!"=>
    #<struct Thor::Command 
      name = "set_default_accessors!",
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >, 
  "create_root"=>
    #<struct Thor::Command 
      name = "create_root", 
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >,
  "create_root_files"=>
    #<struct Thor::Command 
      name = "create_root_files", 
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >, ... 
}
  • #invoke_allで処理を個々に切り分け、#invoke_commandThor::Commandオブジェクトを渡してファイルを生成している。
  • #invoke_commandでは実行するコマンドが重複しないようにcurrentで管理している。

 

command.run(self, *args)を追いかける。

#thor/lib/thor/command.rb

class Thor
  class Command < Struct.new(:name, :description, :long_description, :usage, :options)

    def initialize(name, description, long_description, usage, options = nil)
      super(name.to_s, description, long_description, usage, options || {})
    end

    # ...

    # By default, a command invokes a method in the thor class. You can change this
    # implementation to create custom commands.
    def run(instance, args = [])
      # instance => Rails::Generators::AppGeneratorオブジェクト
      arity = nil

      if private_method?(instance)
        instance.class.handle_no_command_error(name)
      elsif public_method?(instance)
        arity = instance.method(name).arity
        instance.__send__(name, *args)
      elsif local_method?(instance, :method_missing)
        instance.__send__(:method_missing, name.to_sym, *args)
      else
        instance.class.handle_no_command_error(name)
      end
    rescue ArgumentError => e
      handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
    rescue NoMethodError => e
      handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (fail e)
    end

    # ...

  protected
    # ...

    # Given a target, checks if this class name is a public method.
    def public_method?(instance) #:nodoc:
      !(instance.public_methods & [name.to_s, name.to_sym]).empty?
    end

    def private_method?(instance)
      !(instance.private_methods & [name.to_s, name.to_sym]).empty?
    end

    def local_method?(instance, name)
      methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false)
      !(methods & [name.to_s, name.to_sym]).empty?
    end
  • #runではRails::Generators::AppGeneratorにコマンド名と同一のpublicメソッドが存在したとき、Rails::Generators::AppGenerator#コマンド名を実行している。
  • Rails::Generators::AppGeneratorにコマンド名と同一のprotectedメソッドが存在したときはNoMethodErrorを返し、同一のprivateメソッドが存在したときと同一のメソッドが存在しなかったときはUndefinedCommandErrorを返す仕様になっている。一体何の差なんだろうか。

 

instance.__send__(name, *args)を追いかける。

#railties/lib/rails/generators/rails/app/app_generators.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators    
    # ...

    class AppGenerator < AppBase # :nodoc:

      public_task :set_default_accessors!
      public_task :create_root

      def create_root_files
        build(:readme)
        build(:rakefile)
        build(:configru)
        build(:gitignore) unless options[:skip_git]
        build(:gemfile)   unless options[:skip_gemfile]
      end

      def create_app_files
        build(:app)
      end

      def create_bin_files
        build(:bin)
      end

      # ... コマンド名と同じメソッドが続く
  • やっとRailsに戻ってきた。
  • それぞれのメソッドで#buildを実行している。
  • 普通のメソッドではなくpublic_taskで定義されているものもあるけど疲れたので放置。

 

build()を追いかける。

#railties/lib/rails/generators/app_base.rb

require 'digest/md5'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
require 'open-uri'
require 'uri'
require 'rails/generators'
require 'active_support/core_ext/array/extract_options'

module Rails
  module Generators
    class AppBase < Base # :nodoc:
      # ...

    protected
      # ...

      def builder
        @builder ||= begin
          builder_class = get_builder_class
          # builder_class => Rails::AppBuilder
          builder_class.send(:include, ActionMethods)
          # self => Rails::Generators::AppGeneratorオブジェクト
          builder_class.new(self)
        end
      end

      def build(meth, *args)
        builder.send(meth, *args) if builder.respond_to?(meth)
      end
  • #builderではRails::ActionMethodsをインクルードしてRails::ActionMethods#newを用いてRails::Generators::AppGeneratorsオブジェクトを引数にRails::AppBuilderを生成している(はず)。
  • #buildではmethと同一の#builderで生成したRails::AppBuilderオブジェクトのインスタンスメソッドを実行している。

 

builder.send(meth, *args)を追いかける。

#railties/lib/rails/generators/rails/app/app_generators.rb

require 'rails/generators/app_base'

module Rails
  # ...

  class AppBuilder
    def rakefile
      template "Rakefile"
    end

    def readme
      copy_file "README.rdoc", "README.rdoc"
    end

    def gemfile
      template "Gemfile"
    end

    def configru
      template "config.ru"
    end

    def gitignore
      template "gitignore", ".gitignore"
    end

    def app
      directory 'app'

      keep_file  'app/assets/images'
      keep_file  'app/mailers'
      keep_file  'app/models'

      keep_file  'app/controllers/concerns'
      keep_file  'app/models/concerns'
    end

    def bin
      directory "bin" do |content|
        "#{shebang}\n" + content
      end
      chmod "bin", 0755 & ~File.umask, verbose: false
    end

    # ... methと同じメソッドが続く
  end
  
  # ...

end
  • #template#directory, #keep_fileThor::Actionのメソッド。
  • 各行で何が生成されてるか分かるようになってきたのでそろそろ終わろう。

 

おわりに

  • RailsRubyそのものについての様々な知見が得られた。
  • Rails開発者に足を向けて寝られなくなった。

 

参考にさせていただきました

`rails s`読んだ - AnyType

【大学編入】編入勉強を始める前にやったこと

僕が編入勉強を始める前にやったことをまとめてみました。
勉強を始めるまでにやったことは大きく分けて4つあります。

step1 どんな大学生活を送りたいかを考える
step2 大学を選ぶ
step3 合格するための情報を集める
step4 勉強計画を立てる

 

step1 どんな大学生活を送りたいかを考える

まずはどんな大学生活を送りたいか考えます。

研究漬けの生活を送りたい、パリピになりたい、など色々あると思います。
この時点で「〇〇大学の△△研究室で研究したい」という具体的な希望がある人は step3.合格するための情報を集める に進みましょう。

 

僕の場合は次のようになりました。

  • webと機械学習の知識を生かせる研究をしたい
  • 総合大学に行きたい
  • 技術系のアルバイトがしたい
  • 大学外での活動も充実させたい

 

step2 大学を選ぶ

step1で考えた内容を元に大学を決めます。

 

僕の場合だと

  • webと機械学習の知識を生かせる研究をしたい
    →webを中心とした自然言語処理の研究室がある大学

  • 総合大学に行きたい
    →総合大学

  • 技術系のアルバイトがしたい
    →そもそも技術系のアルバイトをするためにはIT企業が必要
    →IT企業が多いのは東京
    →東京にアクセスが容易な大学

  • 大学外での活動も充実させたい
    →勉強以外の時間もしっかり取れる
    高専生の編入実績が多く、単位互換がしやすい大学

これらの条件から大学を探した結果、 筑波大学がヒットしました。

 

step3 合格するための情報を集める

step2で決めた大学に合格するために必要な情報を集めます。

必要な情報とは

  • 編入試験の範囲
  • 先人の勉強記録
  • よく使われている参考書

などです。

方法としては

  • 各大学の募集要項をチェックする
  • 編入ブログを読む
  • 高専で行われている編入セミナーに参加する
  • 編入説明会に参加する
  • Studyplusで検索する

など色々あると思います。

 

step4 勉強計画を立てる

必要な情報が集まったら勉強計画を立てます。
ここで重要になってくるのが「この計画を遂行すれば必ず合格する」と思い込めるような計画を立てることです。 必ず合格すると思い込める計画を立てられると本当に勉強が嫌になったときに踏ん張ることができます。

そして合格すると思い込むためにはstep3で情報を大量に集めて自分の中で合格ラインを作ることが大切です。
5人の編入ブログから作った合格ラインと50人の編入ブログから作った合格ラインだと信頼性が全然違うと思いませんか?

ただ注意してほしいのが「勉強計画は何度も立て直すもの」ということです。
勉強を始める前に作った勉強計画は現実的でなかったり効率があまり良くないことが多いです(体験談)。
無理な計画や無駄の多い計画に気づいたら早めに修正してロスを減らしましょう。

個人的には「1日で○時間勉強する」といった目標よりも「1日でこの参考書の問題を○問解く」といった目標の方がダラけずに勉強を続けやすかったです。

 

 

僕はこの方法で勉強計画まで落とし込みましたが方法は色々あると思います。
他の人の記事も参考にしながら自分が納得する方法を見つけてください。

これから編入を考えている方頑張ってください。

 
編入体験記をまとめました。
euglena1215.hatenablog.jp

MacBook ProでCUDAのGPUモードが使えたり使えなかったりする話

MacBook ProでTheanoのGPUモードを使うために設定を済ませてimportするとエラーが発生。
GPUモードが使えない...??

>>> import theano
WARNING (theano.sandbox.cuda): CUDA is installed, but device gpu is not available
  (error: Unable to get the number of gpus available: CUDA driver version is insufficient for CUDA runtime version)

 

自分のmacのグラフィックスを確認すると
NVIDIA製ではないので使えないのは当たり前でした。

  チップセットのモデル: Intel Iris Graphics 6100  
  種類:  GPU  
  バス:  内蔵  
  VRAM(ダイナミック、最大):  1536 MB  
  製造元:  Intel (0x8086)

 

でも調べているとiMacではCUDAのGPUモードが使えている人はいる様子。
同じApple製品で違うメーカーのGPUが載ってるってこと?
なんでだろう。

ket-30.hatenablog.com

 

さらに調べているとこんな記事が

MacBook Pro現行モデルの技術仕様を見ると最上位機種のグラフィックスチップはIntel Iris Pro GraphicsとAMD Radeon R9 M370Xとなっており、NVIDIA GeForceは搭載されていません。

ですが、MacBook Pro (15-inch, Mid 2012) - 技術仕様を見ると、私が持っているMacBook Proは15インチ2.6GHzモデルなのでNVIDIA GeForce GT 650M、1GB GDDR5メモリが搭載されています。

GeForce GT 650M | NVIDIAには「プログラミング環境」の行に「CUDA」とあるのでCUDAが使えるようです。

引用: GeForce搭載の旧モデルMacBook ProでCUDAをセットアップする手順のメモ · hnakamur's blog at github

昔のMacBook ProではGeForceを選択できたみたい。

というわけでGeForceが載っているものと載っていないものを2011~2015年で調べてみました。

GeForceが載っている
  • Retina, 15-inch, Mid 2014
  • Retina, 15-inch, Early 2013
  • 15-inch, Mid 2012
GeForceが載ってない
  • Retina, 15-inch, Mid 2015
  • Retina, 13-inch, Early 2015
  • Retina, 13-inch, Mid 2014
  • Retina, 13-inch, Late 2013
  • Retina, 13-inch, Early 2013
  • Retina, 13-inch, Late 2012
  • 13-inch, Mid 2012
  • 17-inch, Late 2011
  • 13-inch, Late 2011
  • 15-inch, Late 2011
  • 17-inch, Early 2011
  • 15-inch, Early 2011
  • 13-inch, Early 2011

まとめ

MacBook ProでCUDAのGPUモードを使うなら2012~2014年モデルの15インチを買うべし。

 

その他のApple製品の技術仕様はこちらからどうぞ
アップル – サポート - 技術仕様

graphvizを使って木構造を可視化する関数を作ってみた

卒業研究で木構造を可視化させる必要があったので関数を作ってみました。

木構造・ノードクラスの属性は次のようになります。

Tree クラス
属性名 型,クラス 説明
root Node Treeの根ノード
Node クラス
属性名 型,クラス 説明
num int ノード番号
child Node 子ノード
brother Node 兄弟ノード(木構造で考えると右側のノード)
parent Node 親ノード

 

ソースコード

 

現在研究している自己生成ニューラル木に菖蒲の花のデータセットを学習させたものを可視化させるとこのようになりました。 f:id:euglena1215:20160930153903p:plain

関数を少し修正して花の種類で色分けするとこのようになりました。 f:id:euglena1215:20160930155224p:plain

 

木構造が可視化できると色々捗りますよね。

参考にさせていただきました

PythonからGraphvizを使う - Leaaaaaaaaaaaaarning
画像処理についてあれこれ: Graphvizでノードの塗りつぶし色を指定する

Railsでセキュリティのことを考えずに作ったwebアプリは脆弱性があるのか調べてみた(脆弱性検証編)

この続きです。 euglena1215.hatenablog.jp

検証方法が全く網羅的ではないということを踏まえた上でお読みください。

検証

XSS(クロスサイトスクリプティング)

方法

投稿フォームに以下のリンクに載っているXSSの例を片っ端から入力してみた。
XSS Filter Evasion Cheat Sheet - OWASP

結果

脆弱性は見つからなかった。

理由

rails3からデフォルトで文字列を出力するときにはエスケープされるようになっているため基本的にはXSSは通らない。

なので文字列をエスケープさせたくない場合には
<%= 文字列.html_safe %>
<%= raw 文字列 %>
<%== 文字列 %>
のどれかを使う。

qiita.com

この記事によると <%== を使うのがベストらしい。

SQLインジェクション

方法

検索フォームに ');-- と入力する

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%');--%’)
というクエリが生成され、userの全件取得ができてしまう。ということは…

 


 

方法

検索フォームに’ AND password LIKE ‘password’);— と入力する

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%’ AND password LIKE ‘password’);—%')
というクエリが発行されてpasswordが’password’のユーザが取得できる。
という風に利用者が任意のクエリを発行することができる。これは危ない。
※今回使用したDeviseは暗号化したパスワードをDBに保存しているのですぐにパスワードがバレるというわけではありません

普通に利用しているとユーザがアクセスできないデータにアクセスされる可能性がある。

対策

where("username LIKE '%"+username+"%’”)
where("username LIKE ?","%#{username}%") に変更する。

以下のように修正する。

# before
  # app/models/post.rb
  scope :by_body_like, ->(body){
    where("body LIKE '%"+body+"%'")
  }
  # app/models/user.rb
  scope :by_username_like, ->(username){
    where("username LIKE '%"+username+"%'")
  }
# after
  # app/models/post.rb
  scope :by_body_like, ->(body){
    where("body LIKE :body ESCAPE '\\'", { body: "%#{sanitize_sql_like(body)}%"})
  }
  # app/models/user.rb
  scope :by_username_like, ->(username){
    where("username LIKE :username ESCAPE '\\'", { username: "%#{sanitize_sql_like(username)}%"})
  }

検索でLIKE演算子ワイルドカードである_や%が使われてもいいようにエスケープする必要がある。
Rails 4.2.1以降にはsanitize_sql_likeメソッドが実装されているのでそれを使う。
sanitize_sql_like (ActiveRecord::Sanitization::ClassMethods) - APIdock

?を使うとSQLインジェクションを防ぐことができるということは分かったものの、なぜこれでSQLインジェクションが防げるのかよく分からなかった。
なので今度Active recordを読み解いていこうと思う。
Active record whereメソッド :
rails/query_methods.rb at 0399b71dab8b270b4e40b2aff99194a8b8f2596c · rails/rails · GitHub

これはActive recordのwhereメソッドの仕様ではなくSQLプレースホルダという機能だった。
プレースホルダとはパラメータを後で記述できるようにすることによって、SQLの構文を決定した後にバインドできるのでパラメータがリテラルの外にはみ出す現象が起こらなくなりSQLインジェクションが起こらなくなる機能だ。

 
 

普段SQL文を書いていないことが露呈してしまいました...基礎技術を勉強し直そうと思います。
ご指摘ありがとうこざいます!
 

?を使うSQL文は色々なところで見たことはありましたがプレースホルダという名前があったのは知りませんでした。
ご指摘のおかげでgoogle先生に聞くことができました。ありがとうございます!


方法

検索フォームに ‘); DROP “posts”;— '); DROP TABLE “posts”;-- と入力する
SQLインジェクションが通る状態で複文はどうなるのか試してみた。

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%‘); DROP “posts”;--%')
SELECT "users"."id" FROM "users" WHERE (username LIKE '%'); DROP TABLE “posts”;--%') というクエリが発行されているもののpostsテーブルは削除されていなかった。
なので複文はサポートされてないみたい。(今回はSQLite3での実験だったので他のDBでどうなるかは試していません)

まとめ

  • やはり自分の書いたコードに穴があった。
  • XSSSQLインジェクションチートシートの存在を知ったことが一番の収穫だった。
  • マサカリを投げて間違いを指摘してくれる人がいる。アウトプットの重要性を改めて実感した。

 

参考にさせていただきました

SQL Injection Cheat Sheet
Railsで検索をSQLで簡単に実装する方法 - Qiita
Railsのセキュリティ対策で調べた事 - Qiita
XSS Filter Evasion Cheat Sheet - OWASP
html_safe、raw、「<%==」の比較 - Qiita
https://www.ipa.go.jp/files/000017320.pdf
rails - クエリの基本 - そういうことだったんですね

Railsでセキュリティのことを考えずに作ったwebアプリは脆弱性があるのか調べてみた(webアプリ作成編)

こんにちは。僕はこのたびセキュリティキャンプ九州2016に行ってきました。
一言感想を言わせていただくと大変面白かったです。
ただ一つ不安になったことがあります。それはwebアプリの脆弱性です。 キャンプ中にwebアプリの脆弱性を見つけてみようという講座があり、脆弱性があるwebアプリを公開してみんなで脆弱性を探すというものだったのですが、30分もたたないうちに実装されてないはずの画像アップロード機能が追加されたりページを開いた瞬間音声が流れ出す迷惑サイトに変化していたりと蜂の巣状態になっていました。

そこで今回は普段使っているRuby on Railsで作ったwebアプリに脆弱性が存在するのかを確かめてみようと思います。

仕様

セキュリティキャンプ九州で使用した脆弱SNSの機能は次のようなものでした。

  • ログイン機能(登録・編集)
  • ログインすると掲示板への投稿・削除ができる
  • 投稿内容の一覧
  • 投稿とユーザの検索機能

DB

  • 投稿テーブル(内容/投稿者/日時)
  • ユーザテーブル(ユーザ名/パスワード/URL)

これと同じような機能のwebアプリをRailsでできるだけコードを書かずに作ってみたいと思います。
Railsではログイン機能はコマンドだけでは生成できないので超有名なユーザー管理のgemであるDeviseを使いました。

環境

Rails 4.2.4
Devise 4.1.1

ログイン機能

まずはrailsアプリを生成。 以下のコマンドを実行します。

% rails new testApp
      create
      create  README.rdoc
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
      create  app
      create  app/assets/javascripts/application.js
      create  app/assets/stylesheets/application.css
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/views/layouts/application.html.erb
  — 略 —
      create  vendor/assets/stylesheets/.keep
         run  bundle install
Fetching gem metadata from https://rubygems.org/
Fetching version metadata from https://rubygems.org/
Fetching dependency metadata from https://rubygems.org/
Resolving dependencies......
Using rake 11.2.2
Using i18n 0.7.0
Using json 1.8.3
— 略 —

Gemfileに以下を追記。

# Gemfile
gem ‘devise’ 

bundle installを実行。
% bundle install

rails generateでdeviseをインストールします。
これでユーザー管理を行うための準備が完了します。

% bundle exec rails generate devise:install
Running via Spring preloader in process 4640
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:

       config.assets.initialize_on_precompile = false

     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.

  5. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

言われた通りに設定していきます。
development.rbに追記します。

# config/enviroments/development.rb  
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    :address => 'smtp.gmail.com',
    :port => 587,
    :authentication => :plain,
    :user_name => 'メールアドレス',
    :password => 'パスワード'
}

root用のcontrollerとviewをコマンドで生成します。

% bundle exec rails generate controller home index
Running via Spring preloader in process 7935
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb
      invoke  helper
      create    app/helpers/home_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/home.coffee
      invoke    scss
      create      app/assets/stylesheets/home.scss

routes.rbに追記してroot_urlを設定します。

# config/routes.rb
root to: "home#index"

home/index.html.erbにログインと登録用のURLを追加します。

<h1>Home#index</h1>
<% if user_signed_in? %>
  Logged in as <strong><%= current_user.email %></strong>.
  <%= link_to "Settings", edit_user_registration_path, :class => "navbar-link" %> |
  <%= link_to "Logout", destroy_user_session_path, method: :delete, :class => "navbar-link" %>
<% else %>
  <%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |
  <%= link_to "Login", new_user_session_path, :class => 'navbar-link' %>
<% end %>
<p>Find me in app/views/home/index.html.erb</p>

ログイン情報を出力させるためにlayouts/application.html.erbに追加します。

<!DOCTYPE html>
<html>
<head>
  <title>TestApp</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
<% if notice %>
  <p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
  <p class="alert alert-danger"><%= alert %></p>
<% end %>
<%= yield %>

</body>
</html>

次にユーザー用のmodelとmigrationファイルを生成します。

% bundle exec rails generate devise User
Running via Spring preloader in process 4790
      invoke  active_record
      create    db/migrate/(タイムスタンプ)_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

今回はtrackableとvalidatableは使わないので user.rbの一部をコメントアウトします。

# app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registrable,
         :recoverable, :rememberable
        # :trackable,
        # :validatable
end

その修正に合わせて (タイムスタンプ)__devise_create_users.rbの一部もコメントアウトします。

# db/migrate/(タイムスタンプ)__devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

DBを生成します。以下のコマンドを実行してください。

% bundle exec rake db:migrate
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0012s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0007s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0008s
== 20160920061425 DeviseCreateUsers: migrated (0.0029s) =======================

rails s でサーバを起動させ、http://localhost:3000にアクセスするとこうなります。 f:id:euglena1215:20160921160404p:plain

Sign upから登録をすませると f:id:euglena1215:20160921160515p:plain

これでdeviseのデフォルトのログイン機能は完了ですが、現在ではユーザ情報がメールアドレスとパスワードしか存在しません。 なのでユーザ情報にユーザ名を追加し、ユーザ名とパスワードでログインできるようにします。

以下のコマンドを実行します。

% rails generate migration add_username_to_users username:string
Running via Spring preloader in process 9875
      invoke  active_record
      create    db/migrate/(タイムスタンプ)_add_username_to_users.rb

usernameはログインで使われるためuniqueを与えます。 (タイムスタンプ)_add_username_to_users.rbに追記します。

#db/migrate/(タイムスタンプ)_add_username_to_users.rb
class AddUsernameToUsers < ActiveRecord::Migration
  def change
    add_column :users, :username, :string
    add_index :users, :username, unique: true
  end
end

以下のコマンドを実行します。

% bundle exec rake db:migrate
== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0018s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0025s
== 20160920080558 AddUsernameToUsers: migrated (0.0045s) ======================

user.rbにvalidationを追加します。

validates :username, presence: true, uniqueness: true

ログインで使うデータを変更したため、viewをカスタマイズします。

以下のコマンドを実行します。

% rails g devise:views
Running via Spring preloader in process 11412
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

以下のviewの current_user.emailcurrent_user.username に変更します。
app/views/home/index.html.erb

以下のviewの :email:username に変更し、 email_fieldtext_field に変更します。
app/views/devise/sessions/new.html.erb

以下のviewにusername用のフォームを追加します。
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb

先ほど登録したuserはusernameにデータが入っていないので一度DBをリセットします。
以下のコマンドを実行します。

% bundle exec rake db:migrate:reset
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0013s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0007s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0007s
== 20160920061425 DeviseCreateUsers: migrated (0.0029s) =======================

== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0006s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0011s
== 20160920080558 AddUsernameToUsers: migrated (0.0018s) ======================

rails s でサーバを起動し http://localhost:3000/users/sign_upにアクセスすると f:id:euglena1215:20160921160700p:plain

usernameも登録できるようになっていて f:id:euglena1215:20160921160719p:plain

usernameとpasswordでログインできるようになっています。

次にURLカラムを追加します。 以下のコマンドを実行します。

% rails generate migration add_url_to_users url:string
Running via Spring preloader in process 12882
      invoke  active_record
      create    db/migrate/20160920090632_add_url_to_users.rb

先ほどと同様に以下のviewにurl用のフォームを追加します。
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb

先ほど登録したuserはurlを持っていないので一度DBをリセットします。
以下のコマンドを実行します。

% bundle exec rake db:migrate:reset
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0012s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0006s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0007s
== 20160920061425 DeviseCreateUsers: migrated (0.0028s) =======================

== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0006s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0010s
== 20160920080558 AddUsernameToUsers: migrated (0.0017s) ======================

== 20160920090632 AddUrlToUsers: migrating ====================================
-- add_column(:users, :url, :string)
   -> 0.0005s
== 20160920090632 AddUrlToUsers: migrated (0.0006s) ===========================

これでログイン機能はOKです。

投稿機能

以下のコマンドを実行します。

% rails g scaffold post body:text user:references
Running via Spring preloader in process 13853
      invoke  active_record
      create    db/migrate/20160920092635_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      invoke    test_unit
      create      test/controllers/posts_controller_test.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/posts/index.json.jbuilder
      create      app/views/posts/show.json.jbuilder
      create      app/views/posts/_post.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/posts.coffee
      invoke    scss
      create      app/assets/stylesheets/posts.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

ユーザと投稿は1対多の関係なのでrelationをuser.rbに追記します。
また、ユーザが削除されたときにそのユーザの投稿も削除してほしいのでdependent属性も設定します。

# app/models/user.rb
has_many :posts, dependent: :destroy

以下のコマンドを実行します。

% bundle exec rake db:migrate
== 20160920092635 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0017s
== 20160920092635 CreatePosts: migrated (0.0017s) =============================

投稿と投稿者が結びつくようにフォームを修正します。

# app/views/posts/_form.html.erb
<div class="field">
  <%= f.label :body %><br>
  <%= f.text_area :body %>
  <%= f.hidden_field :user_id, value: current_user.id %>
</div>

まだ色々な微調整は済んでいませんが、基本的な投稿機能は完成です。 (書くのが面倒くさくなりました。誠に申し訳ございません。一番下にソースのリンクを貼っておくので参考にしてください)

検索機能

次に検索機能を実装します。 今後も使っていくようなアプリであれば検索機能用のgemであるransackを使うのですが、今回は簡単な検索のみなのでgemを使わずに実装しようと思います。

まずはscopeを実装します。post.rbとuser.rbに追記してください。

# app/models/post.rb
scope :by_body_like, ->(body){
  where("body LIKE '%"+body+"%'")
}

scope :by_users_id, ->(users_id){
  where(user_id: users_id)
}
# app/models/user.rb
scope :by_username_like, ->(username){
  where("username LIKE '%"+username+"%'")
}

これで Post.by_body_like(‘hoge’) で投稿内容に’home'が含まれる投稿を取得することができます。
また、 Post.by_users_id(User.by_username_like(‘foo’).pluck[:id]) でユーザー名に’foo’が含まれるユーザーの投稿を取得することができます。
注意!! この記述には脆弱性が存在します。詳しくは脆弱性検証編を書いてあります。

次は検索機能で用いるラジオボタン用の定数を設定します。 post.rbに追記します。

# app/models/post.rb
POST_SEARCH = 1
USER_SEARCH = 2

この定数は Post::POST_SEARCH と記述すると使えます。

次にcontrollerを記述します。
今回の検索機能はsearch_typeで検索する対象(postのbody,userのusername)を決定し、searchが検索ワードという実装にしました。
posts_controller.rbのindexメソッドを次のように変更します。

# app/controllers/posts_controller.rb
def index
  if params[:search_type] == Post::POST_SEARCH.to_s
    @posts = Post.by_body_like(params[:search])
  elsif params[:search_type] == Post::USER_SEARCH.to_s
    users_id = User.by_username_like(params[:search]).pluck(:id)
    @posts = Post.by_users_id(users_id)
  else
    @posts = Post.all
  end
end

Post::POST_SEARCH.to_sとなっているのはGETメソッドから得られるパラメータは数値も文字列に変換されているためです。

次にviewを記述します。
posts/index.html.erbに追記します。

# app/views/posts/index.html.erb
<%= form_tag({controller: '/posts',action: 'index'}, method: 'get', class: 'search_form', style: 'padding: 20px;') do %>
  <div class="field">
    <%= radio_button_tag :search_type, Post::POST_SEARCH %>
    <%= label_tag :post %>
    <%= radio_button_tag :search_type, Post::USER_SEARCH %>
    <%= label_tag :user %>
  </div>
  <div class="field">
    <%= text_field_tag :search,'' %>
  </div>
  <div class="actions">
    <%= submit_tag '検索' %>
  </div>
<% end %>

これで検索機能は完成です。

ここから

  • 新規投稿機能をトップページに持ってくる
  • bootstrapをCDNで読み込ませる
  • UIの調整
  • 使ってないメソッド、ルーティング、ビューを削除
  • タイムゾーンを東京に変更

などなどを書き換えると脆弱かもしれないSNSの完成です。 f:id:euglena1215:20160921162147p:plain

次は脆弱性検証編です。 euglena1215.hatenablog.jp

ソースはこちら GitHub - euglena1215/testApp

【大学編入】専門科目の参考書と対策

参考書

プログラミング

C言語によるアルゴリズムとデータ構造

新・明解C言語によるアルゴリズムとデータ構造

新・明解C言語によるアルゴリズムとデータ構造

色々な編入体験記に載っているのアルゴリズムの本です。 基礎固めに役立ちました。
1周

プログラミングコンテストチャレンジブック

通称アリ本です。「C言語によるアルゴリズムとデータ構造」にはグラフ理論や有名なアルゴリズム(エラトステネスの篩、ダイクストラなど)が載ってなかったのでそれらを実装方法を主眼において読みました。ソースはC++ですがCが分かれば読めると思います。

アルゴリズムとデータ構造(青)

アルゴリズムとデータ構造<改訂 C言語版> (電気工学入門シリーズ)

アルゴリズムとデータ構造<改訂 C言語版> (電気工学入門シリーズ)

C言語によるアルゴリズムとデータ構造」にはグラフ理論や有名なアルゴリズム(エラトステネスの篩、ダイクストラなど)が載ってなかったのでそれらを理論的なところを主眼において読みました。この本とアリ本を行ったり来たりしながら勉強すると捗ると思います。 先生に「C言語によるアルゴリズムとデータ構造のワンランク上でダイクストラとか載ってる本貸してください」と言ったらこの本を貸してくれました。

アルゴリズム図鑑

アルゴリズム図鑑

アルゴリズム図鑑

  • Moriteru Ishida
  • 教育
  • 無料
基本的な探索、ソートの名前を聞いたらすぐに挙動をイメージできるように暇なときはこのアプリを使って確認してました。アニメーションを使って分かりやすく説明してくれてるのでとてもイメージしやすかったです。 300円課金すると全てのアルゴリズムが解放されるのですが課金する価値は十分にありました。

論理回路

論理回路入門

論理回路入門 第2版

論理回路入門 第2版

ざーっと一通り読んでから練習問題を解きました。 意外と時間がかかるので注意してください

情報理論

情報理論

情報理論 (電気・電子系教科書シリーズ)

情報理論 (電気・電子系教科書シリーズ)

この本は授業で使ってたのでそのまま使いました。

対策

対策を始める前の僕は「クイックソート、なんか早そう」「グラフ理論なにそれおいしいの?」「計算量ってなんですか」「篩が読めねえ」というような状況で初めて筑波の過去問を解いたときは解き終わるのに2時間半かかり(筑波は数学と専門合わせて2時間)もう死んだと思ってました。
どうせ他の人も同じようなもんだろと思い編入体験記を漁ると「筑波の専門は簡単」「競プロやってたらいける」「直前の1週間に過去問解きまくったらいけた」という記事ばかりで絶望しかありませんでした。
なので他の人より多めに勉強しました。
論理回路情報理論は勉強すれば点が取れると思います。

 

対策の方針はこんな感じでした。
プログラミング
参考書をばーっと読む+写経する
過去問解く
解けない&時間がかかり過ぎるところを中心に参考書を読む
もう一度過去問を解く
を繰り返す
暇なときはアルゴリズム図鑑で復習する

論理回路
論理回路をばーっと目を通す
演習問題を解く
過去問を解く
もう一度過去問を解く
を繰り返す

情報理論
情報理論にバーっと目を通す
過去問を解く
もう一度過去問を解く
を繰り返す

 

僕の勉強のおおまかな流れは
プログラミング
3月下旬~4月上旬
アルゴリズムとデータ構造 読む 1周
4月上旬~試験日
過去問を繰り返し解く、アリ本、アルゴリズムを必要に応じて読む

論理回路
4月中旬~5月中旬
毎週金曜 3時間前後
5月中旬~試験日
過去問を解く

情報理論
6月~試験日
気が向いたときに過去問を解く
分からなかったら情報理論を読む

下の記録を見てもらえれば詳細の勉強記録が載っています。
http://studyplus.jp/users/teppest

 
編入体験記をまとめました。
euglena1215.hatenablog.jp