<repo_name>mitya/rabotnegi-padrino<file_sep>/app/controllers/vacancies.rb
Rabotnegi.controller :vacancies do
  # caches_page :show, :if => -> c { c.request.xhr? }

  get :new do
    @vacancy = Vacancy.new
    render "vacancies/form"
  end

  get :search, "/vacancies" do
    params.merge! city: current_user.city, industry: current_user.industry
    render "vacancies/index"
  end

  get :index_for_json, "/vacancies.json" do
    @vacancies = Vacancy.search(params.slice(:city, :industry, :q)).without(:description).
      order_by(decode_order_to_mongo(params[:sort].presence || "title")).paginate(params[:page], 50)
    render items: @vacancies.map { |v| v.attributes.slice(*%w(title city industry external_id salary_min salary_max employer_name)) }
  end

  get :index, map: "/vacancies/:city(/:industry)", if_params: {city: City, industry: Industry} do
    @vacancies = Vacancy.search(params.slice(:city, :industry, :q)).without(:description).
      order_by(decode_order_to_mongo(params[:sort].presence || "title")).paginate(params[:page], 50)
    current_user.update_if_stored!(city: params[:city], industry: params[:industry])
    render "vacancies/index"
  end
  
  get :favorite do
    @vacancies = current_user.favorite_vacancy_objects
    render "vacancies/favorite"
  end

  get :show, map: "/vacancies/:id", xhr: true do
    @vacancy = Vacancy.get(params[:id])
    partial "vacancies/details"
  end
  
  get :show, map: "/vacancies/:id" do
    @vacancy = Vacancy.get(params[:id])
  
    return params[:id] =~ /^\d{6}$/ ? 410 : 404, render("shared/404") unless @vacancy
    redirect url(:vacancies, :show, id: @vacancy) if CGI.unescape(params[:id]) != @vacancy.to_param
    
    render "vacancies/show"
  end

  post :create do
    @vacancy = Vacancy.new(params[:vacancy])
    @vacancy.poster_ip = request.ip

    halt 422, render("vacancies/form") unless captcha_valid!(@vacancy)

    if @vacancy.save
      flash[:notice] = 'Вакансия опубликована'
      redirect url(:vacancies, :show, id: @vacancy)
    else
      return 422, render("vacancies/form")
    end    
  end

  post :remember, with: :id do
    current_user.favorite_vacancies << BSON::ObjectId(params[:id])
    current_user.save!
    200
  end

  post :forget, with: :id do
    current_user.favorite_vacancies.delete BSON::ObjectId(params[:id])
    current_user.save!
    200
  end
end
<file_sep>/app/models/rabotaru/processor.rb
class Rabotaru::Processor
  attr_accessor :vacancies, :work_dir

  include Gore::EventLog::Accessor
  include Gore::EventHandling
  
  def initialize(key = Gore.date_stamp)
    @vacancies = []
    @work_dir = Rabotnegi.config.rabotaru_dir.join(key)
    @converter = Rabotaru::Converter.new
  end

  def process
    log.info "start"

    read unless vacancies.any?
    remove_duplicates
    filter
    save

    log.info "done"
  end

  def read
    Pathname.glob(@work_dir.join "*.json").each do |file|
      data = file.read.sub!(/;\s*$/, '')
      items = Gore.json.decode(data)
      vacancies = items.map { |item| convert(item) }.compact
      @vacancies.concat(vacancies)
    end
    log.info "read", count: @vacancies.size
  end

  def remove_duplicates
    @vacancies.uniq_by! { |v| v.external_id }
    log.info "remove_duplicates", keep: @vacancies.size
  end
  
  def filter
    new_vacancies, updated_vacancies = [], []
    loaded_external_ids = @vacancies.pluck(:external_id)
    existed_vacancies = Vacancy.where(:external_id.in => loaded_external_ids)
    existed_vacancies_map = existed_vacancies.index_by(&:external_id)

    @vacancies.each do |loaded_vacancy|
      if existed_vacancy = existed_vacancies_map[loaded_vacancy.external_id]
        if existed_vacancy.created_at != loaded_vacancy.created_at
          existed_vacancy.attributes = loaded_vacancy.attributes.except('_id')
          updated_vacancies << existed_vacancy
        end        
      else
        new_vacancies << loaded_vacancy
      end
    end

    now = Time.now
    new_vacancies.each { |v| v.loaded_at = now }

    @vacancies = new_vacancies + updated_vacancies

    raise_event :filtered do
      detalization = @vacancies.each_with_object({}) { |v, memo|
        key = "#{v.city}-#{v.industry}" 
        memo[key] = memo[key].to_i + 1
      }
    
      { filter: {created: new_vacancies.count, updated: updated_vacancies.count, total: @vacancies.count}, details: detalization }
    end
  end

  def save
    @vacancies.send_each(:save!)
  end
  
  def convert(item)
    @converter.convert(item)
  rescue => e
    log.warn "convert.fail", reason: Gore.format_error(e), item: item['position']
    nil
  end  
end
<file_sep>/test/rabotnegi/unit/industry_test.rb
require 'test_helper'

unit_test Industry do
  test "search by one of external ids" do 
    assert_equal :finance, Industry.find_by_external_ids(2001, 2003, 2, 9, 2003)
  end
end<file_sep>/script/test_json.rb
N = 100

# data = Vacancy.first.attributes
data = Vacancy.limit(50).map { |v| v.attributes.slice(:title, :city, :industry, :external_id, :salary_min, :salary_max, :employer_name) }

# puts data.as_json
# puts JSON.generate(data.as_json)
puts JSON.generate(data)  

Benchmark.bm(40) do |b|
  b.report("as_json") do
    N.times { data.as_json }
  end

  b.report("to_json (ActiveSupport::JSON.encode)") do
    N.times { data.to_json }
  end

  b.report("JSON.generate(x.as_json)") do
    N.times { JSON.generate(data.as_json) }
  end

  b.report("JSON.generate") do
    N.times { JSON.generate(data) }
  end
end
<file_sep>/test/lib/core_ext_test.rb
require "test_helper"

unit_test Object do
  dummy = temp_class do
    attr_accessor :fname, :lname
  end
  
  test "class" do
    assert_equal 123.class, 123._class
  end
  
  test "is?" do
    assert 132.is?(String, NilClass, Fixnum)
    assert !132.is?(String, NilClass, Symbol)
  end
  
  test "send_chain" do
    assert_equal "h", "hello".send_chain("first")
    assert_equal 104, "hello".send_chain("first.ord")
    assert_equal "104", "hello".send_chain("first.ord.to_s")
  end
  
  test "assign_attributes" do
    person = dummy.new
    person.assign_attributes(fname: "Joe", lname: "Smith", age: 25)
    assert_equal "Joe", person.fname
    assert_equal "Smith", person.lname
    assert !person.respond_to?(:age)
  end

  test "assign_attributes!" do
    assert_raises NoMethodError do
      dummy.new.assign_attributes!(fname: "Joe", lname: "Smith", age: 25)
    end
  end
end

unit_test File do
  test "write" do
    assert_respond_to File, :write
  end
end

unit_test Hash do
  test "append_string" do
    hash = {aa: "AA"}

    hash.append_string(:aa, "and AA")
    assert_equal "AA and AA", hash[:aa]
    
    hash.append_string(:bb, "and BB")
    assert_equal "and BB", hash[:bb]
  end

  test "prepend_string" do
    hash = {aa: "AA"}

    hash.prepend_string(:aa, "and AA")
    assert_equal "and AA AA", hash[:aa]
    
    hash.prepend_string(:bb, "and BB")
    assert_equal "and BB", hash[:bb]  
  end
end

unit_test "JSON conversions" do
  test "Time" do
    assert_equal "\"2012-01-01T12:00:00Z\"", "2012-01-01T12:00:00Z".to_time.to_json
    assert_equal "2012-01-01T12:00:00Z", "2012-01-01T12:00:00Z".to_time.as_json
  end
  
  test "BSON::ObjectId" do
    id = BSON::ObjectId.new
    assert id.to_json != id.as_json
  end
end

unit_test Module do
  class TProcess
    attr_accessor :status
    def_state_predicates 'status', :started, :failed, :done
  end    

  test "def_state_predicates" do    
    assert_equal :status, TProcess._state_attr
    assert_equal [:started, :failed, :done], TProcess._states
    
    process = TProcess.new
    assert_respond_to process, :started?
    assert_respond_to process, :failed?
    assert_respond_to process, :done?
    
    process.status = "failed"
    assert process.failed?
    assert !process.done?

    process.status = :done
    assert !process.failed?
    assert process.done?
  end
end

unit_test Enumerable do
  test "select_xx/detect_xx" do
    ary = %w(1 2 3 4 5 6 7)
    assert_equal %w(3), ary.select_eq(to_i: 3)
    assert_equal %w(1 2 4 5 6 7), ary.select_neq(to_i: 3)
    assert_equal %w(3 4 5), ary.select_in(to_i: [3, 4, 5])
    assert_equal %w(1 2 6 7), ary.select_nin(to_i: [3, 4, 5])

    assert_equal "3", ary.detect_eq(to_i: 3)
    assert_equal "1", ary.detect_neq(to_i: 3)

    assert_equal %w(3), ary.select_eq(to_i: 3, to_s: "3")
    assert_equal "3", ary.detect_eq(to_i: 3, to_s: "3")
  end
  
  test "map_to_array" do
    assert_equal [ [1, 1], [22, 2] ], %w(1 22).map_to_array(:to_i, :length)
  end
  
  test "pluck_all" do
    assert_equal [ %w(1 2), %w(3 4) ], [ [1,2], [3,4] ].pluck_all(:to_s)
  end
end
<file_sep>/lib/gore.rb
require 'net/http'

module Gore
  def logger
    Padrino.logger
  end
  
  def http
    @http ||= HttpClient.new
  end
  
  def json
    @json ||= Json.new
  end

  def env
    ActiveSupport::StringInquirer.new(Padrino.env.to_s)
  end
  
  def root
    Pathname(Padrino.root)
  end

  module RandomHelpers
    def id(array)
      results = array ? array.map(&:id) : []
      results = results.map(&:to_s) if results.first.is_a?(BSON::ObjectId)
      results
    end
    
    # f("fixed1", "fixed2", conditional1: true, conditional2: false) => "fixed1 fixed2 conditional1"
    # f(wide: true, narrow: false, thin: true) => "wide thin"
    def css_classes_for(*args)
      return nil if args.empty?

      conditions = args.extract_options!
      classes = args.dup
      conditions.each { |clas, condition| classes << clas if condition }
      classes.join(" ")
    end
    
    def inspection(title, *args)
      options = args.extract_options!
      options.reject! { |k,v| v.blank? }
      options = options.map { |k,v| "#{k}=#{inspect_value(v)}" }.join(',')
      args.map! { |arg| Array === arg ? arg.join('-') : arg }
      args = args.join(',')
      string = [args, options].reject(&:blank?).join(' ')
      "{#{string}}"
    end
    
    def inspect_value(value)
      case value
        when Proc then "Proc"
        when Hash then inspect_hash(value)
        when Array then inspect_array(value)
        when nil then "∅"
        when Time then value.xmlschema
        else value
      end
    end
    
    def inspect_hash(hash)
      (hash || {}).map { |key, val| "#{key}: #{inspect_value(val)}" }.join(", ")
    end
    
    def inspect_array(array)
      (array || []).join(", ")
    end
    
    def reformat_caller(string)
      file, line, method = string.match(/(\w+\.rb):(\d+):in `(\w+)'/).try(:captures)
      "#{file}::#{method}::#{line}"
    end    
    
    def object_id?(value)
      value.to_s =~ /^(\d+|[0-9a-f]{24})(-|$)/i
    end
  end
  
  module Jobs
    def enqueue(klass, method, *args)
      Log.debug "Job scheduled #{klass}.#{method}#{args.inspect}"
      Resque::Job.create('main', 'Gore::Jobs::Worker', klass.to_s, method.to_s, args)
    end

    class Worker
      def self.perform(klass, method, args)
        Log.debug "Job started #{klass}.#{method}#{args.inspect}"
        klass.constantize.send(method, *args)
        Log.debug "Job completed #{klass}.#{method}#{args.inspect}"
      end
    end
  end
  
  module Strings
    def time_stamp
      time = Time.current
      time.strftime("%Y%m%d_%H%M%S_#{time.usec}")
    end
    
    def date_stamp
      time = Time.current
      time.strftime("%Y%m%d")
    end
  
    def format_error(exception)
      "#{exception.class} - #{exception.message}"
    end

    def format_error_and_stack(exception)
        # logger.error Gore.backtrace_cleaner.clean(exception.backtrace).join("\n")
      format_error(exception) + "\n#{exception.backtrace.join("\n")}"
    end    
  end
    
  module Server
    def disk_usage(mount_point = '/')
      output = `df #{mount_point}`
      lines = output.split("\n")
      value = lines.second.split.at(-2).to_i
      value
    rescue
      nil
    end
  
    def memory_free
      output = `free -m`
      lines = output.split("\n")
      value = lines.second.split.at(3).to_i
      value
    rescue 
      nil
    end    
  end

  # class Logger
  #   undef :silence
  #   LOG_METHODS = %w(debug info warn error fatal unknown).pluck(:to_sym)
  #   ALL_METHODS = LOG_METHODS + %w(silence level level=).pluck(:to_sym)
  #   
  #   def method_missing(selector, *args, &block)
  #     if ALL_METHODS.include?(selector)
  #       Kernel.puts(args.first) if LOG_METHODS.include?(selector) && $log_to_stdout
  #       Padrino.logger.send(selector, *args, &block)
  #     else
  #       super
  #     end      
  #   end
  #   
  #   def trace(*args)
  #     debug(*args)
  #   end
  # end

  class HttpClient
    def get(url)
      Log.trace "HTTP GET #{url}"
      Net::HTTP.get(URI.parse(url))      
    end
  end
  
  class Json
    def decode(data)
      ActiveSupport::JSON.decode(data)
    end    
  end

  module Debug
    def self.say(message)
      Padrino.logger.info(message)
    end
  end
  
  class Mash < Hash
    def [](key)
      super(key.to_s) || super(key.to_sym)
    end
    
    def slice(*keys)
      hash = self.class.new
      keys.each { |k| hash[String === k ? k.to_sym : k] = self[k] }
      hash
    end
  end

  extend self, Server, Strings, Jobs, RandomHelpers
  
  def self.method_missing(selector, *args, &block)
    return const_get(selector) if selector.to_s =~ /^[A-Z]/ && const_defined?(selector)
    super
  end
end

Padrino::Logger::Extensions.module_eval do
  def trace(message)
    debug(message)
  end
end

Log = Gore.logger
Http = Gore.http
<file_sep>/app/models/salary.rb
class Salary
	attr_accessor :min, :max, :currency

	def initialize(min = nil, max = nil)
		@currency = :rub
		@min = min
		@max = max
	end
	
	def exact?; @min && @max && @min == @max end
	def exact; @min end
	def exact=(value); @min = value; @max = value end
	def negotiable?; !@min && !@max end
	def negotiable=(value); @min = @max = nil if value end
	def min?; @min && !@max end
	def max?; @max && !@min end
	def range?; @max && @min && @max != @min end

	def ==(other)
	  Salary === other &&
		self.min == other.min && 
		self.max == other.max && 
		self.currency == other.currency
	end 
	
  alias eql? ==

	def to_s
	  case
      when negotiable? then "договорная"
      when exact? then "#{exact.to_i} р."
      when min? then "от #{min.to_i} р."
      when max? then "до #{max.to_i} р."
      when range? then "#{min.to_i}—#{max.to_i} р."
    end
	end

	def text
	  case
      when negotiable? then ""
      when exact? then "#{exact}"
      when min? then "#{min}+"
      when max? then "<#{max}"
      when range? then "#{min}-#{max}"
    end
	end
	
	def text=(value)
	  other = self.class.parse(value)
	  self.min = self.max = nil
	  self.min = other.min
	  self.max = other.max
	end
	
	def convert_currency(new_currency)
		@min = Currency.convert(@min, currency, new_currency) if @min
		@max = Currency.convert(@max, currency, new_currency) if @max
		@currency = new_currency
		self
	end

  def self.make(attributes = {})
    salary = new
    attributes.each { |n,v| salary.send("#{n}=", v) }
    salary
	end
	    
	def self.parse(string)
		string.squish!
		params = case string
  		when /(\d+)\s?[-—]\s?(\d+)/, /от (\d+) до (\d+)/ then { :min => $1.to_i, :max => $2.to_i }
  		when /от (\d+)/, /(\d+)\+/ then { :min => $1.to_i }
  		when /до (\d+)/, /<(\d+)/ then { :max => $1.to_i }
  		when /(\d+)/ then { :exact => $1.to_i }
  		else {}
	  end
		make(params)
	end
end
<file_sep>/test/rabotnegi/unit/rabotaru_processor_test.rb
require 'test_helper'

unit_test Rabotaru::Processor do
  setup do    
    @processor = Rabotaru::Processor.new("demo")  
    Dir.mkdir(@processor.work_dir) unless File.exists?(@processor.work_dir)
  end
  
  test "load" do
    FileUtils.cp Dir.glob(Gore.root.join "test/support/rabotaru/*"), @processor.work_dir
    @processor.read
    assert_equal 60, @processor.vacancies.count
    assert_instance_of Vacancy, @processor.vacancies.first
  end
  
  test "remove_dups" do
    @processor.vacancies = [
      Vacancy.new(external_id: 101),
      Vacancy.new(external_id: 102),
      Vacancy.new(external_id: 103),
      Vacancy.new(external_id: 101),
      Vacancy.new(external_id: 102),
      Vacancy.new(external_id: 104)
    ]

    @processor.remove_duplicates
    
    assert_equal 4, @processor.vacancies.count
    assert_equal [101, 102, 103, 104], @processor.vacancies.pluck(:external_id).sort
  end
  
  test "filter" do
    Vacancy.create!(title: "V-1", city: "spb", industry: "it", external_id: 101, created_at: "2008-09-01", description: "no", loaded_at: "2008-09-01")
    Vacancy.create!(title: "V-2", city: "spb", industry: "it", external_id: 102, created_at: "2008-09-01", description: "no", loaded_at: "2008-09-01")
        
    @processor.vacancies = [
      Vacancy.new(title: 'V-1', city: 'spb', created_at: "2008-09-02", external_id: 101, industry: 'it', description: 'updated'),
      Vacancy.new(title: 'V-2', city: 'spb', created_at: "2008-09-01", external_id: 102, industry: 'it', description: 'no'),
      Vacancy.new(title: 'V-3', city: 'spb', created_at: "2008-09-01", external_id: 103, industry: 'it', description: 'no')
    ]

    @processor.filter
    
    assert @processor.vacancies.any? { |v| v.title == "V-3" }
    assert @processor.vacancies.any? { |v| v.title == "V-1" && v.description == 'updated' }
    assert !@processor.vacancies.any? { |v| v.title == "V-2" }
    
    assert_in_delta Time.now, @processor.vacancies.detect_eq(title: 'V-3').loaded_at, 3.seconds
    assert_equal Time.parse("2008-09-01"), @processor.vacancies.detect_eq(title: 'V-1').loaded_at
  end
  
  test "save" do
    @processor.vacancies = [
      Vacancy.new(title: 'V-5', city: 'spb', external_id: 101, industry: 'it'),
      Vacancy.new(title: 'V-6', city: 'spb', external_id: 102, industry: 'it')
    ]

    @processor.save
    
    assert Vacancy.where(title: 'V-5').exists?
    assert Vacancy.where(title: 'V-6').exists?
  end
end
<file_sep>/test/support/factories.rb
Factory.define :vacancy do |f|
  f.sequence :title do |n| "Test Vacancy ##{n}" end
  f.description "lorem lorem lorem"
  f.city "msk"
  f.industry "it"
  f.salary_min 50_000
  f.employer_name "<NAME>"
  f.sequence :external_id do |n| n + 100_000 end
end
<file_sep>/lib/gore/stubbing.rb
module Gore::Stubbing
  @@stubs = []
  
  def self.stub(target, method, &block)
    @@stubs << [target, method]
    target.singleton_class.instance_eval do
      alias_method "original_#{method}", method
      define_method(method, &block)
    end
  end
  
  def self.unstub_all
    @@stubs.each do |target, method|
      target.singleton_class.instance_eval do
        alias_method method, "original_#{method}"
      end      
    end
  end
end
<file_sep>/app/models/currency.rb
class Currency
  @@map = {
  	:usd => "$",
  	:eur => "€",
  	:rub => "руб."
  }

  @@map = {
    usd: "$", 
    eur: "€", 
    rub: "руб."
  }

  @@list = [:rub, :usd, :eur]
  @@rates = { rub: 1.0, usd: 31, eur: 42	}  
  
  class << self
  	def convert(value, source_currency_sym, target_currency_sym)
  		value * rate(source_currency_sym) / rate(target_currency_sym)
  	end
	
  	def rate(currency)
  	  @@rates[currency]
  	end
  end
end
<file_sep>/config.ru
#!/usr/bin/env rackup

require File.expand_path("../config/boot.rb", __FILE__)

Rack::Server.middleware["development"] = [
  [Rack::ContentLength],
  [Rack::Chunked],
  [Rack::ShowExceptions],
  [Rack::Lint]
]

map '/rabotnegi/assets' do
  use Rack::CommonLogger
  run Rabotnegi.assets
end

require 'resque/server'
map "/admin/resque" do
  run Resque::Server.new
end

run Padrino.application
<file_sep>/test/rabotnegi/ui/home_test.rb
require 'test_helper'

ui_test "Home" do
  test "home page" do
    visit "/"

    within '#header' do
      assert_has_link "Поиск вакансий"
      assert_has_link "Публикация вакансий"
    end  
  end  
end
<file_sep>/test/rabotnegi/unit/rabotaru_converter_test.rb
require 'test_helper'

unit_test Rabotaru::Converter do
  DATA = ActiveSupport::JSON.decode('{
      "publishDate": "Fri, 19 Sep 2008 20:07:18 +0400",
      "expireDate": "Fri, 26 Sep 2008 20:07:18 +0400",
      "position": "Менеджер",
      "link": "http://www.rabota.ru/vacancy27047845.html",
      "description": "blah-blah-blah-blah-blah", 
      "rubric_0": {"id": "19", "value": "IT, компьютеры, работа в интернете"}, 
      "rubric_1": {"id": "14", "value": "Секретариат, делопроизводство, АХО"}, 
      "rubric_2": {"id": "12", "value": "Кадровые службы, HR"}, 
      "city": {"id": "2", "value": "Санкт-Петербург"}, 
      "schedule": {"id": "1", "value": "полный рабочий день"}, 
      "education": {"id": "3", "value": "неполное высшее"}, 
      "experience": {"id": "2", "value": "до 2 лет"}, 
      "employer": {"id": "15440", "value": "Apple", "link": "http://www.rabota.ru/agency15440.html"}, 
      "salary": {"min": "20000", "max": "30000", "currency": {"value": "руб", "id": "2"}}, 
      "responsibility": {"value": "blah-blah-blah"}
    }')  
  
  setup do
    @expected_vacancy = Vacancy.new(
      title: 'Менеджер',
      city: 'spb',
      industry: 'it',
      external_id: 27047845,
      employer_name: 'Apple',
      description: 'blah-blah-blah',
      created_at: Time.parse('Fri, 19 Sep 2008 20:07:18 +0400'),
      salary: Salary.make(min: 20000.0, max: 30000.0, currency: :rub)
    )

    @converter = Gore::PureDelegator.new(Rabotaru::Converter.new)
  end

  test "convert vacancy" do
    result = @converter.convert(DATA)
    assert_equal @expected_vacancy.attributes.except('_id'), result.attributes.except('_id')
  end

  test "extract ID from URL" do
    assert_equal 1234567, @converter.extract_id('http://www.rabota.ru/vacancy1234567.html')
  end

  test "convert salaru" do
    assert_equal Salary.make(:negotiable => true), @converter.convert_salary('agreed' => 'yes')
    assert_equal Salary.make(:max => 25000, :currency => :rub), @converter.convert_salary('max' => '25000', 'currency' => {'value' => 'руб'})
    assert_equal Salary.make(:exact => 25000, :currency => :rub), @converter.convert_salary('min' => '25000', 'max' => '25000', 'currency' => {'value' => 'руб'})
    assert_equal Salary.make(:min => 17000, :max => 34000, :currency => :rub), @converter.convert_salary('min' => '17000', 'max' => '34000', 'currency' => {'value' => 'руб'})
  end

  test "convert currency" do
    assert_equal :rub, @converter.convert_currency('руб')
    assert_equal :usd, @converter.convert_currency('usd')
    assert_equal :eur, @converter.convert_currency('eur')  
    assert_equal :rub, @converter.convert_currency('gbp')
  end
end
<file_sep>/app/models/reflections.rb
Gore::MongoReflector.define_metadata do
  # desc Vacancy do
  #   list :industry, :city, [:title, :link, trim: 50], [:employer_name, trim: 40], :created_at, :updated_at, :loaded_at
  #   list_order [:updated_at, :desc]
  #   view :id, :title, :city, :industry, :external_id, :employer_name, :created_at, :updated_at, :loaded_at, :cleaned_at, :salary, :description, :poster_ip
  #   edit title: 'text',
  #     city_name: ['select', options: :city_options], industry_name: ['select', grouped_options: :industry_options],
  #     external_id: 'text',
  #     employer_id: 'text', employer_name: 'text',
  #     created_at: 'date_time', updated_at: 'date_time',
  #     description: 'text_area'
  # end

  desc User do
    list :industry, :city, [:ip, :link], [:agent, trim: 100], :created_at
  end

  # desc Gore::Err, 'errs' do
  #   list created_at: _, source: :link,
  #     url: [trim: 60],
  #     exception: [ ->(err) { "#{err.exception_class}: #{err.exception_message}" }, trim: 100 ]
  #   view id: _, created_at: _, host: _, source: _,
  #     url: ->(err) { "#{err.verb} #{err.url}" },
  #     exception_class: _,
  #     exception_message: _,
  #     params: 'hash_view',
  #     session: 'hash_view', request_headers: 'hash_view', response_headers: 'hash_view', backtrace: :pre
  #   actions update: false, delete: false
  # end

  desc Gore::EventLog::Item, 'event_log_items' do
    list :created_at, :severity, :source, [:event, 'stringify', link: true], [:data, 'inline_multi_view']
    view :id, :created_at, :severity, :source, :event, [:data, 'multi_view'], [:extra, 'hash_view']
    actions update: false, delete: false
  end

  desc Rabotaru::Job, 'rabotaru_jobs' do
    list [:id, :link], :state, :created_at, :updated_at, :started_at, :loaded_at, :processed_at, :cleaned_at, :failed_at
    list_css_classes { |x| { processed: x.cleaned?, failed: x.failed? } }
    view :id, :state, :created_at, :updated_at, :started_at, :loaded_at, :processed_at, :cleaned_at, :failed_at,
      "loadings.count", [:error, 'h'], :run_count, [:cities, 'array_inline'], [:industries, 'array_inline'], [:results, 'hash_of_lines']
    view_subcollection :loadings, 'rabotaru_loadings'
    actions update: false, delete: false
  end

  desc Rabotaru::Loading, 'rabotaru_loadings' do
    list :id, :city, :industry, :state, :error, :changed_at
    view :id, :state, :created_at, :changed_at
    actions update: false, delete: false
  end
end
<file_sep>/lib/gore/event_log.rb
##
# Comments
# * data, extra attributes are not stored when they are nil
# * severity is not stored if it is :info
# * updated_at is not stored
# * created_at is explicitely stored, while it can be extracted from _id
# 
class Gore::EventLog
  class << self
    def write(source, event, severity = :info, data = nil, extra = nil)
      return nil unless severity_logged?(severity)
      item = Item.new source: source, event: event
      
      # don't store those attributes if they have default values
      item.severity = severity unless severity.nil? || severity.to_s == 'info'
      item.data = data unless data.nil?
      item.extra = extra unless extra.nil?
      
      item.save!

      puts "Event: #{item.as_string}" if $log_to_stdout

      item
    end
    
    %w(debug info warn error fatal).each do |severity|
      class_eval <<-ruby
        def #{severity}(source, event, data = nil, extra = nil)
          write(source, event, :#{severity}, data, extra)
        end
      ruby
    end
    
    SEVERITY_LEVELS = { debug: Logger::DEBUG, info: Logger::INFO, warn: Logger::WARN, error: Logger::ERROR }
    
    def severity_logged?(item_severity)
      item_severity_level = SEVERITY_LEVELS[item_severity] || -1
      item_severity_level >= Log.level
    end
  end  

  module Accessor
    def self.included(klass)
      klass.class_eval do
        def self.log
          @log ||= Writer.for_class(self)
        end
      end
    end
    
    def log
      self.class.log
    end    
  end
  
  class Writer
    attr_accessor :source
    
    def initialize(source)
      @source = source.to_s
    end

    METHODS = %w(write debug info warn error)

    def method_missing(selector, *args, &block)
      if METHODS.include?(selector.to_s)
        Gore::EventLog.send(selector, source, *args, &block)
      else
        super
      end
    end
    
    def self.for_class(klass)
      new log_key_for(klass)
    end
    
    def self.log_key_for(klass)
      klass.name.underscore.parameterize('_')
    end
  end

  class Item < Gore::ApplicationModel
    store_in collection: "sys.events"

    field :source
    field :event, type: String
    field :severity, type: Symbol
    field :data
    field :extra, type: Hash

    no_update_tracking

    def time
      id.generation_time
    end

    def severity
      super || :info
    end
    
    def as_string
      "#{time} #{source}::#{event} (#{Gore.inspect_value(data)})"
    end
    
    def to_s
      "#{source}::#{event}@#{time}"
    end
    
    class << self
      def query(options)
        return self unless options[:q].present?
        
        source, event = options[:q].split('::')
        
        conditions = {}
        conditions[:source] = /^#{source}/ if source.present?
        conditions[:event] = /^#{event}/ if event.present?
        
        where(conditions)
      end      
    end
  end
end
<file_sep>/test/rabotnegi/functional/vacancies_controller_test.rb
require 'test_helper'

describe "Vacancies controller" do
  test 'GET /vacancies/:id via XHR' do
    @vacancy = make Vacancy, city: "spb", industry: "it", title: "Программист"

    gets URI.encode("/vacancies/#{@vacancy.to_param}"), {}, xhr: true
    
    assert_have_no_selector "meta"
    assert_have_selector ".entry-details"
  end

  test "GET /vacancies/:non-existing" do
    get "/vacancies/4e415504e999fb2522000003"
    assert_equal 404, response.status
  end

  test "GET /vacancies/:old-site-id" do
    get "/vacancies/123456"
    assert_equal 410, response.status    
  end
  
  test "GET /vacancies/:id-with-wrong-slug" do
    @vacancy = make Vacancy, title: "Программист"
    
    get "/vacancies/#{@vacancy.id}-developer"
    assert response.redirect?
    assert_match URI.encode("/vacancies/#{@vacancy.to_param}"), response.location
  end
  
  test 'GET /vacancies/:city/:industry' do
    v1 = make Vacancy, city: "spb", industry: "it"
    v2 = make Vacancy, city: "spb", industry: "it"
    v3 = make Vacancy, city: "msk", industry: "it"
    v4 = make Vacancy, city: "spb", industry: "opt"
    
    gets "/vacancies/spb/it"

    response.body.must_include v1.title
    response.body.must_include v2.title
    response.body.wont_include v3.title
    response.body.wont_include v4.title
  end
  
  test "GET /vacancies/:city/:industry with sorting" do
    v1 = make Vacancy, city: "spb", industry: "it", employer_name: "AAA"
    v2 = make Vacancy, city: "spb", industry: "it", employer_name: "BBB"
  
    gets "/vacancies/spb/it", sort: "employer_name"
    response.body.must_match %r{#{v1.title}.*#{v2.title}}

    gets "/vacancies/spb/it", sort: "-employer_name"
    response.body.must_match %r{#{v2.title}.*#{v1.title}}
  end
  
  test "new" do
    gets "/vacancies/new"
    assert_contain "Публикация новой вакансии"
  end
  
  test "create valid record" do
    $all_captchas_are_valid = true
    post "/vacancies/create", vacancy: { title: "Developer", city: "msk", industry: "it", salary_text: "55000" }
  
    new_vacancy = Vacancy.last
    new_vacancy.wont_be_nil
    new_vacancy.title.must_equal "Developer"
    new_vacancy.city.must_equal "msk"
    new_vacancy.industry.must_equal "it"
    new_vacancy.salary.exact.must_equal 55_000
    new_vacancy.poster_ip.must_equal "127.0.0.1"
    
    response.must_be :redirect?
    response.location.must_match app.url(:vacancies, :show, id: new_vacancy)
  end
  
  test "create invalid record" do
    post "/vacancies/create", vacancy: { title: nil }
  
    response.status.must_equal 422
    Vacancy.last.must_be_nil
    assert_contain "Публикация новой вакансии"
  end
  
  after do
    $all_captchas_are_valid = false
  end
end
<file_sep>/Gemfile
source 'https://rubygems.org'

gem 'rake'
gem 'sinatra-flash', require: 'sinatra/flash'
gem 'sass'
gem 'slim'
gem 'haml'
gem 'erubis'
gem 'coffee-script'
gem 'sprockets'
gem 'mongoid'
gem "daemons"

gem "unicode_utils"
gem 'resque'
gem 'rmagick'
gem "syslog-logger"
gem "sanitize"

gem 'padrino', '0.12.4'

# gem "therubyracer" # weird bugs otherwise

group :test do
  gem 'mocha'
  gem 'minitest', require: "minitest/autorun"
  gem 'rack-test', require: "rack/test"
  gem "factory_girl"
  gem 'turn'
  gem 'webrat'
  gem 'capybara'
  # gem 'capybara-webkit'
end

group :development, :test do
  gem 'capistrano'
  gem 'thin'
end
<file_sep>/test/rabotnegi/unit/employer_test.rb
require 'test_helper'

unit_test Employer do
  setup do
    @microsoft = Employer.new(:name=>'Microsoft', :login => 'ms', :password => '123', :password_confirmation => '123')
  end
  
  test "creation" do
    assert @microsoft.valid?
  end
  
  test "creation without password" do
    @bad_microsoft = Employer.new(:name => 'Microsoft', :login => 'ms')
    assert !@bad_microsoft.valid?
    assert @bad_microsoft.errors[:password].any?
  end    
end
<file_sep>/script/resque
#!/usr/bin/env ruby

require 'daemons'
require 'optparse'

rails_root = File.expand_path('../..', __FILE__)
ENV['RAILS_PROC'] = 'worker'

id = ARGV.shift
raise "Should be used as: resque ID start|stop|restart|run" unless id.to_i > 0

Daemons.run_proc("resque.#{id}", dir: File.join(rails_root, "tmp/pids"), dir_mode: :normal, log_output: true, log_dir: File.join(rails_root, "log")) do
  Dir.chdir(rails_root)
  require File.join(rails_root, "config/boot.rb")
  worker = Resque::Worker.new("*")
  worker.verbose = true
  worker.work(5)
end

__END__

#!/bin/bash
export JAVA_HOME=/usr/local/java/
CLASSPATH=ajarfile.jar:.

case $1 in
   start)
      echo $$ > /var/run/xyz.pid;
      exec 2>&1 java -cp ${CLASSPATH} org.something.with.main 1>/tmp/xyz.out 
      ;;
    stop)  
      kill `cat /var/run/xyz.pid` ;;
    *)  
      echo "usage: xyz {start|stop}" ;;
esac
exit 0
<file_sep>/lib/gore/pure_delegator.rb
class Gore::PureDelegator < BasicObject
  def initialize(target)
    @target = target
  end

  def call(selector, *args, &block)
    method_missing(selector, *args, &block)
  end

  def method_missing(selector, *args, &block)
    @target.send(selector, *args, &block)
  end
end
  <file_sep>/test/rabotnegi/unit/vacancy_test.rb
require 'test_helper'

unit_test Vacancy do
  setup do
    @vacancy = Vacancy.new    
  end
  
  test 'default salary' do
    assert @vacancy.salary.negotiable?
  end
  
  test "#salary_text=" do
    vacancy = Vacancy.new(salary: Salary.make(exact: 1000))
    assert_equal Salary.make(exact: 1000), vacancy.salary
    vacancy.salary_text = "от 5000"
    assert_equal Salary.make(min: 5000), vacancy.salary
    assert_equal 5000, vacancy.salary_min
    assert_equal nil, vacancy.salary_max
  end
  
  test "#==" do
    assert Vacancy.new != nil
    assert Vacancy.new != Vacancy.new
    assert Vacancy.new(title: "Boss") != Vacancy.new(title: "Boss")
    assert_equal Vacancy.new(title: "Boss", external_id: 100), Vacancy.new(title: "Developer", external_id: 100)
  end
  
  test "#to_param" do
    v1 = make(Vacancy, title: "Ruby Разработчик")
    assert_equal "#{v1.id}-ruby-разработчик", v1.to_param
    
    v1.title = nil
    assert_equal v1.id.to_s, v1.to_param
  end
  
  test "long slugs" do
    assert_equal "ruby-разработчик", make(Vacancy, title: "Ruby Разработчик").slug
    assert_equal "менеджер-по-продажам-промышленного-оборудования", make(Vacancy, title: "Менеджер по продажам промышленного оборудования").slug
    assert_equal "ааааааа-бббббббббб-вввввввввв-ггггггггггг-дддддддд-еееееееее", make(Vacancy, title: "Ааааааа Бббббббббб вввввввввв ггггггггггг дддддддд еееееееее жжжжжжж").slug
  end

  test "#get" do
    v1 = make Vacancy

    assert_equal v1, Vacancy.get(v1.id)
    assert_equal v1, Vacancy.get(v1.id.to_s)
    assert_equal v1, Vacancy.get("#{v1.id}-xxxx-1111")
    assert_equal v1, Vacancy.get("#{v1.id}-1111-xxxx")

    assert_nil Vacancy.get("4daebd518c2e000000000000")
  end  

  test "#search" do
    v1_msk_retail = make Vacancy, title: "query-1", city: "msk", industry: "retail"
    v1_spb_it = make Vacancy, description: "query-1", city: "spb", industry: "it"
    v2 = make Vacancy, description: "query-2"    
    v3 = make Vacancy, employer_name: "query-3"    
    
    Vacancy.search(q: "query-1").must_eq [v1_msk_retail, v1_spb_it]
    Vacancy.search(q: "query-2").must_eq [v2]
    Vacancy.search(q: "query-3").must_eq [v3]
    
    Vacancy.search(q: "query-1", city: "msk").must_eq [v1_msk_retail]    
    Vacancy.search(q: "query-1", industry: "it").must_eq [v1_spb_it]
  end
end
<file_sep>/test/lib/stubbing_test.rb
require "test_helper"

unit_test Gore::Stubbing do
  dummy = temp_class do
    def self.foo(param)
      "original #{param}"
    end
  end
  
  test "stubbing/unstubbing" do
    assert_equal "original passed", dummy.foo("passed")

    Gore::Stubbing.stub(dummy, :foo) { |param| "patched #{param}" }
    assert_equal "patched passed", dummy.foo("passed")    

    Gore::Stubbing.unstub_all
    assert_equal "original passed", dummy.foo("passed")
  end
end
<file_sep>/test/lib/err_mailer_test.rb
require 'test_helper'

describe "Gore::Err mailer" do
  test "notification" do
    err = Gore::Err.new(
      controller: "vacancies", 
      action: "show", 
      url: "http://rabotnegi.test/vacancies/1234", 
      verb: "GET",
      host: "rabotnegi.test", 
      time: Time.current,
      session: {session_var_1: 100, session_var_2: 200}, 
      params: {parameter_1: 'parameter_1_value'}, 
      exception_class: 'ApplicationError',
      exception_message: "a test thing",
      cookies: ["uid"],
      backtrace: "stack line 1\nstack line 2\nstack line 3",
      request_headers: {'Header-1' => '100', 'Header-2' => '200'},
      response_headers: {}
    )
 
    email = err.notify
    email.to.must_equal [Rabotnegi.config.err_recipients]
    email.from.must_equal [Rabotnegi.config.err_sender]
    email.subject.must_equal "[rabotnegi.ru errors] vacancies/show - ApplicationError - a test thing"

    body = email.body.to_s
    body.must_match "GET http://rabotnegi.test/vacancies/1234"
    body.must_match "parameter_1"
    body.must_match "parameter_1_value"
    body.must_match "stack line 1"
    body.must_match "stack line 2"
  end
end
<file_sep>/app/models/rabotaru/loading.rb
module Rabotaru
  class Loading
    include Mongoid::Document
        
    field :city, type: Symbol
    field :industry, type: Symbol
    field :state, type: Symbol, default: 'pending'
    field :error, type: String
    field :fail_count, type: Integer
    # key :city, :industry

    embedded_in :job, class_name: 'Rabotaru::Job'
    validates_presence_of :city, :industry
    def_state_predicates 'state', :pending, :queued, :started, :done, :skipped, :failed
    
    def queue
      mark :queued
      Gore.enqueue(Rabotaru::Loading, :run, job.id, id)
    end

    def run
      mark :started
      loader = Loader.new(city, industry, job.id)
      loader.load
      mark :done
    rescue => e
      Gore::Err.register("Rabotaru::Loading.run", e, params: {city: city, industry: industry})
      mark :failed, error: Gore.format_error(e), fail_count: fail_count.to_i + 1
    end
    
    def changed_at
      self.class._states.map { |state| send("#{state}_at") }.compact.max
    end
    
    def inspect(*args)
      Gore.inspection(self, [city, industry], state)
    end
    
    def to_s
      inspect
    end
    
    def self.run(job_id, loading_id)
      job = Job.find(job_id)
      loading = job.loadings.find(loading_id)
      loading.run
    end
  end
end
<file_sep>/app/models/vacancy.rb
class Vacancy < Gore::ApplicationModel
  field :title
  field :description
  field :external_id, type: Integer
  field :industry
  field :city
  field :salary_min, type: Integer
  field :salary_max, type: Integer
  field :employer_id, type: Integer
  field :employer_name
  field :poster_ip, type: String
  field :loaded_at, type: Time
  field :cleaned_at, type: Time
  
  index city: 1
  index industry: 1
  index city: 1, industry: 1
  index city: 1, title: 1

  validates_presence_of :title, :industry, :city

  belongs_to :employer
  
  scope :old, ->(date = 1.month.ago) { where(:updated_at.lt => date) }

  before_save do
    self.employer_id = employer.id if employer
    self.employer_name = employer.name if employer
  end

  def ==(other)
    Vacancy === other && external_id? ? self.external_id == other.external_id : super
  end

  def to_s
    title
  end
  
  def to_param
    [id, slug].reject(&:blank?).join('-')
  end
  
  def slug
    Gore::RussianInflector.parameterize(title).truncate(60, separator: '-', omission: '')    
  end

  def city_name
    City.get(city).name
  end

  def industry_name
    Industry.get(industry).name
  end
  
  def salary
    Salary.new(salary_min, salary_max)
  end
  
  def salary=(salary)
    self.salary_min = self.salary_max = nil
    self.salary_min = salary.min if salary
    self.salary_max = salary.max if salary
  end
  
  def salary_text
    salary.try(:text)
  end
  
  def salary_text=(value)
    self.salary = Salary.parse(value)
  end
  
  def self.search(params)
    params = params.symbolize_keys
    params.assert_valid_keys(:city, :industry, :q)
    query = Regexp.new(params[:q] || "", true)

    scope = self
    scope = scope.where(city: params[:city]) if params[:city].present?
    scope = scope.where(industry: params[:industry]) if params[:industry].present?
    scope = scope.any_of({title: query}, {employer_name: query}, {description: query}) if params[:q].present?
    scope
  end

  instance_eval { alias query search }
  
  def self.get(id)
    id ||= ""
    id = id.gsub(/\-.*/, '') if String === id
    where(_id: id).first
  end
  
  def self.cleanup
    old.destroy
  end
end
<file_sep>/lib/gore/log_filter.rb
module Gore::LogFilter
  FILTERED_LOG_ENTRIES = [
    "Served asset", 'Started GET "/assets/',
    "['system.namespaces'].find({})",
    "MONGODB [WARNING] Please note that logging negatively impacts client-side performance.",
    "MONGODB admin['$cmd'].find({:ismaster=>1}).limit(-1)",
    "MONGODB admin['$cmd'].find({:buildinfo=>1}).limit(-1)"
  ]

  def self.register
    Padrino::Logger.class_eval do  
      def write(message = nil)
        return if String === message && FILTERED_LOG_ENTRIES.any? { |pattern| message.include?(pattern) }
        self << message
      end  
    end    
  end
end
<file_sep>/lib/gore/sass_functions.rb
module Gore::SassFunctions
  def self.register
    Sass::Script::Functions.module_eval do
      def image_url(source)
        ::Sass::Script::String.new "url(/rabotnegi/assets/#{source.value})"
      end
  
      declare :image_url, [:source]
    end
  end
end
<file_sep>/test/lib/sort_expressions_test.rb
require "test_helper"

unit_test Gore::SortExpressions do
  test "decode_order_to_mongo" do
    assert_equal [["date", Mongo::ASCENDING]], Gore::SortExpressions.decode_order_to_mongo("date")
    assert_equal [["date", Mongo::DESCENDING]], Gore::SortExpressions.decode_order_to_mongo("-date")
  end
end
<file_sep>/app/controllers/admin.rb
Rabotnegi.controller :admin do
  before { admin_required }
  layout 'admin'

  get :index do
    render "admin/dashboard"
  end
end

Rabotnegi.controller :admin_items, map: "admin/:collection/" do
  before { admin_required }
  before { @collection = Gore::MongoReflector.metadata_for(params[:collection]) }
  layout "admin"

  get :index, map: "/#{@_map}" do
    @scope = @collection.klass.respond_to?(:query) ? @collection.klass.query(q: params[:q]) : @collection.klass
    @models = @scope.order_by(@collection.list_order).paginate(params[:page], @collection.list_page_size)
    render "admin_items/index"
  end

  get :show, map: ":id" do
    @model = @collection.klass.get(params[:id])
    render "admin_items/show"
  end

  get :edit, map: "edit/:id" do
    @model = @collection.klass.get(params[:id])
    render "admin_items/edit"
  end

  post :update, map: "update/:id" do
    @model = @collection.klass.get(params[:id])
    update_model @model, params[@collection.singular], url(:admin_item, @collection.key, @model)
  end

  post :delete, map: "delete/:id" do
    @model = @collection.klass.get(params[:id])
    @model.destroy
    flash.notice = "#{@model} была удалена"
    redirect url(:admin_items, @collection.key)
  end
end
<file_sep>/lib/gore/controller_helpers.rb
module Gore::ControllerHelpers
  module Urls
    def absolute_url(*args)
      uri url(*args)
    end

    def current_url(params = {})
      current_path(params.stringify_keys)
    end

    def asset_path(kind, source)
      return source if source =~ /^http/
      return source if source =~ /^\//
      asset = settings.assets.find_asset(source)
      return nil unless asset
      "/rabotnegi/assets/" + asset.digest_path
    end
  
    def decode_order_to_mongo(param = params[:sort])
      Gore::SortExpressions.decode_order_to_mongo(param)
    end  

    def encode_order(field, reverse_by_default = false, param = params[:sort])
      Gore::SortExpressions.encode_order(field, params, reverse_by_default)
    end    
  end
  
  module Identification
    # (vacancy#1234) => "v-2134"
    def web_id_for_record(record)
      return nil unless record
      [web_prefix_for_class(record.class), record.id].join("-")
    end
  
    # (Vacancy) => "v"
    # (User) => "user"  
    def web_prefix_for_class(klass)
      case klass
        when Vacancy then "v"
        else ActiveModel::Naming.singular(record)
      end    
    end
  
    # (vacancy#1234) => "v-1234"
    # (:edit, vacancy#1234, :custom) => "edit-v-1234-custom"
    def web_id(*args)
      args.map { |x| x.respond_to?(:to_key) ? web_id_for_record(x) : x }.join("-")
    end    
  end  
  
  module Users
    def current_user
      return @current_user if defined? @current_user
      @current_user = find_user_by_cookie || find_user_by_ip || create_new_user
      store_user_cookie unless request.cookies['uid']
      @current_user
    end  
  
    def find_user_by_cookie
      return nil unless request.cookies['uid']
      user_id = settings.message_encryptor.decrypt_and_verify(request.cookies['uid'])
      User.find(user_id)
    rescue
      response.delete_cookie 'uid'
      nil
    end
    
    def find_user_by_ip
      User.where(agent: request.user_agent, ip: request.ip).first
    end
    
    def create_new_user
      bot? ? User.new : User.create!(agent: request.user_agent, ip: request.ip)
    end
    
    def store_user_cookie
      user_token = settings.message_encryptor.encrypt_and_sign(@current_user.id)
      response.set_cookie 'uid', value: user_token, path: request.script_name, expires: 2.years.from_now            
    end

    #
    # Authentication
    # 
  
    def admin_required
      return if Gore.env.development?
      halt 401, {"WWW-Authenticate" => %(Basic realm="Restricted Area")}, "" unless authorized?
    end

    def authorized?
      @auth ||= Rack::Auth::Basic::Request.new(request.env)
      @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == ['admin', '0000']
    end
    
    # 
    # Other
    # 

    def bot?
      false # request.user_agent =~ /Googlebot|YandexBot|Deepnet|Nigma|bingbot|stat.cctld.ru/
    end  

    # def employer_required
    #   current_employer || redirect(employer_path)
    # end
    # 
    # def resume_required
    #   current_resume || redirect_to(worker_login_path)
    # end
    # 
    # def current_employer
    #   return @current_employer if defined? @current_employer
    #   @current_employer = session[:employer_id] ? Employer.get(session[:employer_id]) : nil
    # end
    # 
    # def current_resume
    #   return @current_resume if defined? @current_resume
    #   resume_id = session[:resume_id] || cookies[:resume_id]
    #   if @current_resume = resume_id ? Resume.get(resume_id) : nil
    #     session[:resume_id] = @current_resume.id
    #     cookies[:resume_id] = @current_resume.id
    #   else
    #     session[:resume_id] = nil
    #     cookies.delete(:resume_id)
    #   end
    #   @current_resume
    # end    
  end

  module Captchas
    def captcha_valid?
      return @captcha_valid unless @captcha_valid.nil?
      return true if $all_captchas_are_valid
      @captcha_valid = Gore::Captcha.valid?(params[:captcha_id], params[:captcha_text])
    end

    def captcha_valid!(object = nil)
      if captcha_valid?
        true
      else
        object.errors.add(:captcha, "invalid") if object
        log.warn 'captcha_wrong', ip_adress: request.ip, captcha_text: params[:captcha_text]
        false
      end
    end
  
    def captcha_section
      if captcha_valid?
        hidden_field_tag(:captcha_text, value: params[:captcha_text]) + hidden_field_tag(:captcha_id, value: params[:captcha_id])
      else
        @captcha = Gore::Captcha.create!
        @captcha_url = url(:captcha, id: @captcha)
        div "captcha" do
          image_tag(@captcha_url, width: 100, height: 28) + br + text_field_tag(:captcha_text) + hidden_field_tag(:captcha_id, value: @captcha.id)
        end
      end
    end    
  end
end
<file_sep>/lib/gore/err.rb
class Gore::Err < Gore::ApplicationModel
  store_in collection: "sys.exceptions"
  
  field :source
  field :host
  field :params, type: Hash
  field :exception_class
  field :exception_message
  field :backtrace

  field :controller
  field :action

  field :url
  field :verb
  field :session, type: Hash
  field :cookies, type: Hash
  field :request_headers, type: Hash
  field :response_headers, type: Hash

  index created_at: 1

  validates_presence_of :exception_class
  
  scope :recent, ->(since = 1.hour.ago) { where(:created_at.gte => since) }

  def source
    self[:source] || "#{controller}/#{action}"
  end

  def notify
    err = self
    Rabotnegi.email do
      def method_missing(selector, *args, &block)
        super
      rescue NoMethodError
        @__dummy__ ||= Rabotnegi.new!
        @__dummy__.respond_to?(selector) ? @__dummy__.send(selector, *args, &block) : raise
      end
      
      from Rabotnegi.config.err_sender
      to Rabotnegi.config.err_recipients
      subject "[rabotnegi.ru errors] #{err}"
      content_type "text/html; charset=utf-8"
      locals err: err
      body render("err_notification")
    end
  end

  def to_s
    "#{source} - #{exception_class} - #{exception_message.to_s.truncate(40)}"
  end

  class << self
    def register(source, exception, data = {})
      Log.error "!!! Error logged: #{exception.class} #{exception.message}"
      
      data.update(
        source: source,
        host: Socket.gethostname, 
        time: Time.now,
        exception_class: exception.class.name,
        exception_message: exception.message,
        backtrace: exception.backtrace.join("\n")
      )
      
      err = create!(data)
      err.notify if recent.count < Rabotnegi.config.err_max_notifications_per_hour
      err

    rescue => e
      puts "!!! ERROR IN ERROR LOGGING: #{e.class}: #{e.message}"
      puts e.backtrace.join("\n")
    end
    
    def query(params)
      params = params.symbolize_keys
      params.assert_valid_keys(:q)
      query = Regexp.new(params[:q] || "", true)

      scope = self
      scope = scope.any_of({exception_class: query}, {exception_message: query}, {url: query}) if params[:q].present?
      scope
    end
  end  
end
<file_sep>/config/boot.rb
# Defines our constants
RACK_ENV = ENV['RACK_ENV'] ||= 'development'  unless defined?(RACK_ENV)
PADRINO_ROOT = File.expand_path('../..', __FILE__) unless defined?(PADRINO_ROOT)


# Load our dependencies
require 'rubygems' unless defined?(Gem)
require 'bundler/setup'

# bundler_group = ENV["RACK_ENV"]
# bundler_group = "test" if bundler_group == "testui"
Bundler.require(:default, RACK_ENV)

# Padrino::Logger::Config[:production] = { :log_level => :info, :stream => :to_file }
# Padrino::Logger::Config[:testprod] = { :log_level => :info, :stream => :stdout }
# Padrino::Logger::Config[:testui] = { :log_level => :info, :stream => :stdout }
#
# silence_warnings { Sass::Engine::DEFAULT_OPTIONS = Sass::Engine::DEFAULT_OPTIONS.dup.merge(style: :compact) }

Padrino.before_load do
  # I18n.locale = 'ru'
end

Padrino.after_load do
  Gore::SassFunctions.register
end

Padrino.load!
<file_sep>/test/support/data.rb
# Syntax: vacancy(title, industry = 'it', city = 'spb', other = {})
def vacancy(title, *args)
  other = args.extract_options!
  attrs = {title: title}
  attrs[:industry] = args.first || 'it'
  attrs[:city] = args.second || 'spb'
  attrs[:created_at] = "2011-09-01"
  attrs[:description] = "lorem ipsum sit amet"
  attrs.merge!(other)
  Vacancy.create!(attrs)
end

vacancy "Designer", employer_name: "Apple"
vacancy "Ruby Developer", employer_name: "Apple"
vacancy "Python Developer", employer_name: "Apple"
vacancy "JavaScript Developer", employer_name: "Apple", description: "Do some JS and frontend stuff"
vacancy "Manager", employer_name: "Apple"
vacancy "Tester", employer_name: "Apple"

vacancy "Designer", city: 'msk', employer_name: "<NAME>"
vacancy "Ruby Developer", city: 'msk', employer_name: "Msk Co"

vacancy "Бухгалтер", "retail", employer_name: "<NAME>"
vacancy "Главбух", "retail", employer_name: "<NAME>"
vacancy "Ассистент", "retail", employer_name: "<NAME>"
vacancy "Гендир", "retail", employer_name: "<NAME>"
<file_sep>/test/lib/err_test.rb
require 'test_helper'

unit_test Gore::Err do
  ERROR_DATA = {
    url: "http://rabotnegi.test/vacancies/1234", 
    verb: "GET",
    session: {session_var_1: 100, session_var_2: 200}, 
    params: {parameter_1: 'parameter_1_value'}, 
    cookies: ["uid"],
    request_headers: {'Header-1' => '100', 'Header-2' => '200'},
    response_headers: {}
  }

  setup do
    sent_emails.clear
    Gore::Err.delete_all
    @exception = ArgumentError.new("test error message")
    @exception.set_backtrace ["stack line 1", "stack line 2", "stack line 3"]
  end
  
  test "register a request error" do
    err = Gore::Err.register("vacancies/show", @exception, ERROR_DATA)

    Gore::Err.count.must_equal 1
    Gore::Err.last.exception_message.must_equal "test error message"
    Gore::Err.last.exception_class.must_equal "ArgumentError"
    Gore::Err.last.backtrace.must_equal ["stack line 1", "stack line 2", "stack line 3"].join("\n")

    assert_emails 1
    sent_emails.last.subject.must_match "test error message"
  end

  test "register an error when there were too many other errors this hour" do
    Rabotnegi.config.err_max_notifications_per_hour.times { Gore::Err.create!(ERROR_DATA.merge(exception_class: "ArgumentError")) }

    err = Gore::Err.register("vacancies/show", @exception, ERROR_DATA)
    assert_equal Rabotnegi.config.err_max_notifications_per_hour + 1, Gore::Err.count
    assert_emails 0
  end
end
<file_sep>/script/rr_scheduler.rb
#!/usr/bin/env ruby

require 'pp'
# require 'daemons'
# require 'optparse'
# 
rails_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
ENV['RAILS_PROC'] = 'worker-rr'
# 
# id = ARGV.shift
# raise("Numeric ID should be given as the first argument") unless id.to_i > 0
# 
# Daemons.run_proc("resque.#{id}", dir: File.join(rails_root, "tmp/pids"), dir_mode: :normal, log_output: true, log_dir: File.join(rails_root, "log")) do
#   Dir.chdir(rails_root)
#   require File.join(rails_root, "config/environment")
#   worker = Resque::Worker.new("*")
#   worker.verbose = true
#   worker.work(5)
# end

require File.join(rails_root, "config/environment")
Rabotaru.start_job
<file_sep>/app/models/rabotaru.rb
module Rabotaru
  def self.start_job(options = {})
    Job.create!(options).run
  end
end
<file_sep>/lib/gore/russian_inflector.rb
module Gore::RussianInflector
	def self.inflect(number, word, end1, end2, end5, strategy = :normal)
		number_by_100 = number % 100
	  ending = case strategy
		when :normal
			case number_by_100
			when 1 then end1
			when 2..4 then end2
			when 5..20 then end5
			else
				case number_by_100 % 10
				when 1 then end1
				when 2..4 then end2
				when 0, 5..9 then end5
				end
			end
		when :more
			case number_by_100
			when 1 then end2
			when 2..20 then end5
			else
				case number_by_100 % 10
				when 1 then end2
				when 0, 2..9 then end5
				end
			end	
		end
		word + ending
	end	
	
	def self.parameterize(string, sep = "-")
    string ||= ""

	  parameterized_string = UnicodeUtils.downcase(string).gsub(/[^a-z0-9\u0430-\u044f\-_]+/i, sep)

	  if sep.present?
      re_sep = Regexp.escape(sep)
      parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
      parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
    end
    
    parameterized_string
	end
end

if $0 == __FILE__
	1.upto(150) do |i|
		print "#{i} "
		print Gore::RussianInflector.inflect(i, 'ваканс', 'ия', 'ии', 'ий' )
		print "\n"
	end

	1.upto(150) do |i|
		print "более #{i} "
		print Gore::RussianInflector.inflect(i, 'ваканс', 'ия', 'ии', 'ий', :more)
		print "\n"
	end
end
<file_sep>/lib/gore/event_handling.rb
module Gore::EventHandling
  def event_handlers
    @event_handlers ||= {}
  end
  
  def event_handlers_for(event)
    event = event.to_sym
    event_handlers[event] || []
  end
  
  def add_event_handler(event, &block)
    event = event.to_sym
    event_handlers[event] ||= []
    event_handlers[event] << block
  end
  
  def remove_event_handler(event, &block)
    event = event.to_sym    
    event_handlers[event] ||= []
    event_handlers[event].delete_if { |h| h == block }
  end
  
  def raise_event(event, &block)
    event = event.to_sym
    return if event_handlers[event].blank?
    
    event_data = block.call
    event_handlers[event].each { |handler| handler.call(event_data) }
  end
end
<file_sep>/config/database.rb
database_name = case Padrino.env
  when :development then 'rabotnegi_dev'
  when :production  then 'rabotnegi_prod'
  when :test        then 'rabotnegi_test'
  when :testui      then 'rabotnegi_testui'
  when :testprod    then 'rabotnegi_dev'
end

# mongoid_options = {}
# mongoid_options = {logger: Padrino.logger} if Padrino.env == :development # && ($*.first !~ /^(test|routes)/)

old_log_level, Padrino.logger.level = Padrino.logger.level, Padrino::Logger::Levels[:error]

# Mongoid.database = Mongo::Connection.new('localhost', Mongo::Connection::DEFAULT_PORT, mongoid_options).db(database_name)
Mongoid.logger = Padrino.logger
Mongoid.load!("config/mongoid.yml")

Padrino.logger.level = old_log_level

# You can also configure Mongoid this way
# Mongoid.configure do |config|
#   name = @settings["database"]
#   host = @settings["host"]
#   config.master = Mongo::Connection.new.db(name)
#   config.slaves = [
#     Mongo::Connection.new(host, @settings["slave_one"]["port"], :slave_ok => true).db(name),
#     Mongo::Connection.new(host, @settings["slave_two"]["port"], :slave_ok => true).db(name)
#   ]
# end
<file_sep>/test/rabotnegi/unit/user_test.rb
require 'test_helper'

unit_test User do
  test "#favorite_vacancies" do
    user = User.create!
    v1 = make Vacancy
    v2 = make Vacancy
    
    user.favorite_vacancies << v1.id
    user.save!    
    user.reload.favorite_vacancies.must_equal [v1.id]

    user.favorite_vacancies << BSON::ObjectId(v2.id.to_s)
    user.save!
    user.reload.favorite_vacancies.must_equal [v1.id, v2.id]
  end
end
<file_sep>/app/models/employer.rb
class Employer < Gore::ApplicationModel
  field :name
  field :login
  field :password
  field :id, type: Integer   

  validates_presence_of :name, :login, :password
  validates_uniqueness_of :login
  validates_confirmation_of :password

  has_many :vacancies

  attr_accessor :password_confirmation
  
  def to_s
    name
  end
  
  def add_vacancy(vacancy)
    vacancy.employer_id = id
    vacancy.employer_name = name    
    vacancies << vacancy    
  end

  def self.authenticate(login, password)
    where(:login => login, :password => <PASSWORD>).first || raise(ArgumentError)
  end
end
<file_sep>/test/rabotnegi/unit/rabotaru_loader_test.rb
require 'test_helper'

unit_test Rabotaru::Loader do
  setup do
    @directory = Rabotnegi.config.rabotaru_dir.join(Gore.date_stamp)
  end

  teardown do
    FileUtils.rm_rf(@directory)
  end
  
  test "load loads the feed from the web and writes it to the file" do
    Http.stubs(:get).returns("downloaded data")

    loader = Rabotaru::Loader.new("spb", "it")
    loader.load
    
    assert @directory.directory?
    assert_equal "downloaded data", @directory.join("spb-it.json").read
  end
  
  test "load doesn't reload the file if it already exists" do
    @directory.mkpath
    File.write(@directory.join("spb-it.json"), "existing data")
    Http.stubs(:get).never

    loader = Rabotaru::Loader.new("spb", "it")
    loader.load
    
    assert_equal "existing data", @directory.join("spb-it.json").read
  end
end
<file_sep>/test/rabotnegi/unit/vacancy_cleaner_test.rb
require 'test_helper'

unit_test VacancyCleaner do
  test "clean title" do
    assert_equal "Помошник повара", VacancyCleaner.clean_title("ПОМОШНИК ПОВАРА")
    assert_equal "Помошник повара", VacancyCleaner.clean_title("помошник повара")
    assert_equal "Программист на .NET", VacancyCleaner.clean_title("программист на .NET")
    
    assert_equal %{"Quotes" «Rusquotes» – 'Quote'}, VacancyCleaner.clean_title("&quot;Quotes&quot; &laquo;Rusquotes&raquo; &ndash; &#039;Quote&#039;")
  end

  test "clean employer name" do
    assert_equal %{"Quotes" «Rusquotes» – 'Quote'}, VacancyCleaner.clean_employer_name("&quot;Quotes&quot; &laquo;Rusquotes&raquo; &ndash; &#039;Quote&#039;")
  end
  
  test "clean description" do
    f = ->(str) { VacancyCleaner.clean_description(str) }
    
    assert_equal %{, "}, f.("&nbsp; &sbquo; &quot;")
    assert_equal "<p>aa bb </p><p>cc </p>", f.("  aa  bb  \n  cc  \n  ")
    assert_equal "TEXT", f.("<strong><strong><strong>TEXT</strong></strong></strong>")    
    assert_equal "<h4>Требования</h4>", f.("<strong><strong><strong>Требования</strong></strong></strong>")    
    assert_equal "<p>• hello</p>", f.("<p> - hello</p>")    
    assert_equal "<p>• hello</p>", f.("<p> * hello</p>")    
    assert_equal "<p>• hello</p>", f.("<p>* hello</p>")    
    # assert_equal "<p>• hello</p>", f.("<p> &bull; hello</p>")    
  end
  
  test "clean" do
    vacancy = make Vacancy, title: "РАБОЧИЙ", employer_name: "&quot;Газпром&quot;", description: "<strong>Тест</strong>"
    VacancyCleaner.clean(vacancy)
    
    vacancy.reload
  
    assert_equal "Рабочий", vacancy.title
    assert_equal '"Газпром"', vacancy.employer_name
    assert_equal "Тест", vacancy.description
    
    original_data = JSON.parse Rabotnegi.config.original_vacancies_data_dir.join(Time.now.strftime("%Y%m")).join("#{vacancy.id}.json").read
    
    assert_equal "РАБОЧИЙ", original_data["title"]
    assert_equal '&quot;Газпром&quot;', original_data["employer_name"]
    assert_equal "<strong>Тест</strong>", original_data["description"]
  end
end
<file_sep>/script/test_encryption.rb
secret = Rabotnegi::Application.config.secret_token
message = Vacancy.first.id

verifier = ActiveSupport::MessageVerifier.new(secret)
encryptor = ActiveSupport::MessageEncryptor.new(secret)

p verifier.generate(message), encryptor.encrypt_and_sign(message), encryptor.encrypt_and_sign(message)
p "BAh7B0kiD3Nlc3Npb25faWQGOgZFRiIlZDFkOGM0N2Y4NzZkMGQ2MDhmYjM3OGY5YWY2N2IzMjNJIhBfY3NyZl90b2tlbgY7AEZJIjE3aHNEVjk2VmRhbVQyOVZJUVZjSHVKWHYwRGliSXFyRDQvRGNxeHVmUWdVPQY7AEY%3D--4448899871ab030f6af4b3489efd5939ba529efe"
<file_sep>/test/support/mocks.rb
module CurrencyConverter
	@@rates = { rub: 1.0, usd: 25.0, eur: 35.0 }
end
<file_sep>/lib/gore/mongo_reflector.rb
class Gore::MongoReflector
  cattr_accessor :collections
  @@collections = {}
  
  def self.metadata_for(key) @@collections[key.to_s] || Collection.new(key.to_s.classify.constantize) end
  def self.define_metadata(&block) Builder.new.instance_eval(&block) end

  class Collection
    attr_accessor :klass, :key
    attr_accessor :list_fields, :list_order, :list_page_size, :list_css_classes
    attr_accessor :view_fields, :view_subcollections, :edit_fields, :actions

    def initialize(klass, key = nil)
      @klass = klass
      @key = key || @klass.model_name.plural

      @view_subcollections = []
      @list_order = [:_id, :desc]
      @list_page_size = 40
      @list_css_classes = ->(m){}
      @actions = {}
    end

    def searchable?
      @klass.respond_to?(:query) 
    end
    
    def to_param
      key
    end
    
    def plural
      @klass.model_name.plural 
    end
    
    def singular
      @klass.model_name.singular 
    end
    
    def list_fields
      @list_fields || klass_fields 
    end 
      
    def view_fields
      @view_fields || klass_fields 
    end
    
    def stored_fields
      @klass.fields.reject{ |k,f| k.starts_with?('_') }.map{ |k,f| k } 
    end

    private

    def klass_fields
      @klass_fields ||= klass.fields.
        reject { |key, mongo_field| key == '_type' }.
        map { |key, mongo_field| Field.new(key, collection: self) }
    end    
  end
  
  class Field
    attr_accessor :name, :format, :args
    attr_accessor :trim, :css, :link
    attr_accessor :collection

    def initialize(name, options = {})
      @name = name.to_s
      assign_attributes!(options)
    end
    
    def title
      key = @name.gsub('.', '_')
      I18n.t(
        "attributes.#{collection.singular}.#{key}",
        default: [:"attributes.common.#{key}", key.humanize]
      )
    end
    
    def custom?
      format.is_a?(Proc)
    end
    
    def css
      Gore.css_classes_for @css, wide: format.in?([:hash, :pre])
    end
    
    def inspect
      Gore.inspection(self, name, format: format)
    end
    
    def helper
      case format
        when 'text' then 'text_field'
        when 'date_time' then 'text_field'
        else format
      end
    end
    
    def helper_args
      args
    end
  end
  
  class Subcollection
    attr_accessor :key, :accessor
    
    def initialize(accessor, key)
      @accessor = accessor
      @key = key
    end
    
    def title
      I18n.t("attributes.#{collection.singular}.#{accessor}", 
        default: [:"attributes.common.#{accessor}", accessor.to_s.humanize]
      )
    end
    
    def collection
      Gore::MongoReflector.metadata_for(key)
    end
  end

  class Builder
    attr_accessor :current_collection
    
    def desc(klass, key = nil, &block)
      @current_collection = Collection.new(klass, key)
      Gore::MongoReflector.collections[@current_collection.key] = @current_collection
      instance_eval(&block) if block_given?
      @current_collection      
    end

    def list(*params)
      @current_collection.list_fields = build_fields(params)
    end

    def list_order(value)
      @current_collection.list_order = value
    end

    def list_page_size(value)
      @current_collection.list_page_size = value
    end

    def list_css_classes(&block)
      @current_collection.list_css_classes = block
    end
    
    def actions(params = {})
      @current_collection.actions = params
    end

    def view(*params)
      @current_collection.view_fields = build_fields(params)
    end

    def edit(field_specs)
      @current_collection.edit_fields = field_specs.map do |key, spec|
        if Array === spec
          helper = spec.shift
          args = spec
        else
          helper = spec
          args = []
        end
        Field.new(key, format: helper, args: args, collection: @current_collection)
      end
    end
    
    def view_subcollection(accessor, key)
      @current_collection.view_subcollections << Subcollection.new(accessor, key)
    end

    def _
    end
    
    private

    # Accepts a list of:
    #   :name
    #   :name, :format, option1: 'value1'
    #   :name, [:format, option1: 'value1']
    def build_fields(params_list)
      params_list = params_list.first.to_a if Hash === params_list.first
      
      params_list.map do |params|
        if Array === params
          params.flatten! if Array === params.second
          options = params.extract_options!
          name = params.first
          format = params.second
        else
          name = params
          options = {}
        end

        attributes = {format: format, collection: @current_collection}.merge(options)
        Field.new(name, attributes)
      end
    end
  end
end
<file_sep>/test/rabotnegi/unit/resume_test.rb
require 'test_helper'

unit_test Resume do
  setup do
    @resume = Resume.new
  end
  
  test "name format" do
    assert_equal 'Mitya', Resume.new(:fname => 'Mitya').name
    assert_equal '<NAME>', Resume.new(:fname => 'Mitya', :lname => 'Ivanov').name
  end
end
<file_sep>/Capfile
load 'deploy' if respond_to?(:namespace)
load 'deploy/assets'

require 'bundler/capistrano'

Dir['lib/recipes/*.cap'].each { |recipe| load(recipe) }

set :repository,  "<EMAIL>:dmitryso/boxx.git"
set :deploy_via, :remote_cache # remote_cache | checkout
set :scm, :git
set :user, "apprunner"
set :password, ENV["<PASSWORD>"]
set :git_enable_submodules, true
set :keep_releases, 3
set :sudo, 'sudo'
set :use_sudo, false
set :rails_env, :production
set :sudo_prompt, "xxxx"
set :bundle_without, [:development, :test]
set :shared_children, fetch(:shared_children) + %w(data)
set :public_children, []
set :project, "rabotnegi"
set :application, "rabotnegi"
set :domain, "rabotnegi.ru"
set :host, "www.rabotnegi.ru"
set :deploy_to, "/app/#{application}"
set :git_host, "bitbucket.org"
set :git_key, "/users/dima/.ssh/id_main"

server host, :web, :app, :db, :primary => true

# set :default_environment, { RUBYOPT: "-Ku" }
# set :ssh_options, {keys: ["#{ENV['HOME']}/.ssh/id_rsa"]}
default_run_options[:pty] = true # required for the first time repo access to answer "yes"

set :passenger_config_path, "/etc/apache2/sites-available/#{application}"
set :logrotate_config_path, "/etc/logrotate.d/#{application}"
set :nginx_config_path, "/opt/nginx/conf/sites/#{application}"
set :cron_config_path, "/etc/cron.d/#{application}"
set :base_path, "/data"
set :backups_path, "/data/backup"
set :backups_bucket, "rabotnegi_backups"
set :logs_path, "/data/log"
set :tmp_path, "/data/tmp"
set :app_log_path, "#{logs_path}/#{project}.log"
set :database, "rabotnegi_prod"
set :local_database, "rabotnegi_dev"
set :collections_to_restore, "vacancies"
set :admin_email, "<EMAIL>"

# deploy
#   update
#     update_code
#       strategy.deploy!
#       deploy:assets:symlink
#       finalize_update (symlink shared log/pid/system dirs)
        after "deploy:finalize_update", "deploy:update_custom_symlinks"
#       bundle:install
#     deploy:assets:precompile
#     symlink
#   restart
after "deploy", "crontab:install"
after "deploy", "resque:restart"

cron "15,30,45", "vacancies:kill_spam"
cron "0 3",      "vacancies:cleanup"
cron "30 5",     "rabotaru:start_job"
cron "0 4,16",   "data:dump DB=#{database} DIR=#{backups_path} BUCKET=#{backups_bucket}"
cron "*/5",      "cron:ping"

task :demo do
end
<file_sep>/test/rabotnegi/functional/controllers_test.rb
require 'test_helper'

describe "Controllers" do
  test "default locale" do
    gets "/tests/noop"
    I18n.locale.must_equal :ru
  end

  test "error notifications" do
    Gore::Err.delete_all
    
    assert_raises ArgumentError do
      gets "/tests/error"
    end
    
    Gore::Err.count.must_eq 1
    assert_emails 1
  end
  
  test "sitemap" do
    get "/sitemap.xml"    
    response.must_be :ok?
    response.body.must_match "/vacancies/msk/office"
    response.body.must_match "/vacancies/spb/it"
  end
  
  describe "current user" do
    setup do
      @user_1 = User.create!(ip: "2.2.2.1")
      @user_2 = User.create!(agent: "test browser", ip: "2.2.2.2")
      @user_3 = User.create!(ip: "2.2.2.3")
    end

    it "find the user when a valid user_id cookie is provided" do
      set_cookie "uid=#{URI.encode_www_form_component app.message_encryptor.encrypt_and_sign(@user_1.id)}"
      gets "/tests/noop"
      app.last_instance.current_user.must_equal @user_1
    end

    it "find the user by the user agent and ip address" do
      gets "/tests/noop", {}, {"REMOTE_ADDR" => "2.2.2.2", "HTTP_USER_AGENT" => "test browser"}
      app.last_instance.current_user.must_eq @user_2
    end
    
    it "create a new one when can't find an existing" do
      gets "/tests/noop", {}, {"REMOTE_ADDR" => "3.3.3.3", "HTTP_USER_AGENT" => "another browser"}
      current_user = app.last_instance.current_user

      current_user.wont_be_nil
      current_user.wont_be :in?, [@user_1, @user_2, @user_3]
      current_user.ip.must_eq "3.3.3.3"
      current_user.agent.must_eq "another browser"
    end    
  end
end
<file_sep>/test/rabotnegi/functional/helpers_test.rb
require 'test_helper'

describe "Helpers" do
  test "web_id" do
    vacancy = make Vacancy
    get "/tests/noop"
    
    assert_equal "v-#{vacancy.id}", helpers.web_id(vacancy)
    assert_equal "v-#{vacancy.id}-details", helpers.web_id(vacancy, :details)
  end
end
<file_sep>/lib/ext/log_helpers.rb
# # __w(customer.name, "Customer") => /// Customer = "Joe"
# # __w(customer.name) => /// "Joe"
# def __w(object, comment = nil)
#   comment &&= "#{comment} = "
#   inspection = PP.pp(object, StringIO.new, 100).string rescue object.inspect
#   Rails.logger.debug("/// #{comment}#{inspection}")
# end
# 
# # __d(customer.name, binding) => /// customer.name = "Joe"
# # __d(@customer.name) => /// @customer.name = "Joe"
# def __d(expression, binding = nil)
#   value = eval(expression.to_s, binding)
#   __w(value, expression.to_s)
# end
# 
def __p(*messages)
  puts "/// #{messages.pluck(:inspect).join(', ')}"
end

def __pl(label, data)
  puts "/// #{label} = #{data.inspect}"
end

# def __pl(*args)
#   location = Gore.reformat_caller(caller.first)
#   puts "/// #{location} -- #{args.inspect}"
# end
# 
# def __l(*args)
#   text = args.length == 1 && String === args.first ? args.first : args.map(&:inspect).join(", ")
#   Log.trace "/// #{text}"
# end
# 
# def __ll(*args)
#   location = Gore.reformat_caller(caller.first)
#   Log.trace "/// #{location} -- #{args.map(&:inspect).join(", ")}"
# end

def __t(*args)
  source = caller.first
  source.sub!(Padrino.root, "")
  source.sub!(%r{`block \(\d+ levels\) in <top \(required\)>'}, "block")
  puts "  \e[31mTRACE\e[0m - #{source} - #{args.pluck(:inspect).join(', ')}" 
end      
<file_sep>/test/test.rake
require 'rake/testtask'

namespace :test do
  %w(lib rabotnegi/unit rabotnegi/functional).each do |folder|
    Rake::TestTask.new("#{folder.gsub('/', ':')}") do |test|
      test.libs << "test"
      test.pattern = "test/#{folder}/**/*_test.rb"
      test.verbose = true
    end
  end

  Rake::TestTask.new("all") do |test|
    test.libs << "test"
    test.test_files = FileList[
      'test/rabotnegi/unit/**/*_test.rb',
      'test/rabotnegi/functional/**/*_test.rb',
      'test/lib/**/*_test.rb', 
    ]  
    test.verbose = true
  end

  Rake::TestTask.new("ui") do |t|
    t.libs << "test"
    t.pattern = 'test/rabotnegi/ui/**/*_test.rb'
    t.verbose = true
  end
  
  task "unit" => "test:rabotnegi:unit"
  task "func" => "test:rabotnegi:functional"  
end

task "test" => "test:all"
<file_sep>/app/models/user.rb
class User < Gore::ApplicationModel
  field :industry
  field :city  
  field :agent
  field :ip
  field :queries, type: Array
  field :favorite_vacancies, type: Array, default: []
  
  def favorite_vacancy_objects
    Vacancy.find(favorite_vacancies)
  end
  
  def to_s
    "User@#{ip}"
  end
end
<file_sep>/test/rabotnegi/unit/salary_test.rb
require 'test_helper'

unit_test Salary do
  setup do
    @between_1000_2000 = Salary.make(:min => 1000, :max => 2000)
    @above_1000 = Salary.make(:min => 1000)
    @below_1000 = Salary.make(:max => 1000)
    @exactly_1000 = Salary.make(:exact => 1000)
  end
  
  test "create with one attribute" do
    actual = Salary.make(:exact => 1000)
		expected = Salary.make; expected.exact = 1000
		assert_equal expected, actual
  end
  
  test "create with many attributes" do
    actual = Salary.make(:min => 1000, :negotiable => false, :currency => :usd)
    expected = Salary.make; expected.min = 1000; expected.currency = :usd; expected.negotiable = false
		assert_equal expected, actual    
  end
  
  test "parse exact value" do
    assert_equal @exactly_1000, Salary.parse('1000')
  end
  
  test "parse max value" do
    assert_equal @below_1000, Salary.parse('<1000')
    assert_equal @below_1000, Salary.parse('до 1000')
  end
  
  test "parse min value" do
    assert_equal @above_1000, Salary.parse('от 1000')
    assert_equal @above_1000, Salary.parse('1000+')
  end
  
  test "parse range" do
    assert_equal @between_1000_2000, Salary.parse('1000-2000')
  end

  test "parse with bad input" do
    assert_equal Salary.new, Salary.parse('тыщща')
  end
  
  test "to_s" do
    assert_equal '1000—2000 р.', @between_1000_2000.to_s
  end
  
  test "==" do
    assert_equal Salary.make(:exact => 1000, :currency => :usd), Salary.make(:exact => 1000, :currency => :usd)
  end
  
  test "convert_currency" do
    salary = Salary.make(:exact => 1000, :currency => :usd)
    salary.convert_currency(:rub)
    
    assert_equal 31_000, salary.exact
    assert_equal :rub, salary.currency
  end
  
  test "text=" do
    salary = Salary.make(exact: 1000)
    salary.text = "от 5000"
    assert_equal Salary.make(min: 5000), salary
  end
end
<file_sep>/test/lib/event_log_test.rb
require "test_helper"

describe Gore::EventLog do
  setup do
    Gore::EventLog::Item.delete_all
  end
  
  test "write string" do
    Gore::EventLog.write("Source1", "event1", :warn, "message1")
    assert Gore::EventLog::Item.where(source: "Source1", event: "event1", data: "message1", severity: "warn").exists?
  end
  
  test "write array" do
    Gore::EventLog.write("Source", "test", :warn, [:an, "array"])
    assert Gore::EventLog::Item.where(source: "Source", event: "test", data: [:an, "array"], severity: "warn").exists?
    assert Gore::EventLog::Item.where(source: "Source", event: "test", data: ["an", "array"], severity: "warn").exists?    
  end
  
  test "write hash" do
    Gore::EventLog.write("Source", "test", :warn, a: "hash", number: 1, symbol: :hello)
    assert Gore::EventLog::Item.where(source: "Source", event: "test", data: {a: "hash", number: 1, symbol: :hello}, severity: "warn").exists?
    assert Gore::EventLog::Item.where(source: "Source", event: "test", data: {"a" => "hash", "number" => 1, "symbol" => "hello"}, severity: "warn").exists?    
  end
  
  test "shorthand write methods" do
    assert_respond_to Gore::EventLog, :debug
    assert_respond_to Gore::EventLog, :info
    assert_respond_to Gore::EventLog, :warn
    assert_respond_to Gore::EventLog, :error

    Gore::EventLog.error("Source2", "event2", "message2")
    assert Gore::EventLog::Item.where(source: "Source2", event: "event2", data: "message2", severity: "error").exists?    
  end
  
  test "write with extra data" do
    Gore::EventLog.write("S", "e", :warn, {d1: 11, d2: 22}, {e1: 11, e2: 22})
    assert Gore::EventLog::Item.where(source: "S", event: "e", data: {d1: 11, d2: 22}, extra: {e1: 11, e2: 22}, severity: "warn").exists?
  end

  test "nil data and nil extra are not stored" do
    item = Gore::EventLog.write("Src", "evnt", :info)
    item.reload

    assert_equal nil, item.data
    assert_equal nil, item.extra    
    refute item.attributes.include?("data")
    refute item.attributes.include?("extra")
  end

  test ":info severity is not stored" do
    item = Gore::EventLog.write("Src", "evnt", :info)
    item.reload

    assert_equal :info, item.severity
    refute item.attributes.include?("severity")
  end
  
  test "events with low severity are not logged" do
    begin
      old_log_level = Log.level
      Log.level = 2 # warn
      
      Gore::EventLog.write("Src", "debug-message", :debug)
      Gore::EventLog.write("Src", "info-message", :info)
      Gore::EventLog.write("Src", "warn-message", :warn)
      Gore::EventLog.write("Src", "error-message", :error)      
      
      assert Gore::EventLog::Item.where(source: "Src", event: "warn-message").exists?
      assert Gore::EventLog::Item.where(source: "Src", event: "error-message").exists?
      refute Gore::EventLog::Item.where(source: "Src", event: "debug-message").exists?
      refute Gore::EventLog::Item.where(source: "Src", event: "info-message").exists?
    ensure
      Log.level = old_log_level.to_i
    end
  end
end

describe Gore::EventLog::Item do
  test "time" do
    item = Gore::EventLog::Item.create!(source: "Src", event: "evnt")
    assert_equal item.time, item.created_at
  end
  
  test "updated_at is not stored" do
    item = Gore::EventLog::Item.create!(source: "Src", event: "evnt")
    item.reload

    refute_nil item.created_at
    assert_nil item.updated_at
    refute item.attributes.include?("updated_at")
  end
  
end

describe Gore::EventLog::Accessor do
  setup do
    Gore::EventLog::Item.delete_all
  end
  
  test "log attributes" do
    assert_respond_to Vacancy, :log
    assert_respond_to Vacancy.new, :log

    assert_instance_of Gore::EventLog::Writer, Vacancy.log
    assert_instance_of Gore::EventLog::Writer, Vacancy.new.log

    assert_equal "vacancy", Vacancy.log.source
    assert_equal "vacancy", Vacancy.new.log.source
    assert_equal "user", User.log.source
    assert_equal "user", User.new.log.source
    assert_equal "rabotaru_job", Rabotaru::Job.log.source
  end
  
  test "log attributes methods" do
    Vacancy.log.write("some_vacancy_event", :warn, "class message")
    assert Gore::EventLog::Item.where(source: "vacancy", event: "some_vacancy_event", data: "class message", severity: "warn").exists?

    Vacancy.new.log.warn("some_vacancy_event", "instance message")
    assert Gore::EventLog::Item.where(source: "vacancy", event: "some_vacancy_event", data: "instance message", severity: "warn").exists?
  end  
end

describe Gore::EventLog::Writer do
  setup do
    Gore::EventLog::Item.delete_all
  end
  
  test "for_class" do
    assert_equal "user", Gore::EventLog::Writer.for_class(User).source
    assert_equal "rabotaru_job", Gore::EventLog::Writer.for_class(Rabotaru::Job).source
  end
  
  test "write" do
    log = Gore::EventLog::Writer.new("TestSource")

    log.write("event0", :warn, "details0")
    assert Gore::EventLog::Item.where(source: "TestSource", event: "event0", data: "details0").exists?

    log.warn("event1", "details1")
    assert Gore::EventLog::Item.where(source: "TestSource", event: "event1", data: "details1", severity: "warn").exists?
  end
end
<file_sep>/test/rabotnegi/unit/init_test.rb
require 'test_helper'

unit_test "Domain initialization" do
  test "industry data loading" do
    assert Industry.all.size > 10
    assert Industry.popular.size > 5
    assert Industry.other.size > 5    
  end
  
  test "city data loading" do
    assert_equal 5, City.all.size
  end
end<file_sep>/app/models/industry.rb
class Industry < Struct.new(:code, :external_id, :name, :group)
  cattr_reader :all, :popular, :other
  alias key code

  def to_s
    name
  end
  
  def self.[](code)
    if Array === code
      code = code.map(&:to_sym)
      all.select { |x| x.code.in?(code) }
    else
      all.detect { |x| x.code == code.to_sym }
    end
  end
  singleton_class.send(:alias_method, :get, :[])
  
  def self.each(&block)
    @@all.each(&block)
  end
  
  def self.find_by_external_id(external_id)
    external_id = external_id.to_i
    industry = @@all.find { |industry| industry.external_id == external_id } || raise(ArgumentError, "Отрасль ##{external_id} не найдена")
    industry.code
  end

  def self.find_by_external_ids(*external_ids)
    external_ids.each do |eid|
      return find_by_external_id(eid) rescue next
    end
  end
  
  def self.key_matches?(key)
    key = key.to_s
    key.blank? || all.any? { |obj| obj.key_str == key }
  end
  
  def key_str
    @key_str ||= key.to_s
  end
  
  def log_key
    key
  end
    
  @@all = [
     Industry.new(:it,                      19, 'Информационные технологии',    :popular),
     Industry.new(:finance,                  2, 'Бухгалтерия и финансы',        :popular),
     Industry.new(:transportation,          83, 'Транспорт',                    :popular),
     Industry.new(:logistics,               78, 'Логистика',                    :popular),
     Industry.new(:service,                 92, 'Обслуживающий персонал',       :popular),
     Industry.new(:wholesale,               75, 'Оптовая торговля',             :popular),
     Industry.new(:manufactoring,           22, 'Производство',                 :popular),
     Industry.new(:restaurant,              85, 'Рестораны и питание',          :popular),
     Industry.new(:retail,                  60, 'Розничная торговля',           :popular),
     Industry.new(:office,                  14, 'Делопроизводство',             :popular),
     Industry.new(:building,                37, 'Строительство и архитектура',  :popular),
     Industry.new(:hr,                      12, 'Кадровые службы',              :other),                      
     Industry.new(:marketing,               49, 'Маркетинг, реклама, PR',       :other),  
     Industry.new(:medicine,                90, 'Медицина, фармация',           :other),  
     Industry.new(:realty,                 107, 'Недвижимость',                 :other),  
     Industry.new(:sales,                 1011, 'Продажа услуг',                :other),  
     Industry.new(:publishing,              55, 'Издательство, полиграфия',     :other),  
     Industry.new(:insurance,              109, 'Страхование',                  :other),  
     Industry.new(:telecom,                 94, 'Телекоммуникации',             :other),  
     Industry.new(:executives,             751, 'Топ-менеджмент',               :other),  
     Industry.new(:hospitality,            111, 'Туризм, гостиничное дело',     :other),  
     Industry.new(:telework,              1006, 'Удаленная работа',             :other),  
     Industry.new(:householding,            46, 'Эксплуатация зданий',          :other),  
     Industry.new(:law,                      9, 'Юриспруденция',                :other)                          
  ].sort_by(&:name)

  @@popular, @@other = @@all.partition { |industry| industry.group == :popular }

  def self.q
    query = Object.new
    class << query
      def method_missing(selector, *args)
        Industry[selector.to_sym]
      end
    end
    query
  end
end
<file_sep>/script/test_console.rb

__END__
sudo tail /var/log/syslog

sudo ln -sf /opt/ruby/bin/* /usr/bin
find /usr/bin -lname /opt/ruby/bin/\*
sudo sh -c 'mv /usr/bin/ruby /usr/bin/ruby_en && cp /usr/bin/ruby_ru /usr/bin/ruby'

sudo cat > /usr/bin/ruby_ru
  #!/bin/sh
  export RUBYOPT="-Ku"
  exec "/usr/bin/ruby" "$@"

sudo /opt/nginx/sbin/nginx -s stop
sudo ruby-build 1.9.3-p125 /opt/ruby

cd /app/rabotnegi/current
bundle exec rake -T

q cd app
q cd shared
q cd log
q conf nginx
q conf bash
q log app
q log syslog
q log cron.out
q log cron.err
<file_sep>/lib/ext/mongoid.rb
module MongoidPagination
  extend ActiveSupport::Concern

  included do
    def self.paginate(page_num, page_size = 10)
      criteria = page(page_num, page_size)
      results = criteria.to_a
      results.extend(MongoidPagination::Collection)
      results.criteria = criteria
      results.total_count = criteria.count
      results
    end

    scope :page, proc { |page_num, page_size = 10| limit(page_size).offset(page_size * ([page_num.to_i, 1].max - 1)) } do
      include MongoidPagination::Collection
    end
  end

  module Collection
    def criteria
      @criteria || self
    end

    def criteria=(object)
      @criteria = object
    end

    def limit_value
      criteria.options[:limit]
    end

    def offset_value
      criteria.options[:skip]
    end

    def total_count
      @total_count ||= criteria.count
    end
    
    def total_count=(value)
      @total_count = value
    end

    def per(num)
      if (n = num.to_i) <= 0
        self
      else
        limit(n).offset(offset_value / limit_value * n)
      end
    end

    # Total number of pages
    def num_pages
      (total_count.to_f / limit_value).ceil
    end

    alias total_pages num_pages

    # Current page number
    def current_page
      (offset_value / limit_value) + 1
    end

    # First page of the collection ?
    def first_page?
      current_page == 1
    end

    # Last page of the collection?
    def last_page?
      current_page >= num_pages
    end    

    # current_page - 1 or nil if there is no previous page
    def previous_page
      current_page > 1 ? (current_page - 1) : nil
    end

    # current_page + 1 or nil if there is no next page
    def next_page
      current_page < total_pages ? (current_page + 1) : nil
    end    
  end
end

module Mongoid::Document
  def store(attributes = {})
    update_attributes!(attributes)
  end  

  def mark(state, other_attributes = {})
    self[self.class._state_attr] = state
    self["#{state}_at"] = Time.now
    update_attributes!(other_attributes)
  end
  
  def update_if_stored!(attributes = {})
    self.attributes = attributes
    save! unless new?
  end  
    
  module ClassMethods
    def get(id)
      find(id)
    end
    
    def def_state_predicates(storage, *states)
      super
      states.each { |state| field "#{state}_at", type: Time }    
    end 
    
    def no_update_tracking
      class_eval <<-ruby
        def set_updated_at      
        end      
      ruby
    end
  end
  
  include MongoidPagination
end
<file_sep>/test/lib/mongo_reflector_test.rb
require "test_helper"

describe Gore::MongoReflector do
  test "real definitions" do
    vacancy = Gore::MongoReflector.metadata_for('vacancies')
  
    assert_equal 'vacancy', vacancy.singular
    assert_equal 'vacancies', vacancy.plural
    assert_equal 'vacancies', vacancy.key
    assert_equal true, vacancy.searchable?
    assert_equal Vacancy, vacancy.klass

    # log_item = Gore::MongoReflector.metadata_for('log_items')
    #   
    # assert_equal 'mongo_log_item', log_item.singular
    # assert_equal 'mongo_log_items', log_item.plural
    # assert_equal 'log_items', log_item.key
    # assert_equal true, log_item.searchable?
    # assert_equal MongoLog::Item, log_item.klass
  end
end

describe Gore::MongoReflector::Builder do
  dummy = temp_class Gore::ApplicationModel do
    field :name
    field :email    
  end
  
  test "list" do
    collection_1 = Gore::MongoReflector::Builder.new.desc(dummy) do
      list :id, [:name, :link], [:email, trim: 20], [:url, :link, trim: 30]
    end
    fields_1 = collection_1.list_fields.index_by { |field| field.name.to_sym }
  
    collection_2 = Gore::MongoReflector::Builder.new.desc(dummy) do
      list id: _, name: :link, email: {trim: 20}, url: [:link, trim: 30]
    end
    fields_2 = collection_2.list_fields.index_by { |field| field.name.to_sym }
  
    [fields_1, fields_2].each do |fields|
      assert_equal 'id', fields[:id].name
      assert_equal 'name', fields[:name].name
      assert_equal :link, fields[:name].format
      assert_equal 'email', fields[:email].name
      assert_equal nil, fields[:email].format
      assert_equal 20, fields[:email].trim
      assert_equal 'url', fields[:url].name
      assert_equal :link, fields[:url].format
      assert_equal 30, fields[:url].trim
    end
  end  
  
  test "list options" do
    collection = Gore::MongoReflector::Builder.new.desc(dummy) do
      list_order :name
      list_page_size 33
      actions update: false
    end
    
    assert_equal :name, collection.list_order
    assert_equal 33, collection.list_page_size
    assert_equal false, collection.actions[:update]    
    assert_equal nil, collection.actions[:delete]    
  end
  
  test "list_css_classes" do
    collection = Gore::MongoReflector::Builder.new.desc(dummy) do
      list_css_classes { |x| {joe: x.name == 'Joe'} }
    end
  
    assert_equal Hash[joe: true], collection.list_css_classes.(dummy.new(name: "Joe"))
    assert_equal Hash[joe: false], collection.list_css_classes.(dummy.new(name: "Bob"))
  end
  
  test "view_subcollection" do
    collection = Gore::MongoReflector::Builder.new.desc(dummy) do
      view_subcollection :loadings, 'rabotaru_loadings'
    end
    
    assert_equal :loadings, collection.view_subcollections.first.accessor
    assert_equal 'rabotaru_loadings', collection.view_subcollections.first.key
    assert_equal Gore::MongoReflector.metadata_for('rabotaru_loadings'), collection.view_subcollections.first.collection
  end
  
  test "edit" do
    collection = Gore::MongoReflector::Builder.new.desc(dummy) do
      edit title: 'text', city_name: ['combo', City.all], created_at: 'date_time'
    end
  
    fields = collection.edit_fields.index_by { |field| field.name.to_sym }
    assert_equal 'title', fields[:title].name
    assert_equal 'text', fields[:title].format
    assert_equal 'combo', fields[:city_name].format
    assert_equal [City.all], fields[:city_name].args
    assert_equal 'date_time', fields[:created_at].format
  end
end
<file_sep>/Rakefile
require 'bundler/setup'
require 'padrino-core/cli/rake'
require 'thor'

require File.dirname(__FILE__) + '/config/boot.rb'

PadrinoTasks.use(:mongoid)
PadrinoTasks.init

require 'resque/tasks'
require 'rake/sprocketstask'

namespace :data do
  # prod: rake data:dump DB=rabotnegi_prod DIR=/data/backup BUCKET=rabotnegi_backups
  # dev:  rake data:dump DB=rabotnegi_dev DIR=tmp BUCKET=rabotnegi_backups
  #       scp rba:/data/backup/rabotnegi_prod-latest.tbz ~/desktop
  task :dump do
    db, dir, bucket = ENV.values_at('DB', 'DIR', 'BUCKET')
    id = Time.now.strftime("%Y%m%d_%H%M%S")
    name = "#{db}-#{id}"
    
    sh "mongodump -d #{db} -o tmp/#{name}"
    sh "tar -C tmp -cj #{name} > #{dir}/#{name}.tbz"
    sh "rm -rf tmp/#{name} #{dir}/#{db}-latest.tbz"
    sh "ln -s #{name}.tbz #{dir}/#{db}-latest.tbz"
    
    if bucket.present?
      if `which gsutil`.present?
        sh "gsutil cp #{dir}/#{name}.tbz gs://#{bucket}"
      else
        raise "Boto is not there" unless File.exist?("/data/etc/boto.conf")
        raise "GSUtil is not there" unless File.exist?("/data/gsutil/gsutil")
        
        sh "env BOTO_CONFIG=/data/etc/boto.conf /data/gsutil/gsutil cp #{dir}/#{name}.tbz gs://#{bucket}"
      end
    end
  end
  
  # eg. rake data:upload FILE=/u/backup/dump-20120101-120000.tbz BUCKET=/rabotnegi-backup  
  task :upload do
    file, bucket = ENV.values_at['FILE', 'BUCKET']
    sh "BOTO_CONFIG=/data/etc/boto.conf /data/gsutil/gsutil cp #{file} gs://#{bucket}"
  end

  task :clone do
    source, target = ENV.values_at('SRC', 'DST')
    sh "rm -rf tmp/#{source}"
    sh "mongodump -d #{source} -o tmp"
    sh "mongorestore -d #{target} --drop tmp/#{source}"
    sh "rm -rf tmp/#{source}"
  end
end

namespace :dev do
  task :restore do
    src = ENV['SRC'] || "tmp/db.rabotnegi.dev"
    sh "mongorestore -d rabotnegi_dev --drop #{src}"
  end  
  
  task :rm do
    targets = %w(/public/rabotnegi/assets/* /tmp/cache/*).map { |f| Gore.root.join(f) }
    system "rm -rf #{targets.join(' ')}"
  end  
end

task "cron:ping" => :environment do
  Padrino.logger.info "Cron ping: event.count=#{Gore::EventLog::Item.count}"
  Gore.enqueue Gore::Debug, :say, "Resque ping (scheduled at #{Time.now})"
end

task "resque:setup" => :environment do
  ENV['QUEUE'] = '*'
end

Rake::SprocketsTask.new do |t|
  t.environment = Rabotnegi.assets
  t.output = "./public/rabotnegi/assets"
  t.assets = %w( vendor.js application.js application.css admin.js admin.css ) + Dir.glob(Padrino.root("app/assets/images/*")).map { |path| path.gsub("#{Padrino.root}/app/assets/images/", '') }
end

task "assets:precompile" => "assets" do
  `cp app/assets/images/* public/rabotnegi/assets`
end
<file_sep>/app/models/vacancy_cleaner.rb
module VacancyCleaner
  extend self
  include Gore::EventLog::Accessor

  UNESCAPES = {"&quot;" => '"', "&laquo;" => '«', "&raquo;" => '»', "&ndash;" => '–', "&mdash;" => '—', "&#039;" => "'"}
  TOTAL_SANITIZER_OPTIONS = {}
  SANITIZER_OPTIONS = {
    elements:  %w(div strong em b i ul ol li p h3 h4 br hr h5),
    attributes: { all: %w(id class style) }
  }

  def clean_all(loaded_after = nil)
    collection = loaded_after ? Vacancy.where(:loaded_at.gte => loaded_after) : Vacancy.all
    collection = collection.where(cleaned_at: nil)

    log.info __method__, count: collection.count, threshold: loaded_after

    collection.each { |vacancy| VacancyCleaner.clean(vacancy) }
  end

  def clean(vacancy)
    original_data = {title: vacancy.title, employer_name: vacancy.employer_name, description: vacancy.description}
    store_original_data(vacancy, original_data)
    
    vacancy.title = clean_title(vacancy.title)
    vacancy.employer_name = clean_employer_name(vacancy.employer_name)
    vacancy.description = clean_description(vacancy.description)
    vacancy.cleaned_at = Time.now
    
    vacancy.save!

  rescue => e
    Gore::Err.register("VacancyCleaner.clean", e, params: {vacancy_id: vacancy.id})
  end

  def clean_title(title)
    result = (title || "").dup.mb_chars
    result.gsub!(/&[#\w]+;/) { |match| UNESCAPES[match] || match }
    result = result.downcase if result.upcase == result
    result[0] = result[0].upcase if result.first.downcase == result.first
    result = Sanitize.clean(result, TOTAL_SANITIZER_OPTIONS)
    result.to_s
  end
  
  def clean_employer_name(employer_name)
    result = (employer_name || "").dup.mb_chars
    result.gsub!(/&[#\w]+;/) { |match| UNESCAPES[match] || match }
    result = Sanitize.clean(result, TOTAL_SANITIZER_OPTIONS)
    result.to_s
  end

  def clean_description(description)
    desc = (description || "").dup.mb_chars
    
    desc.gsub! /\n/, '<br />' if desc !~ /<\w+/
    
    desc.squish!
    desc.gsub! /&nbsp;/, ' '
    desc.gsub! /&sbquo;/, ','
    desc.gsub! /&quot;/, '"'

    desc.gsub! %r{(\w+>)\s+(</?\w)}, '\1 \2' # tag>    </?tag => tag> </tag
    desc.gsub! %r{(<br />\s*)+}, '<br>' # <br />  <br /> => <br>
    desc.gsub! %r{(<strong>)+}, '<strong>'
    desc.gsub! %r{(</strong>)+}, '</strong>'
    desc.gsub! %r{<strong>(Требования|Условия|Обязанности):?</strong>}, '<h4>\1</h4>'
    desc.gsub! %r{</h4>:}, '</h4>'
    desc.gsub! %r{<strong><br></strong>}, ''
    desc.gsub! %r{<strong>}, ''
    desc.gsub! %r{</strong>}, ''

    desc.gsub! %r{(</(h4|ul)+>)<br>}, '\1'
    desc.gsub! %r{([^<>]*)<br>}, '<p>\1</p>'
    desc.gsub! %r{<br>(<(h4|ul)+)}, '\1'
    desc.gsub! %r{&bull;(.*?)(?=&bull;|<|\Z)}, '<p>• \1</p>'
    desc.gsub! %r{<p>\s*(-|\*|·)\s*}, '<p>• '
    desc.gsub! %r{</ul>\s*<ul>}, ''    
    desc.gsub! %r{<li>\s*-\s*}, '<li>'
    
    desc.gsub! %r{<li>\s*</li>}, ''    
    desc.gsub! %r{<ul>\s*</ul>}, ''
    desc.gsub! %r{<ul><ul>}, '<ul>'
    desc.gsub! %r{</ul></ul>}, '</ul>'
    desc.gsub! %r{<p>[\s-]*</p>}, ''
    desc.squish!
      
    desc = Sanitize.clean(desc, SANITIZER_OPTIONS)
    
    desc.to_s
  end    
    
  private
    
  def store_original_data(vacancy, data)
    working_dir = Rabotnegi.config.original_vacancies_data_dir.join(vacancy.created_at.strftime("%Y%m"))
    working_dir.mkpath
    File.write(working_dir.join("#{vacancy.id}.json"), JSON.generate(data))
  end
end
<file_sep>/app/models/resume.rb
class Resume < Gore::ApplicationModel
  field :id, type: Integer
  field :fname
  field :lname
  field :password
  field :city
  field :job_title
  field :industry
  field :min_salary, type: Integer
  field :view_count, type: Integer, default: 0
  field :job_reqs
  field :about_me
  field :contact_info

  validates_presence_of :fname, :lname, :city, :job_title, :industry, :contact_info
  validates_numericality_of :min_salary

  def name
    "#{fname} #{lname}".squish
  end
    
  def to_s
    "#{name} — #{job_title} (от #{min_salary} р.)"
  end
    
  def self.search(params)
    params = params.symbolize_keys
    params.assert_valid_keys(:city, :industry, :salary, :keywords)
    query = Regexp.new(params[:keywords] || "")    

    scope = self
    scope = scope.where(city: params[:city]) if params[:city].present?
    scope = scope.where(industry: params[:industry]) if params[:industry].present?
    scope = scope.where(job_title: query) if params[:keywords].present?
    
    if params[:salary].present?
      direction, value = params[:salary].match(/(-?)(\d+)/).captures
      op = direction == '-' ? :lte : :gte
      scope = scope.where(:min_salary.send(op) => value)
    end
    
    scope
  end
  
  instance_eval { alias query search }

  def self.authenticate(name, password)
    name =~ /(\w+)\s+(\w+)/ || raise(ArgumentError, "Имя имеет неправильный формат")
    first, last = $1, $2
    resume = where(lname: last, fname: first).first || raise(ArgumentError, "Резюме «#{name}» не найдено")
    resume.password == password || raise(ArgumentError, "Неправильный пароль")
    resume
  end
end
<file_sep>/lib/gore/sort_expressions.rb
module Gore::SortExpressions
  module_function
  
  # generates a sort parameter like "date" or "-date"
  # reverses (adds a "-") when the pararmeter is already used for sorting
  def encode_order(field, param, reverse_by_default = false)
    param.blank??
      reverse_by_default ? "-#{field}" : field.to_s :
      param == field.to_s ?
        "-#{field}" :
        field.to_s
  end
  
  # "date"  => [:date, false]
  # "-date" => [:date, true]
  def decode_order_to_array(param)
    param.present??
      param.starts_with?('-') ? 
        [param.from(1), true] : 
        [param, false] :
      [nil, false]
  end  

  # "date"  => "date asc"
  # "-date" => "date desc"
  def decode_order_to_expr(param)
    field, reverse = decode_order_to_array(param)
    modifier = reverse ? :desc : :asc
    field.nil? ? nil : "#{field} #{modifier}"
  end

  # "date"  => ["date", Mongo::ASCENDING]
  # "-date" => ["date", Mongo::DESCENDING]
  def decode_order_to_mongo(param)
    field, reverse = decode_order_to_array(param)
    [[field, reverse ? Mongo::DESCENDING : Mongo::ASCENDING]]
  end
end
<file_sep>/app/models/tasks.rb
module Tasks
  extend self
  include Gore::EventLog::Accessor
  
  def kill_spam
    bad_emails = %w(gmail.com anyerp.com).join('|')
    bad_ips = %w(172.16.58.3 192.168.3.11)
    
    filter = [ { employer_name: Regexp.new( "@(#{bad_emails})$" ) }, { poster_ip: bad_ips } ]
    vacancies = Vacancy.any_of(filter)
    vacancies.delete_all
          
    # Counter.inc "antispam.runs"
    # Counter.inc "antispam.removed_vacancies", vacancies.count
    
    log.info __method__, removed: vacancies.count    
  end
  
end
<file_sep>/lib/gore/view_helpers.rb
# depends on: element
# some functions depend on: localize, link_to, url, current_url, truncate, number_with_precision
module Gore::ViewHelpers
  module Common
    # Works like content_tag. But:
    #   * :klass option can be used as :class
    #   * last argument is treated like a :class
    def element(name, *args, &block)
      options = args.extract_options!
    
      css_class = options.delete(:klass) if options.include?(:klass)
      css_class = args.pop if args.last.is_a?(String) && (args.length == 2 || args.length == 1 && block)

      options[:class] = css_class if css_class.present?
      content_tag(name, args.first, options, &block)
    end  
  
    %w(div p b).each do |tag|
      class_eval <<-ruby
        def #{tag}(*args, &block)
          element(:#{tag}, *args, &block)
        end
      ruby
    end

    def br
      tag(:br)
    end

    # Fast cycling helper
    def xcycle(*values)
      @xcycle_counter ||= -1
      @xcycle_counter += 1
      values[@xcycle_counter.modulo(values.length)]
    end
    
    def centering_table(&proc)
      "<table class='centered'><tr><td>#{capture(&proc)}</table>".html_safe
    end    
  
    def lorem
      "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
      Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure 
      dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
      proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    end
  
    def output(block, result)
      block_is_template?(block) ? concat_content(result) : result
    end
  
  end
  
  module Inspection
    def hash_as_lines(hash)
      hash.map { |key, val| "#{key}: #{val}" }.join("\n")
    end
  
    def hash_as_dl(hash)
      return element(:p, "—") if hash.blank?
    
      element :dl do
        hash.map { |key, val| element(:dt, key) + element(:dd, val) }.join.html_safe
      end
    end

    def hash_view(hash)
      return "" if hash.blank?
      element :table, "hash" do
        hash.map do |key, value|
          element(:tr) do
            title = key.to_s.titleize.gsub(' ', '-')
            element(:th, title) + element(:td, Gore.inspect_value(value))
          end        
        end.join.html_safe
      end
    end
  
    def hash_of_lines(hash)
      hash = hash.dup
      hash.each_key do |k|
        hash[k] = Gore.inspect_value(hash[k])
      end
      hash_view(hash)
    end
  
    def array_view(array)
      element :ul, "array" do
        array.map do |item|
          element(:li, item.to_s)
        end.join.html_safe
      end    
    end

    def array_inline(array)
      Gore.inspect_array(array)
    end
    
    def inline_multi_view(obj)
      case obj
        when Array then array_inline(obj)
        when Hash then Gore.inspect_hash(obj)
        when String then obj
      end
    end
  
    def multi_view(obj)
      case obj
        when Array then array_view(obj)
        when Hash then hash_view(obj)
        when String then obj
      end
    end
    
    # options: compact, trim
    def inspect_value(value, options = {})
      return "" if value.blank?

      case value
        when Time then options[:compact] ? l(value.localtime, format: :short) : l(value.localtime)
        when Integer then number(value)
        when BSON::ObjectId then options[:compact] ? short_object_id(value) : value
        when Symbol then value.to_s.humanize
        when String then trim(value, options[:trim])
        else trim(value, options[:trim])
      end
    end    
  end
  
  module Admin
    # <input type="search" id="q"> <input type="submit" value="Поиск">
    def search_tag
      text_field = text_field_tag(:q, value: params[:q], type: "search", id: 'q', :class => "search", autofocus: true)
      text_field + " " + submit_tag("Поиск")
    end

    # Render either a list of items with pager, either "no data" message.
    def listing(collection, &block)
      html = if collection.any?
        element(:table, "listing", &block).to_s + pagination(collection).to_s
      else
        element :div, "Ничего не найдено.", "no-data-message"
      end
      html
    end    

    def required_mark(options = {})
      content_tag :span, '(обязательное поле)', {:class => 'required-mark'}.update(options)
    end 

    def edit_icon
      image_tag 'edit.gif', :title => 'Редактировать', :alt => 'Редактировать'
    end
  
    def delete_icon
      image_tag 'delete.gif', :title => 'Удалить', :alt => 'Удалить'
    end
    
    Colors = %w(000 666 8f4bcf 519618 8f0d0d 387aad ab882e 8f88cf 4a7558 3aa c400b7 00f e10c02 800000 808000 008000 000080 800080 F0F 408099 FF8000 008080)

    def color_code(item)
      @color_codes ||= {}
      @color_codes[item] ||= Colors[@color_codes.size.remainder(Colors.length)]
      color = @color_codes[item]
      element :span, item, :class => "color", style: "background-color: ##{color}"
    end
    
  end
  
  module Formatting
    def trim(value, length = nil)
      length ? truncate(value.to_s, length: length, separator: ' ') : value.to_s
    end
        
    def stringify(obj)
      obj.to_s
    end

    def time(seconds)
      min = seconds.to_i / 60
      sec = seconds.to_i - min * 60
      "%02i:%02i" % [min, sec]
    end

    # 05:200
    # 02:30:200  
    def sec_usec(seconds)
      "%.3f" % [seconds] if seconds
    end
  
    def datetime(time)
      time.localtime.strftime("%d.%m.%Y %H:%M:%S %Z")
    end

    def datetime_full(time)
      time.localtime.strftime("%d.%m.%Y %H:%M")
    end
  
    def date_full(time)
      time.localtime.strftime("%d.%m.%Y")    
    end

    def number(value)
      number_with_delimiter(value)
    end

    def limited_number(value, threshold)
      value < threshold ? value : element(:span, value, :class => "extreme-number")
    end

    def short_object_id(value)
      "#{value.to_s.first(8)}-#{value.to_s.last(4)}"
    end
  end
  
  module Editing
    def submit_section(label)
      element :div, 'submit' do
        tag(:input, type: 'submit', value: label, :class => 'action-button') +
        element(:span, (" или " + element(:a, 'Отменить', 'ui', href: request.referer)), 'cancel')
      end
    end
  
    def errors_for(object, options = {})
      return '' if object.errors.empty?
        
      header_message = options.delete(:header_message) || translate("errors.system.header")
      error_messages = object.errors.map do |attr, message|
        translate("errors.#{object.class.model_name.plural}.#{attr}", :default => message)
      end

      partial "shared/errors", locals: {header: header_message, errors: error_messages}
    end   

    def trb(label, content = nil, options = {}, &block)
      if block_given?
        options = content || {}
        content = capture(&block)
      end
      
      options.assert_valid_keys(:required, :id, :before, :after, :comment, :class)
      label = [label]
      content = [content]
    
      row_id = options[:rid]
      row_id = row_id.join('_') if row_id.is_a?(Array)
    
      row_options = {}
      row_options[:id] = "#{row_id}_row" if row_id
      row_options[:class] = options[:class].present?? [options[:class]] : []
      row_options[:class] << "required" if options[:required]
      row_options[:class] << "high" if options[:high]
      row_options[:class] = row_options[:class].any?? row_options[:class].join(' ') : nil
        
      content.unshift options[:before] if options[:before]
      content.push content_tag(:span, '(обязательное поле)', :class => 'required-mark') if options[:required]
      content.push options[:after] if options[:after]
      content.push tag(:br) + content_tag(:small, options[:comment]) if options[:comment]

      element :tr, row_options do
        element(:th, label.join(' ').html_safe) +
        element(:td, content.join(' ').html_safe) + 
        element(:td, "", :class => "other")
      end
    end
  
    def trs(content, row_options = {})
      content_tag :tr, row_options do
        content_tag :td, content.html_safe, :colspan => 2
      end
    end
  
    def tr1(first, options = {})
      trb(first, nil, options)
    end

    def tr2(last, options = {})
      trb(nil, last, options)
    end
  
    def tr(name, title, content, options = {})
      label = label_tag(name, title + ':')
      trb(label, content, options)
    end
    
    def wrapper(&block)
      output block, element(:table, "form-layout", &block)
    end    
  end

  module Layout
    # sets the page title, id, class
    def page(id, title = nil, options = {})
      @page_id = id
      @page_title = title
      @page_class = options[:class] || options[:klass]
      @page_class = nil if @page_class.blank?

      if options[:path]
        options.merge! :tab => options[:path].first, :navbar => options[:path].second, :navlink => options[:path].third
      end

      @current_tab = "#{options[:tab]}-tab" if options[:tab]
      @current_navbar = "#{options[:navbar]}-nav-bar" if options[:navbar]
      @current_navlink = "#{options[:navlink]}-link" if options[:navlink]
    
      if @current_navbar == "casual-employers-nav-bar" && session[:employer_id]
        @current_navbar = "pro-employers-nav-bar"
      end
    end
  
  	def meta(name, content)
    	@meta_properties ||= {}
    	@meta_properties[name] = content
  	end
    
    # (nil) => "Работнеги.ру"
    # ("Вакансии") => "Вакансии - Работнеги.ру"
    # ("Строители", "Вакансии") => "Строители - Вакансии - Работнеги.ру"
    # ("Строители", " ", "Вакансии") => "Строители - Вакансии - Работнеги.ру"
  	def document_title
  	  [@page_title, "Работнеги.ру"].flatten.reject(&:blank?).join(' - ')
  	end
  end

  module Collections
  	def found_objects_info(collection, word, e1, e2, e5)
			object = Gore::RussianInflector.inflect(collection.total_pages <= 1 ? collection.size : collection.total_count, word, e1, e2, e5)

  		"Найдено #{b collection.total_count} #{object}. 
       Показаны #{b(collection.offset_value + 1)} — #{b(collection.offset_value + collection.limit_value)}".html_safe
  	end
	  
    def sorting_state_class_for(field)
      current_field, _ = Gore::SortExpressions.decode_order_to_array(params[:sort])
      field.to_s == current_field ? "sorted" : ""
    end    
    
    def pagination(collection, options = {})
      return "" unless collection.respond_to?(:total_pages) && collection.total_pages > 1
      element :div, "pager" do
        pagination_links(collection, options)
      end 
    end

    def pagination_links(collection, options = {})
      links_model = pagination_links_model(collection)
  
      links = links_model.map do |key|
        case
        when key == collection.current_page
          element :em, key
        when key.is_a?(Numeric)
          link_to key, current_url(page: key), :class => "num"
        when key == :gap
          element :span, '&hellip;', "gap"
        end
      end.join(' ')

      if collection.first_page?
        links.insert 0, element(:span, "&larr; предыдущая станица", "stub")
      else
        links.insert 0, element(:span, "&larr;", "dir") + link_to("предыдущая станица", current_url(page: collection.previous_page))
      end

      if collection.last_page?
        links << element(:span, "следующая страница &rarr;", "stub")
      else
        links << link_to("следующая страница", current_url(page: collection.next_page))
        links << element(:span, "&rarr;", "dir")
      end    
  
      element :div, links, "pagination"
    end

    def pagination_links_model(collection)
      links = []
  
      total_pages = collection.total_pages
      current_page = collection.current_page
      inner_window = 2
      outer_window = 0

      window_from = current_page - inner_window
      window_to = current_page + inner_window
  
      if window_to > total_pages
        window_from -= window_to - total_pages
        window_to = total_pages
      end
  
      if window_from < 1
        window_to += 1 - window_from
        window_from = 1
        window_to = total_pages if window_to > total_pages
      end
  
      middle = window_from..window_to

      # left window
      if outer_window + 3 < middle.first # there's a gap
        left = (1..(outer_window + 1)).to_a
        left << :gap
      else # runs into visible pages
        left = 1...middle.first
      end

      # right window
      if total_pages - outer_window - 2 > middle.last # again, gap
        right = ((total_pages - outer_window)..total_pages).to_a
        right.unshift :gap
      else # runs into visible pages
        right = (middle.last + 1)..total_pages
      end
  
      links = left.to_a + middle.to_a + right.to_a
    end    
  end

  class FormBuilder < Padrino::Helpers::FormBuilder::StandardFormBuilder
    def text_block(attr, caption, options = {})
      control_block_for(:text_field, attr, caption, options)
    end

    def text_area_block(attr, caption, options = {})
      control_block_for(:text_area, attr, caption, options)
    end

    def select_block(attr, caption, options = {})
      control_block_for(:select, attr, caption, options)
    end
    
    def submit_block(caption)
      template.tr2 template.submit_section(caption)
    end

    def captcha_block
      if template.captcha_valid?
        template.tr2 template.captcha_section
      else
        label = template.label_tag("Защитный код", for: "captcha_text")
        template.trb label, template.captcha_section, comment: "Введите 4 латинские буквы которые паказаны на картинке.", :class => "captcha"        
      end      
    end

    private
    
    def control_block_for(control_name, attr, caption, options)
      block_options = options.extract!(:required, :before, :after, :comment)
      label = label(attr, caption: caption + ':')
      control = send(control_name, attr, options)
      template.trb(label, control, block_options)
    end
  end
end
<file_sep>/test/rabotnegi/ui/vacancies_test.rb
require 'test_helper'

ui_test "Vacancies" do
  describe "search" do
    test "link to search page" do
      visit "/"
      click_link "Поиск вакансий"
      assert_equal "/vacancies", current_path
    end
  
    test "search page" do
      visit "/vacancies"
      in_content do
        assert_has_select "Город"
        assert_has_select "Отрасль"
        assert_has_field "Ключевые слова"
      end
    end
  
    test "search by city/industry" do
      visit "/vacancies"
    
      pick 'Город', 'Санкт-Петербург'
      pick 'Отрасль', 'Информационные технологии'
      click_button "Найти"
  
      in_content do
        assert_has_contents "Designer", "Ruby Developer", "Apple"
        assert_has_no_contents "Главбух", "Рога и Копыта" # other industry/city
        assert_has_no_contents "Msk Co" # other city      
      end
    end
  
    test "search with query" do
      visit "/vacancies"
        
      pick 'Город', 'Санкт-Петербург'
      pick 'Отрасль', 'Информационные технологии'
      fill 'Ключевые слова', 'Java'
      click_button "Найти"
  
      in_content do
        assert_has_contents "JavaScript Developer"
        assert_has_no_contents "Ruby Developer"
      end
    end
  
    test "search without results" do
      visit "/vacancies"    
    
      pick 'Город', 'Екатеринбург'
      pick 'Отрасль', 'Информационные технологии'
      click_button "Найти"
  
      in_content do
        assert_has_content "Информации о вакансиях в выбранном городе/отрасли сейчас нет"
      end  
    end 
  end
  
  describe "search results" do
    test "view search result" do
      vacancy = Vacancy.where(title: "JavaScript Developer", city: 'spb').first
      vacancy_block = "#v-#{vacancy.id}-details .entry-box"
        
      visit "/vacancies"
  
      pick 'Город', 'Санкт-Петербург'
      fill 'Ключевые слова', 'javascript'
      click_button "Найти"
  
      click_link "JavaScript Developer"
      assert_has_selector vacancy_block, visible: true, text: "Do some JS and frontend stuff"
  
      click_link "JavaScript Developer"
      assert_has_selector vacancy_block, visible: false
      
      within(vacancy_block) { visit_link("Открыть вакансию на отдельной странице") }
    
      assert_has_selector 'h2', text: "JavaScript Developer"
      assert_title_like "JavaScript Developer"
    end   
  end

  describe "posting" do
    test "post" do
      title = "Негr ##{Gore.time_stamp}"
      
      visit "/vacancies/new"
      
      fill "Должность", title
      fill "Работодатель", "СтройНам"
      pick "Город", "Москва"        
      pick "Отрасль", "Строительство и архитектура"        
      fill "Зарплата", "30000"
      fill "Описание", "ла-ла-ла-ла надо нам е-щёёё бабла"
      
      click_button "Опубликовать"
    
      assert_has_content "Вакансия опубликована"
    
      vacancy = Vacancy.asc(:created_at).last
      vacancy.title.must_equal title 
      vacancy.city.must_equal "msk"
      vacancy.industry.must_equal "building"
      vacancy.employer_name.must_equal "СтройНам"
      vacancy.description.must_equal "ла-ла-ла-ла надо нам е-щёёё бабла"
      vacancy.salary.must_equal Salary.make(exact: 30_000)
      
      assert_title_like title
      assert_has_content title
      assert_has_content "ла-ла-ла-ла надо нам е-щёёё бабла"
    end
    
    test "post invalid data" do
      title = "Негr ##{Gore.time_stamp}"
      
      visit "/vacancies/new"
      click_button "Опубликовать"
      
      assert_has_content "Вы что-то неправильно заполнили"
      within(".errors") { assert_has_content "Должность" }
    end   
  end

  describe "favorites" do
    test "favorites" do
      record = Vacancy.where(title: "JavaScript Developer", city: 'spb').first
      row = "#v-#{record.id}"
      
      visit "/vacancies"
      User.last.update_attributes(favorite_vacancies: [])
    
      pick 'Город', 'Санкт-Петербург'
      pick 'Отрасль', 'Информационные технологии'
      click_button "Найти"
      
      assert_equal [], User.last.favorite_vacancies

      assert_has_no_class find(row + " .star"), "star-enabled"
      find(row + " .star").click
      
      skip "seems we can't click spans here"
      assert_has_class find(row + " .star"), "star-enabled"
      
      assert_equal [record.id], User.last.favorite_vacancies
      
      visit "/vacancies/favorite"
      in_content do
        assert_has_selector "tr.entry-header", count: 1
        assert_has_content "JavaScript Developer"
      end
    end    
  end
end
<file_sep>/lib/ext/core.rb
class Object
  def _class
    self.class
  end
  
  def is?(*types)
    types.any? { |type| self.is_a?(type) }
  end

  # joe.send_chain 'name.last' #=> 'Smith'
  def send_chain(expression)
    expression = expression.to_s
    return self if expression.blank?
    return self.send(expression) unless expression.include?(".")
    
    expression.split('.').inject(self) { |result, method| result.send(method) }
  end
  
  def assign_attributes(attributes)
    attributes.each_pair { |k,v| send("#{k}=", v) if respond_to?("#{k}=") } if attributes
  end  

  def assign_attributes!(attributes)
    attributes.each_pair { |k,v| send("#{k}=", v) } if attributes
  end  
end

module Enumerable
  # ary.select { |val| val.to_i == 3 }
  # ary.select_eq(to_i: 3)
  def select_eq(params = {})
    params = params.to_a
    select { |obj| params.all? { |k,v| obj.send(k) == v } }
  end

  def select_neq(params)
    params = params.to_a
    select { |obj| params.all? { |k,v| obj.send(k) != v } }
  end

  def select_in(params)
    params = params.to_a
    select { |obj| params.all? { |k,v| obj.send(k).in?(v) } }
  end

  def select_nin(params)
    params = params.to_a
    select { |obj| params.all? { |k,v| !obj.send(k).in?(v) } }
  end
  
  def detect_eq(params = {})
    params = params.to_a
    detect { |obj| params.all? { |k,v| obj.send(k) == v } }
  end

  def detect_neq(params)
    params = params.to_a
    detect { |obj| params.all? { |k,v| obj.send(k) != v } }
  end  
  
  def send_each(method, *args, &block)
    each { |obj| obj.send(method, *args, &block) }
  end
  
  def map_to_array(*attrs)
    map { |obj| attrs.inject([]) { |array, attr| array << obj.send(attr) } }
  end

  def pluck(attr)
    map { |obj| obj.send(attr) }
  end
  
  def pluck_all(attr)
    map { |args| args.map(&attr) }
  end
end

class Module
  def def_struct_ctor
    module_eval <<-RUBY
      def intialize(attributes = {})
        assign_attributes!(attributes)
        super
      end
    RUBY
  end
  
  def def_state_predicates(storage, *states)
    module_eval <<-RUBY
      def self._state_attr
        :#{storage}
      end
      
      def self._states
        [ #{states.map { |state| ":#{state}" }.join(',')} ]
      end      
    RUBY
    
    states.each do |state|
      module_eval <<-RUBY
        def #{state}?
          self.#{storage} == :#{state} || self.#{storage} == '#{state}'
        end
      RUBY
    end
  end  
end

class Hash
  def append_string(key, text)
    self[key] ||= ""
    self[key] = self[key].present?? self[key] + ' ' + text.to_s : text.to_s
  end

  def prepend_string(key, text)
    self[key] ||= ""
    self[key] = self[key].present?? text.to_s + ' ' + self[key] : text.to_s
  end
end

# class Time
#   def to_json(*args)
#     as_json.to_json
#   end
# end
# 
# class BSON::ObjectId
#   def to_json(*args)
#     as_json.to_json
#   end  
# end

class File
  def self.write(path, data = nil)
    Log.trace "Writing file #{path} (#{data.try(:size)}bytes)"
    open(path, 'w') { |file| file << data }
  end  
end
<file_sep>/script/test_vacancy_dups.rb
# clones = [].to_set
# Vacancy.all.each_with_index do |v1, i|
#   next if v1.external_id.blank?
#   next if clones.include?(v1)
# 
#   dups = Vacancy.where(external_id: v1.external_id, title: v1.title, employer_name: v1.employer_name, city: v1.city, industry: v1.industry, description: v1.description).excludes(id: v1.id).to_a
#   next if dups.empty?
# 
#   dups.unshift(v1)
#   dup_views = dups.map { |v| [v.id, v.created_at.to_s(:num), v.updated_at.to_s(:num)].join('-') }.join(' ')
#   puts "#{v1.external_id}-#{dups.size}: #{dup_views} -- #{v1.title}"
#   
#   clones.merge(dups)
# end

# clones.each do |v|
#   raise "Vacancy #{v.id}/#{v.external_id} is not a clone" unless Vacancy.where(external_id: v.external_id).excludes(id: v.id).present?
# end
# 
# puts "Removing #{clones.size} clones"
# Vacancy.delete_all(conditions: {_id: clones})


uniqs = Vacancy.all.to_a.uniq_by { |v| v.external_id }
puts "Removing #{Vacancy.count - uniqs.count} clones"
Vacancy.not_in(_id: uniqs.map(&:id)).delete_all
<file_sep>/script/test_mongo_vs_sql.rb
# coding: utf-8

RECORD_COUNT = 1000

mysql = Mysql2::Client.new(host: "localhost", username: "root", database: "rabotnegi_dev")
query = "SELECT `vacancies`.* FROM `vacancies` LIMIT #{RECORD_COUNT}"

p_ar = -> do
  results = Vacancy.limit(RECORD_COUNT).all[1]
end

p_select_rows = -> do
  Vacancy.connection.select_rows(query)[1]
end

p_select_all = -> do
  Vacancy.connection.select_all(query)[1]
end

p_mysql_2 = -> do
  results = []
  mysql.query(query).each { |x| results << x }
  results
end

Vacancy.uncached do
  Vacancy.limit(5).all

  Benchmark.bm(20) do |b|
    Gore.logger.debug "---------------------------------------"

    b.report("p_ar", &p_ar)
    b.report("p_select_rows", &p_select_rows)
    b.report("p_select_all", &p_select_all)
    b.report("p_mysql_2", &p_mysql_2)
  end
end
<file_sep>/lib/gore/testing.rb
module Gore::Testing
  module Globals
    def ui_test(name, &block)
      describe name do
        include Capybara::DSL
        include Gore::Testing::CapybaraHelpers
        
        after { Capybara.current_driver = Capybara.default_driver }

        class_eval(&block)
      end
    end  
  end
  
  module Cases
    def no_test(*args)
    end
    
    def visual_test(name, &block)
      # test(name, &block)
    end
    
    def temp_class(base = Object, &block)
      name = "TempClass#{rand(1000)}"
      Gore::Testing::Cases.const_set(name, Class.new(base, &block))
    end        
  end

  module Helpers
    def make(factory_name, *args, &block)
      factory_name = factory_name.model_name.singular if Class === factory_name
      Factory(factory_name, *args, &block)
    end
    
    def patch(*args)
      Stubber.stub(*args)
    end
    
    def sent_emails
      Mail::TestMailer.deliveries
    end
  end
  
  module RackHelpers
    def gets(*args)
      get(*args)
      assert_equal 200, last_response.status
    end
  end
  
  module Assertions
    def assert_equals(actual, expected, msg = nil)
      assert_equal(expected, actual, msg)
    end

    def assert_matches(value, pattern, msg = nil)
      assert_match(pattern, value, msg)
    end
  
    def assert_size(size, collection)
      assert_equal size, collection.size
    end
    
    def assert_blank(object)
      assert object.blank?
    end
    
    def assert_same_elements(a1, a2)
      assert_equal a1.size, a2.size
      a1.each { |x| assert a2.include?(x), "#{a2.inspect} is expected to include #{x.inspect}" }
    end
    
    def assert_emails(count)
      assert_equal count, sent_emails.count
    end
  end
  
  module CapybaraHelpers
    def assert_has_contents(*strings)
      strings.each { |string| assert_has_content(string) }
    end

    def assert_has_no_contents(*strings)
      strings.each { |string| assert_has_no_content(string) }
    end
  
    def in_content(&block)
      within '#content', &block
    end

    def pick(from, text)
      select text, from: from
    end
  
    def fill(locator, with)
      fill_in locator, with: with
    end
  
    def assert_title_like(string)
      assert_match string, find("title").text
    end
  
    def assert_has_class(target, klass)
      target = find(target) unless target.is?(Capybara::Node::Element)
      assert_match %r{\b#{klass}\b}, target[:class]
    end
  
    def assert_has_no_class(target, klass)
      target = find(target) unless target.is?(Capybara::Node::Element)
      refute_match(/\b#{klass}\b/, target[:class])
    end
  
    def visit_link(locator)
      link = find_link(locator)
      assert link, "Link [#{locator}] is not found"
      visit link['href']
    end
  
    def sop
      save_and_open_page
    end
  
    def use_rack_test
      Capybara.current_driver = :rack_test
    end    
    
    def method_missing(method, *args, &block)
      return super unless method.to_s.starts_with?("assert_")
      predicate = method.to_s.sub(/^assert_/, '') + '?'
      return super unless page.respond_to?(predicate)
      assert page.send(predicate, *args, &block), "Failure: page.#{predicate}#{args.inspect}"
    end     
  end
end

<file_sep>/test/lib/gore_test.rb
require "test_helper"

describe Gore do
  describe "helpers" do
    test '#object_id?' do
      assert Gore.object_id?(1234)
      assert Gore.object_id?("1234")
      
      assert Gore.object_id?("1234-hello")
      assert !Gore.object_id?("1234hello")
      
      assert Gore.object_id?("4f09f1975a12ae7d50000001")
      assert Gore.object_id?("4f09f1975a12ae7d50000001-hello")
      assert Gore.object_id?(BSON::ObjectId.new)
      
      assert !Gore.object_id?("msk")
      assert !Gore.object_id?("hello-1234")
      assert !Gore.object_id?("1f09f1975a1")
      assert !Gore.object_id?("")
    end
  end
  
  describe Gore::Mash do
    setup do
      @mash = Gore::Mash.new.merge!(:symbol => "a symbol", "string" => "a string")
    end
    
    test "indexer" do
      @mash[:symbol].must_equal  "a symbol"
      @mash['symbol'].must_equal "a symbol"
      @mash[:string].must_equal  "a string"
      @mash['string'].must_equal "a string"
    end

    test "slice" do
      @mash.slice(:symbol, :string).must_equal Gore::Mash.new.merge!(:symbol => "a symbol", :string => "a string")
      @mash.slice('symbol', 'string').must_equal Gore::Mash.new.merge!(:symbol => "a symbol", :string => "a string")
    end
  end
end
<file_sep>/app/models/rabotaru/loader.rb
# Loads the vacancies feed into tmp/rabotaru-DATE/CITY-INDUSTRY.rss
class Rabotaru::Loader
  FeedUrlTemplate = "http://www.rabota.ru/v3_jsonExport.html?wt=f&c=%{city}&r=%{industry}&ot=t&cu=2&p=30&d=desc&cs=t&start=0&pp=50&fv=f&rc=992&new=1&t=1&country=1&c_rus=2&c_ukr=41&c_ec=133&sm=103"
  attr_accessor :working_directory, :city, :industry
  include Gore::EventLog::Accessor

  def initialize(city, industry, job_key = Gore.date_stamp)
    @working_directory = Rabotnegi.config.rabotaru_dir.join(job_key)
    @working_directory = Rabotnegi.config.rabotaru_dir.join(job_key)
    @city = City.get(city)
    @industry = Industry.get(industry)
  end

  def load
    working_directory.mkpath
    feed_file = working_directory.join("#{city.key}-#{industry.key}.json")
    return if feed_file.size?

    feed_url = FeedUrlTemplate % {city: city.external_id, industry: industry.external_id}
    feed_json = Http.get(feed_url)
    File.write(feed_file, feed_json)
    log.info 'feed_loaded', [city.key, industry.key, feed_json.size]
  end  
end
<file_sep>/app/models/rabotaru/job.rb
module Rabotaru
  class Job < Gore::ApplicationModel
    field :id, type: String, default: ->{ Time.now.strftime("%Y%m%d-%H%M%S-%6N")  }
    field :state, type: Symbol, default: 'pending'
    field :cities, type: Array
    field :industries, type: Array
    field :run_count, type: Integer
    field :error, type: String
    field :results, type: Hash
    embeds_many :loadings, class_name: 'Rabotaru::Loading'
    def_state_predicates 'state', :pending, :started, :failed, :loaded, :processed, :cleaned
    store_in collection: "rabotaru.jobs"
    
    attr_accessor :period, :queue, :current

    before_create do 
      self.cities ||= City.all.pluck(:key)
      self.industries ||= Industry.all.pluck(:key)
    end

    def run
      @period ||= Rabotnegi.config.rabotaru_period
      @queue ||= cities.product(industries).map { |city, industry| Loading.new(city: city, industry: industry) }
      @current = queue.shift

      mark :started, run_count: run_count.to_i + 1

      trap('INT') do
        reload
        mark :failed, error: "Received INT signal" if started? || pending?
        exit
      end

      loop { done = tick; break if done }
      
    rescue => e
      Gore::Err.register("Rabotaru::Job.run", e, params: {job_id: id})
      mark :failed, error: Gore.format_error(e)
    end
    
    def rerun
      @queue = loadings_to_retry
      run
    end
    
    def postprocess
      processor = Processor.new(id)
      processor.add_event_handler(:filtered) { |data| store(results: data) }
      processor.process
      mark :processed
      
      VacancyCleaner.clean_all(started_at)
      mark :cleaned
      
    rescue => e
      Gore::Err.register("Rabotaru::Job.postprocess", e, params: {job_id: id})
      mark :failed, error: Gore.format_error(e)
    end
    
    def to_s
      "RabotaruJob(#{id})"
    end

    private

    def loadings_to_retry
      pending_items = cities.product(industries) - loadings.map_to_array(:city, :industry)
      pending_loadings = pending_items.map { |city, industry| Loading.new(city: city, industry: industry) }
      
      non_done_loadings = loadings.select_neq(state: :done)
      non_done_loadings.send_each(:mark, :pending)
      
      pending_loadings.concat(non_done_loadings)
      pending_loadings.sort_by! { |loading| loading.new? ? 1 : 0 }
      pending_loadings
    end
  
    def tick
      reload
      current.reload if current && (current.queued? || current.started?)
      log.debug 'tick', current: current.inspect, loadings_count: loadings.count
      case current.try(:state)
      when nil
        mark :loaded
        Gore.enqueue(Rabotaru::Job, :postprocess, id)
        return true
      when :pending
        loadings << current unless loadings.include?(current)
        current.queue
        wait
      when :started, :queued
        wait
        if Time.now - current.changed_at > 20.minutes
          current.mark :failed, error: "Skipped after timeout"
          @current = queue.shift
        end
      when :failed
        if loadings.select(&:failed?).count > 3
          mark :failed
          return true
        end
        @current = queue.shift
      when :done
        time_since_done = Time.now - current.done_at
        @current = queue.shift
        wait(period - time_since_done) if time_since_done < period && period - time_since_done > 1
      end
      
      false
    end

    def wait(timeout = period)
      sleep(timeout)
    end

    def self.postprocess(job_id)
      find(job_id).postprocess
    end
  end
end
<file_sep>/doc/README.txt
# To restore the development database - 
rake data:restore_dev src=tmp/db_110810_1510
mongorestore -d rabotnegi_dev --drop tmp/db_110810_1510
mongorestore -d rabotnegi_dev --drop tmp/db_110810_1510

  
# Resque
PIDFILE=./tmp/resque.pid BACKGROUND=yes QUEUE=file_serve rake environment resque:work
PIDFILE=./tmp/resque.pid BACKGROUND=yes QUEUE=* rake resque:work

 
# Deployment

mongod, redis + resque, rsyslog, nginx + passenger, cron, monit

## commands
sudo /opt/nginx/sbin/nginx -s reload

## Configs
/etc/profile — rubyopt
/etc/apache2/httpd.conf
/apps/bin/ruby — rubyopt
/opt/nginx/conf/nginx.conf
/opt/nginx/conf/sites/
/var/lib/mongodb
/etc/mongodb.conf

## Logs
/var/log/mongodb/mongodb.log
/opt/nginx/logs/error.log

## Reinstalling Passenger and Nginx
  wget http://nginx.org/download/nginx-1.0.12.tar.gz
  tar xzf nginx-1.0.12.tar.gz
  sudo passenger-install-nginx-module
    /home/apprunner/nginx-1.0.12
    --with-http_gzip_static_module

## Installing RVM
sudo bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer )
sudo usermod -a -G rvm apprunner


sudo htpasswd -b htpasswd admin 0000
<file_sep>/test/rabotnegi/unit/application_model_test.rb
require 'test_helper'

unit_test 'Gore::ApplicationModel' do
  test 'default salary' do
    vacancy = make Vacancy
    
    assert_respond_to Vacancy, :get
    assert_respond_to Employer, :get
    
    assert Vacancy.singleton_methods(false).include?(:get)
    assert Employer.singleton_methods(false).exclude?(:get)
  end
end
<file_sep>/test/rabotnegi/unit/rabotaru_job_test.rb
require 'test_helper'

unit_test Rabotaru::Job do
  test "run w/o problems" do
    Rabotaru::Loader.any_instance.stubs(:load)
    Rabotaru::Processor.any_instance.stubs(:process)

    job = Rabotaru::Job.create!(cities: %W(spb msk), industries: %w(it retail), period: 0.001)
    job.run
    job.reload

    assert_equal :cleaned, job.state
    assert_equal %w(spb msk), job.cities
    assert_equal %w(it retail), job.industries
    assert_equal 4, job.loadings.count
    assert_equal [:done] * 4, job.loadings.pluck(:state)
  end
  
  teardown do
    Rabotaru::Loader.any_instance.unstub(:load)
    Rabotaru::Processor.any_instance.unstub(:process)
  end
  
  test "loadings_to_retry" do
    job = Rabotaru::Job.create!
    job.loadings.create! city: "spb", industry: "it", state: "done"
    job.loadings.create! city: "spb", industry: "retail", state: "done"
    job.loadings.create! city: "msk", industry: "telecom", state: "done"
    job.loadings.create! city: "msk", industry: "building", state: "failed"
    job.loadings.create! city: "msk", industry: "office", state: "started"
      
    loadings_to_retry = job.send(:loadings_to_retry)
    assert loadings_to_retry.detect_eq(city: :msk, industry: :building, state: :pending)
    assert loadings_to_retry.detect_eq(city: :msk, industry: :office, state: :pending)
    assert !loadings_to_retry.detect_eq(city: :spb, industry: :it)
    assert !loadings_to_retry.detect_eq(city: :spb, industry: :retail)
    assert !loadings_to_retry.detect_eq(city: :msk, industry: :telecom)
    assert_equal City.all.count * Industry.all.count - 3, loadings_to_retry.count
  end
  
  test "loadings to retry for a job with a limited input" do
    job = Rabotaru::Job.create!(cities: [:spb, :msk], industries: [:it, :retail])
    job.loadings.create! city: "spb", industry: "it", state: "done"
    job.loadings.create! city: "spb", industry: "retail", state: "done"
    job.loadings.create! city: "msk", industry: "retail", state: "failed"
    job.reload
    
    assert_equal 2, job.send(:loadings_to_retry).count
  end
end
<file_sep>/lib/ext/datetime_formats.rb
# %a - The abbreviated weekday name (``Sun'')
# %A - The  full  weekday  name (``Sunday'')
# %b - The abbreviated month name (``Jan'')
# %B - The  full  month  name (``January'')
# %c - The preferred local date and time representation
# %d - Day of the month (01..31)
# %H - Hour of the day, 24-hour clock (00..23)
# %I - Hour of the day, 12-hour clock (01..12)
# %j - Day of the year (001..366)
# %m - Month of the year (01..12)
# %M - Minute of the hour (00..59)
# %p - Meridian indicator (``AM''  or  ``PM'')
# %S - Second of the minute (00..60)
# %U - Week  number  of the current year,
#         starting with the first Sunday as the first
#         day of the first week (00..53)
# %W - Week  number  of the current year,
#         starting with the first Monday as the first
#         day of the first week (00..53)
# %w - Day of the week (Sunday is 0, 0..6)
# %x - Preferred representation for the date alone, no time
# %X - Preferred representation for the time alone, no date
# %y - Year without a century (00..99)
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
# 
#  t = Time.now
#  t.strftime("Printed on %m/%d/%Y")   #=> "Printed on 04/09/2003"
#  t.strftime("at %I:%M%p")            #=> "at 08:56AM"

# Rails formats:
# :db           => "%Y-%m-%d %H:%M:%S",
# :number       => "%Y%m%d%H%M%S",
# :time         => "%H:%M",
# :short        => "%d %b %H:%M",
# :long         => "%B %d, %Y %H:%M",
# :long_ordinal => lambda { |time| time.strftime("%B #{ActiveSupport::Inflector.ordinalize(time.day)}, %Y %H:%M") },
# :rfc822       => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") }

# time.iso8601
# time.xmlschema

formats = {
  num: "%y%m%d_%H%M%S",
  humane: ->(val) { val.year == Time.now.year ? val.strftime("%b #{val.day} %H:%M") : val.to_s(:rus_zone) },
  short_date: "%d.%m.%Y %H:%M:%S",
  rus_full: "%d.%m.%Y %H:%M",
  rus_zone: "%d.%m.%Y %H:%M:%S %Z",
  rus_usec: ->(val) { val.strftime("%d.%m.%Y %H:%M:%S.#{'%06d' % val.usec} %Z") }
}
    
::Time::DATE_FORMATS.update(formats)
::Date::DATE_FORMATS.update(formats)
<file_sep>/app/helpers.rb
Rabotnegi.helpers do
  def inspect_field(model, field, options = {})
    value = model.send_chain(field.name) unless field.custom?
    options[:trim] = field.trim

    result = case field.format
      when :city then City.get(value)
      when :industry then Industry.get(value)
      when :pre then element(:pre, trim(value, options[:trim]))
      when String then send(field.format, value)
      when Proc then trim(field.format.(model), options[:trim])
      else inspect_value(value, options)
    end
    
    result = link_to(result, url(:admin_items, :show, collection: field.collection.key, id: model)) if field.format == :link || field.link
    
    result
  end  

  def city_options
    settings.ui_cache.city_options ||= City.all.map { |city| [city.name, city.code.to_s] }
  end

  def industry_options
    settings.ui_cache.industry_options ||= [
      ['Популярные', Industry.popular.map { |industry| [industry.name, industry.code.to_s] }],
      ['Остальные', Industry.other.map { |industry| [industry.name, industry.code.to_s] }]
    ]
  end

  def salary_options
    settings.ui_cache.salary_options ||= [
    	['до 10 000', -10000],
    	['до 20 000', -20000],
    	['до 30 000', -30000],
    	['до 40 000', -40000],
    	['до 50 000', -50000],
    	['от 10 000',  10000],
    	['от 20 000',  20000],
    	['от 30 000',  30000],
    	['от 40 000',  40000],
    	['от 50 000',  50000]
    ]
  end

  def vacancies_page_title
    if @vacancies
      city = City.get(params[:city])
      industry = Industry.get(params[:industry]) if params[:industry].present?
      query = params[:q]
      page = params[:page]
  
      content = "Вакансии — #{city.name}"
      content << " — #{industry.name}" if industry
      content << " — #{query}" if query
      content << ", стр. №#{page}" if page
      content
    else
      "Поиск вакансий"
    end
  end
  
  def back_to_all_vacancies_url_for(vacancy)
    request.referer =~ /vacancies/ ? request.referer : url(:vacancies, :index, city: vacancy.city, industry: vacancy.industry)
  end  
    
  def resumes_page_title
    if @resumes
      city = City[params[:city]] if params[:city].present?
      industry = Industry[params[:industry]] if params[:industry].present?
      query = params[:q]
      page = params[:page]
    
      content = "Резюме — #{city.name}"
      content << " — #{industry.name}" if industry
      content << " — #{query}" if query
      content << ", стр. №#{page}" if page
      content
    else
      "Поиск резюме"
    end
  end  
end
<file_sep>/test/test_helper.rb
PADRINO_ENV = ENV["X_RACK_ENV"] || ($*.to_s =~ %r{/ui/} ? "testui" : "test") unless defined?(PADRINO_ENV)
require File.expand_path('../../config/boot', __FILE__)

require "support/mocks"
require "support/factories"

require "capybara"
require "capybara/dsl"

if Gore.env.testui?
  [Vacancy, User, Gore::EventLog::Item].each(&:delete_all)
  load "test/support/data.rb"  
  puts "Seeded #{Gore.env}"
end

class MiniTest::Spec
  include Mocha::API
  include Rack::Test::Methods
  include Webrat::Matchers
  include Gore::Testing::Helpers
  include Gore::Testing::RackHelpers
  include Gore::Testing::Assertions
  extend Gore::Testing::Cases

  class << self
    alias test it
    alias setup before
    alias teardown after
  end

  alias response last_response
  
  teardown do
    Vacancy.delete_all
    User.delete_all
  end unless Gore.env.testprod? || Gore.env.testui?

  def app
    Rabotnegi.tap { |app| }
  end

  def helpers
    app.last_instance
  end

  def response_body
    last_response.body
  end  
end

module MiniTest::Expectations
  infect_an_assertion :assert_equal, :must_eq
end

include Gore::Testing::Globals
alias unit_test describe

Webrat.configure do |config|
  config.mode = :rack
end

Turn.config do |c|
  c.format = :pretty # outline pretty dotted progress marshal cue
end

Capybara.run_server = false
Capybara.app_host = "http://localhost:3002"
Capybara.server_port = 3002
Capybara.default_driver = :webkit
Capybara.javascript_driver = :webkit
<file_sep>/test/rabotnegi/unit/currency_test.rb
require 'test_helper'

unit_test Currency do
  test "convertion" do
    assert_in_delta 73, Currency.convert(100, :usd, :eur), 1
    assert_equal 1, Currency.convert(31, :rub, :usd)
  end
  
  test "convertion to the same currency" do
    assert_equal 100, Currency.convert(100, :usd, :usd)
    assert_equal 100, Currency.convert(100, :rub, :rub)
  end
end<file_sep>/test/lib/russian_inflector_test.rb
require "test_helper"

unit_test Gore::RussianInflector do
  test "parameterize" do
    assert_equal "ruby-developer", Gore::RussianInflector.parameterize("Ruby Developer")    
    assert_equal "ruby-разработчик", Gore::RussianInflector.parameterize("Ruby Разработчик")    
    assert_equal "ruby-разработчик", Gore::RussianInflector.parameterize("Ruby - Разработчик.")  
    assert_equal "торговый-представитель-20", Gore::RussianInflector.parameterize("Торговый представитель № 20")
    assert_equal "менеджер-по-продажам-промышленного-оборудования", Gore::RussianInflector.parameterize("Менеджер по продажам промышленного оборудования")
    assert_equal "бухгалтер-по-расчету-заработной-платы", Gore::RussianInflector.parameterize("Бухгалтер по расчету заработной платы")
  end
  
  test "truncate" do
    assert_equal "ruby-разработчик", "ruby-разработчик".truncate(30, separator: '-', omission: '')
    assert_equal "менеджер-по-продажам", "менеджер-по-продажам-промышленного-оборудования".truncate(30, separator: '-', omission: '')
    assert_equal "бухгалтер-по-расчету", "бухгалтер-по-расчету-заработной-платы".truncate(30, separator: '-', omission: '')

    assert_equal "ruby-разработчик", "ruby-разработчик".truncate(40, separator: '-', omission: '')
    assert_equal "менеджер-по-продажам-промышленного", "менеджер-по-продажам-промышленного-оборудования".truncate(40, separator: '-', omission: '')
    assert_equal "бухгалтер-по-расчету-заработной-платы", "бухгалтер-по-расчету-заработной-платы".truncate(40, separator: '-', omission: '')
  end
end
<file_sep>/script/test_temp.rb
Rabotaru::Loader.new(city: 'spb', industry: 'it').load

Rack::Mount::Utils.const_set(:UNSAFE_PCHAR, /[^-_.!~*'()a-zA-Zа-яА-Я\d:@&=+$,;%]/.freeze)
p Rack::Mount::Utils.escape_uri("/vacancies/here/12345-по-русски")
<file_sep>/test/lib/captcha_test.rb
require 'test_helper'

describe Gore::Captcha do
  subject { Gore::Captcha.create! }
    
  it "has a long random generated key" do
    subject.id.to_s.must_match %r<[0-9a-f]{24}>
  end
    
  it "has a random generated text" do
    subject.text.wont_be_nil
    subject.text.must_match %r<[0-9A-Z]{5}>
  end
  
  it "generates an image and writes it to a file" do
    image_path = Padrino.root("tmp/test.tempfiles/#{subject.id}.jpeg")
    Gore::Captcha.write_image_for_text("ABCD", File.open(image_path, "wb"))

    file_info = `file #{image_path}`
    file_info.must_match %r{JPEG image data}
  end
  
  test '#image_file returns a path to the tempfile with the captcha image' do
    file_path = subject.image_file
    file_info = `file #{file_path}`
    file_info.must_match %r{JPEG image data}
  end
  
  test '#valid?' do
    captcha = Gore::Captcha.create!(text: "RIGHT")
    assert Gore::Captcha.valid?(captcha.id, "right")
    assert Gore::Captcha.valid?(captcha.id, "RIGHT")
    assert !Gore::Captcha.valid?(captcha.id, "WRONG")
    assert !Gore::Captcha.valid?("4f3bd25fe999fb24ed000001", "right")
    assert !Gore::Captcha.valid?("invalid-id", "right")
  end
end

describe "Capcha Controller" do
  let(:captcha) { Gore::Captcha.create! }
  
  test "GET /captcha/:id.jpeg" do
    get "/captcha/#{captcha.id}.jpeg"
    response.status.must_equal 200
    response.content_type.must_equal "image/jpeg"
    response.content_length.must_be_close_to 2.kilobytes, 500.bytes
  end

  test "GET /captcha/:invalid-id.jpeg" do
    get "/captcha/#{BSON::ObjectId.new}.jpeg"
    response.status.must_equal 404
  end
end

describe "Captcha helpers" do
  test "GET /page-with-capthca" do
    get "/vacancies/new"
    captcha = Gore::Captcha.last
    assert_have_selector "div.captcha input[type=text][name=captcha_text]"
    assert_have_selector "div.captcha input[type=hidden][name=captcha_id][value='#{captcha.id}']"      

    get "/vacancies/new", captcha_id: captcha.id, captcha_text: "WRONG"
    captcha2 = Gore::Captcha.last
    assert captcha.id != captcha2.id
    assert_have_selector "div.captcha input[type=text][name=captcha_text]"
    assert_have_selector "div.captcha input[type=hidden][name=captcha_id][value='#{captcha2.id}']"

    get "/vacancies/new", captcha_id: captcha2.id, captcha_text: captcha2.text
    assert_have_selector "input[type=hidden][name=captcha_text][value='#{captcha2.text}']"
    assert_have_selector "input[type=hidden][name=captcha_id][value='#{captcha2.id}']"
  end
end
<file_sep>/app/controllers/controllers.rb
Rabotnegi.controllers do
  get "/" do    
    render "vacancies/index"
  end

  get "/sitemap.xml" do
    render "shared/sitemap"
  end

  get "/site/info" do
    render env: Gore.env, 
      db: Vacancy.db.name, 
      counts: { vacancies: Vacancy.count, events: Gore::EventLog::Item.count, users: User.count },
      settings: {
        static: settings.static,
        raise_errors: settings.raise_errors,
        reload_templates: settings.reload_templates,
        logging: settings.logging,
        log_level: Padrino.logger.level,
        log_path: Padrino.logger.log.respond_to?(:path) && Padrino.logger.log.path 
      }
  end

  get "/site/env" do
    Gore.env
  end
  
  # 
  # Captcha
  # 
  
  get "/captcha/:id.jpeg", name: :captcha do
    captcha = Gore::Captcha.find(params[:id]) rescue nil
    halt 404 unless captcha
    send_file captcha.image_file, type: 'image/jpeg', disposition: 'inline'
  end

  # 
  # Tests
  # 

  get "/tests/noop" do
    ""
  end

  get "/tests/error" do
    raise ArgumentError, "shit happens"
  end
  
  # 
  # Dev
  # 
  
  get "/dev" do
    render "dev/dev"
  end

  get '/dev/request' do
    render env.select { |k,v| [String, Numeric, Symbol, TrueClass, FalseClass, NilClass, Array, Hash].any? { |klass| klass === v } }
  end
  
  get "/dev/error" do
    raise "shit happens"
  end

  get "dev/typo" do
    bum_bum_shit
  end

  get "/dev/lorem" do
    render "dev/lorem", layout: false
  end  
end
<file_sep>/app/models/city.rb
class City < Struct.new(:code, :external_id, :name)
  alias key code

  def to_s
    name
  end

  def inspect
    "City(#{code}/#{external_id} #{name})"
  end

  def key_str
    @key_str ||= key.to_s
  end

  def log_key
    key
  end

  cattr_reader :all
  @@all = [
  	City.new(:msk, 1, "Москва"),
  	City.new(:spb, 2, "Санкт-Петербург"),
  	City.new(:ekb, 3, "Екатеринбург"),
  	City.new(:nn,  4, "Нижний Новгород"),
  	City.new(:nsk, 9, "Новосибирск")  
  ]

  class << self
    def [](code)
      if Array === code
        code = code.map(&:to_sym)
        all.select { |x| x.code.in?(code) }
      else
        all.detect { |x| x.code == code.to_sym }
      end
    end
    alias_method :get, :[]
  
    def each(&block)
      @@all.each(&block)
    end
  
    def find_by_external_id(external_id)
      external_id = external_id.to_i
      city = @@all.find { |city| city.external_id == external_id } || raise(ArgumentError, "Город ##{external_id} не найден")
      city.code
    end    

    def key_matches?(key)
      key = key.to_s
      key.blank? || all.any? { |obj| obj.key_str == key }
    end

    def q
      query = Object.new
      class << query
        def method_missing(selector, *args)
          City[selector.to_sym]
        end
      end
      query
    end
  end  
end

<file_sep>/lib/gore/captcha.rb
module Gore
  class Captcha
    include Mongoid::Document
    store_in collection: "sys.captchas"
    field :text, type: String, default: -> { Captcha.generate_random_text }
    field :created_at, type: DateTime, default: -> { Time.now }

    validates_presence_of :text    
    
    # convert -fill darkblue -edge 10 -background white -size 100x28 -wave 3x69 -gravity 'Center' -pointsize 22 -implode 0.2 label:ABC01 'captcha.jpg' 2>&1
    def image_file
      outfile = Tempfile.new(['gore_captcha', '.jpg'])
      outfile.binmode

      Services.write_image_for_text(text, outfile)
      File.expand_path(outfile.path)
    end
    
    module Services
      extend self
      
      def valid?(id, text)
        captcha = Captcha.find(id) rescue nil
        captcha && captcha.text == text.to_s.upcase
      end
          
      def generate_random_text(length = 5)
        Alphabet.sample(length).join
      end      
      
      # write_image File.open(Padrino.root("tmp/captcha.jpeg"), 'wb')
      def write_image_for_text(text, file)
        amplitude = 2 + rand(2)
        frequency = 50 + rand(20)

        params = ['-fill darkblue', '-edge 10', '-background white']
        params << "-size 100x28"
        params << "-wave #{amplitude}x#{frequency}"      
        params << "-gravity 'Center'"
        params << "-pointsize 22"
        params << "-implode 0.2"
        params << "label:#{text}"
        params << "'#{File.expand_path(file.path)}'"

        command = "convert #{params.join(' ')} 2>&1"
        output = `#{command}`

        raise StandardError, "Error while running #{command}: #{output}" unless $?.exitstatus == 0
      
      ensure
        file.close
      end       
    end

    extend Services
        
    Alphabet = ('A'..'Z').to_a + ('0'..'9').to_a
  end
end
<file_sep>/app/app.rb
class Rabotnegi < Padrino::Application
  register SassInitializer
  register Padrino::Rendering
  register Padrino::Mailer
  register Padrino::Helpers

  enable :sessions

  include Gore::EventLog::Accessor

  set :locale_path, %w(config/locales/ru.core.yml config/locales/ru.yml)

  set :uid_secret_token, 'dc00acaaa4039a2b9f9840f226022c62fd4b6eb7fa45ca289eb8727aba365d0f4ded23a3768c6c81ef2593da8fde51f9405aedcb71621a57a2de768042f336e5'
  set :message_encryptor, ActiveSupport::MessageEncryptor.new(uid_secret_token)

  set :default_builder, Gore::ViewHelpers::FormBuilder
  set :ui_cache, OpenStruct.new

  set :assets do
    @sprockets ||= Sprockets::Environment.new.tap do |env|
      env.append_path 'app/assets/javascripts'
      env.append_path 'app/assets/stylesheets'
      env.append_path 'app/assets/images'
      env.append_path 'public/vendor'
    end
  end

  set :delivery_method, :smtp => {
    address: "smtp.gmail.com",
    port: 587,
    user_name: "railsapp",
    password: "<PASSWORD>",
    authentication: :plain,
    enable_starttls_auto: true
  }
  
  set :config, OpenStruct.new.tap { |config|
    config.admin_login = 'admin'
    config.admin_password = '<PASSWORD>'
    config.err_max_notifications_per_hour = 2
    config.err_sender = "<EMAIL>"
    config.err_recipients = "<EMAIL>"
    config.original_vacancies_data_dir = Gore.root.join("tmp/vacancies_content")
    config.rabotaru_dir = Gore.root.join("tmp/rabotaru")
    config.rabotaru_period = 15
    config.default_queue = :main
    config.google_analytics_id = "UA-1612812-2" 
  }

  set :xhr do |truth| condition { request.xhr? } end
  set :match_id do |truth| condition { Gore.object_id?(params[:id]) } end
  set :if_params do |*args| 
    mapping = args
    condition { mapping.all? { |param, matcher| matcher.key_matches?(params[param]) } } 
  end

  Slim::Engine.set_default_options disable_escape: true, disable_capture: false
  Resque.redis.namespace = "rabotnegi:jobs"

  configure :testprod, :testui do    
    enable :logging
    disable :static
    disable :reload_templates
    disable :raise_errors
    set :delivery_method, :test
  end

  configure :development do
    register Gore::LogFilter
    set :delivery_method, :test
    set :show_exceptions, :after_handler
    
    config.rabotaru_period = 5
    
    Slim::Engine.set_default_options pretty: true
    # Resque.inline = true    
  end

  configure :test do
    enable :raise_errors
    set :delivery_method, :test
    config.rabotaru_dir = Gore.root.join("tmp/test.rabotaru")
    config.original_vacancies_data_dir = Gore.root.join("tmp/test.vacancies_content")
    Resque.inline = true
  end
  
  configure :production do
    enable :logging
    disable :static
    
    # syslog_facility = ENV["RAILS_PROC"].presence || "web" 
    # Padrino.logger = SyslogLogger.new("rab-#{syslog_facility}", Syslog::LOG_USER, Syslog::LOG_PID)
    # Padrino.log_level = :info    
  end

  helpers Gore::ControllerHelpers::Urls
  helpers Gore::ControllerHelpers::Identification
  helpers Gore::ControllerHelpers::Users
  helpers Gore::ControllerHelpers::Captchas

  helpers Gore::ViewHelpers::Common
  helpers Gore::ViewHelpers::Inspection
  helpers Gore::ViewHelpers::Admin
  helpers Gore::ViewHelpers::Formatting
  helpers Gore::ViewHelpers::Editing
  helpers Gore::ViewHelpers::Layout
  helpers Gore::ViewHelpers::Collections

  ##
  # Caching support
  #
  # register Padrino::Cache
  # enable :caching
  #
  # set :cache, Padrino::Cache::Store::Memcache.new(::Memcached.new('127.0.0.1:11211', :exception_retry_limit => 1))
  # set :cache, Padrino::Cache::Store::Memcache.new(::Dalli::Client.new('127.0.0.1:11211', :exception_retry_limit => 1))
  # set :cache, Padrino::Cache::Store::Redis.new(::Redis.new(:host => '127.0.0.1', :port => 6379, :db => 0))
  # set :cache, Padrino::Cache::Store::Memory.new(50)
  # set :cache, Padrino::Cache::Store::File.new(Padrino.root('tmp', app_name.to_s, 'cache')) # default choice
  # 


  # 
  # Filters & error handlers
  # 

  error 500..599 do
    raise env["sinatra.error"] if Gore.env.development?
    
    Gore::Err.register route.named || request.path, env["sinatra.error"],
      params: params.except("captures"),
      url: request.url, 
      verb: request.request_method,
      session: session.to_hash.except('flash'),
      flash: flash.to_hash,
      request_headers: env.select { |k,v| k.starts_with?("HTTP_") },
      response_headers: headers
        
    raise env["sinatra.error"]
  end
  
  before { settings.set :last_instance, self } if Gore.env.test?
  before { logger.info "Start #{request.path}" } if Gore.env.development?
  # before { `touch #{Padrino.root("app/app.rb")}` } if Gore.env.development?

  ##
  # Overrides
  #
  
  # Overriden to enable string/symbol access to all (path & query) parameters.
  def indifferent_hash
    Gore::Mash.new
  end  

  # Fix the totally broken view engines.
  def concat_content(content)
    content
  end

  # def block_is_template?(block)
  #   false
  # end
  
  # def form_tag(url, options={}, &block)
  #   desired_method = options[:method]
  #   data_method = options.delete(:method) if options[:method].to_s !~ /get|post/i
  #   options.reverse_merge!(:method => "post", :action => url)
  #   options[:enctype] = "multipart/form-data" if options.delete(:multipart)
  #   options["data-remote"] = "true" if options.delete(:remote)
  #   options["data-method"] = data_method if data_method
  #   options["accept-charset"] ||= "UTF-8"
  #   inner_form_html  = hidden_form_method_field(desired_method)
  #   inner_form_html += capture_html(&block).to_s
  #   content_tag(:form, inner_form_html, options)
  # end  
  
  def label_tag(name, *args, &block)
    options = args.extract_options!
    options[:caption] = args.first if args.any?
    super(name, options, &block)
  end
  
  def select_tag(name, options={})
    options[:options] = send(options[:options]) if options[:options].is_a?(Symbol)
    options[:grouped_options] = send(options[:grouped_options]) if options[:grouped_options].is_a?(Symbol)   
    super
  end

  def capture_html(*args, &block)
    block_given? && block.call(*args)
  end
       
  def dump_errors!(boom)
    return super unless %w(development testui).include?(Gore.env)
    return super unless Array === boom.backtrace

    boom.backtrace.reject! { |line| line =~ /thin|thor|eventmachine|rack|barista|http_router|tilt/ }
    boom.backtrace.map! { |line| line.gsub(Padrino.root, '') }
    super
  end

  assets.context_class.class_eval do
    def asset_path(path, options = {})
      "/assets/#{path}"
    end
  end
  
end
<file_sep>/app/models/rabotaru/converter.rb
# Convert json data to vacancy models
class Rabotaru::Converter
  include Gore::EventLog::Accessor

  def convert(hash)
    vacancy = Vacancy.new
    vacancy.title = hash['position']
    vacancy.description = hash['responsibility']['value']
    vacancy.external_id = extract_id(hash['link'])
    vacancy.employer_name = hash['employer'] && hash['employer']['value']
    vacancy.city = City.find_by_external_id(hash['city']['id']).to_s
    vacancy.industry = Industry.find_by_external_ids(
      hash['rubric_0'] && hash['rubric_0']['id'], 
      hash['rubric_1'] && hash['rubric_1']['id'], 
      hash['rubric_2'] && hash['rubric_2']['id']
    ).to_s
    vacancy.salary = convert_salary(hash['salary'])
    vacancy.created_at = Time.parse(hash['publishDate'])
    vacancy
  end

  private

  # http://www.rabota.ru/vacancy1234567.html' => 1234567
  def extract_id(link)
    %r{http://www.rabota.ru/vacancy(\d+).html} =~ link
    $1.to_i
  end

  # {"min": "27000", "max": "35000", "currency": {"value": "руб", "id": "2"}} => Salary(min: 27000, max: 35000, currency: :rub)
  # {"min": "10000, "max": "10000", "currency": {"value": "руб", "id": "2"}}
  # {"min": "27000", "currency": {"value": "руб", "id": "2"}}
  # {"agreed":"yes"}
  def convert_salary(hash)
    salary = Salary.new
    if hash['agreed'] == 'yes'
      salary.negotiable = true
    else
      salary.negotiable = false
      case 
        when hash['min'] == hash['max']
          salary.exact = hash['min'].to_i
        when hash['max'].blank?
          salary.min = hash['min'].to_i
        when hash['min'].blank?
          salary.max = hash['max'].to_i
        else
          salary.min = hash['min'].to_i
          salary.max = hash['max'].to_i
      end
      salary.currency = convert_currency(hash['currency']['value'])
      salary.convert_currency(:rub)
    end
    salary
  end

  # 'rub' => :rub
  def convert_currency(currency_name)    
    case currency_name.downcase
      when 'руб', 'rub' then :rub
      when 'usd' then :usd
      when 'eur' then :eur
      else
        log.warn 'convert_currency.unknown', currency: currency_name
        :rub
    end
  end
end
<file_sep>/lib/tasks/app.rake
require "shellwords"

namespace :app do
  task :load_demo_data do
    Gore.env = 'test'
    Rake::Task['environment'].invoke

    puts "Loading 100 vacancies into the `#{Gore.env}` database" 
    100.times { Vacancy.create title: "Test", description: "Hello", industry: "it", city: "msk" }
  end
end

namespace :rabotaru do
  task :start_job => :environment do
     Rabotaru.start_job
  end

  # Usage: rake rabotaru:load CITY=spb,msk INDUSTRY=telework
  task :load => :environment do
    $log_to_stdout = true

    options = {}
    options[:cities] = ENV['CITY'].split(',').pluck(:to_sym) if ENV['CITY'].present?
    options[:industries] = ENV['INDUSTRY'].split(',').pluck(:to_sym) if ENV['INDUSTRY'].present?

    Rabotaru.start_job(options)
  end

  task :reload => :environment do
    $log_to_stdout = true
    Rabotaru::Job.asc(:created_at).last.try(:rerun)
  end
end
  
namespace :vacancies do
  task :count => :environment do
    puts Vacancy.count
  end

  task kill_spam: :environment do
    Tasks.kill_spam
  end    
  
  task :cleanup => :environment do
    Vacancy.cleanup
  end
  
  task sanitize: :environment do
    Vacancy.all.each do |v|
      sanitizer = HTML::FullSanitizer.new
      v.title = sanitizer.sanitize(v.title)
      v.employer_name = sanitizer.sanitize(v.employer_name)
      v.save!
    end    
  end
  
  task clean: :environment do
    since = Time.parse ENV['SINCE']
    puts "Cleaning vacancies since #{since}"
    VacancyCleaner.clean_all since
  end
end  
<file_sep>/app/views/shared/sitemap.builder
xml.instruct!
xml.urlset :xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9" do
  City.each do |city|
    Industry.each do |industry|
      xml.url {
        xml.loc absolute_url(:vacancies, :index, city: city.code, industry: industry.code)
        xml.changefreq 'daily'
      }
    end
  end
end
<file_sep>/lib/gore/application_model.rb
class Gore::ApplicationModel
  def self.inherited(derived)
    derived.class_eval do
      include Mongoid::Document
      include Mongoid::Timestamps
      include Gore::EventLog::Accessor
    end
  end
end
<file_sep>/test/lib/event_handling_test.rb
require "test_helper"

unit_test Gore::EventHandling do
  service_klass = temp_class do
    include Gore::EventHandling
    attr :ivar
    
    def f1
      raise_event :hello do
        :hello_from_f1
      end
    end

    def f2
      raise_event :hello do
        :hello_from_f2
      end
    end
    
    def f3
      raise_event :hello do
        @ivar = :modified_in_f3_hello
        :hello_from_f3
      end      
    end
  end

  setup do
    @service = service_klass.new
  end

  test "basic handling" do
    hello_event_results = []
    
    @service.add_event_handler(:hello) { |data| hello_event_results << data }
    
    @service.f1
    @service.f2    

    assert hello_event_results.include?(:hello_from_f1)
    assert hello_event_results.include?(:hello_from_f2)
  end
  
  def foo(x)
    
  end
  
  test "event generator should not be invoked when there are no event handlers attached" do
    assert_nil @service.ivar
    @service.f3    
    assert_nil @service.ivar

    @service.add_event_handler(:hello) { |data| }

    @service.f3
    assert_equal :modified_in_f3_hello, @service.ivar
  end
  
  test "remove a handler" do
    handler = ->(data) { hello_event_results << data }
    
    @service.add_event_handler(:hello, &handler)
    assert_equal 1, @service.event_handlers_for(:hello).count
    
    @service.remove_event_handler(:hello, &handler)
    assert_equal 0, @service.event_handlers_for(:hello).count
  end
end
<file_sep>/doc/todo.txt
* model in a different database
* model in the namespace
* static asset served from /APP/
* static asset served by the web server
* dev-time class reload