Utilisateur:Piglobot/Code

Une page de Wikipédia, l'encyclopédie libre.

user_category.rb[modifier | modifier le code]

require 'job'

class UserCategory < Piglobot::Job
  def initialize(*args)
    super
    @name = "[[Utilisateur:Piglobot/Utilisateurs catégorisés dans main|Utilisateurs catégorisés dans main]]"
  end
  
  def process
    20.times do
      step_and_sleep
    end
  end
  
  def step_and_sleep
    step
    sleep 2
  end
  
  def step
    @done = false
    @data ||= {}
    
    if @data[:done]
      @done = true
      log("Toutes les catégories ont été traitées")
      return
    end
    
    categories = @data[:categories]
    if categories.nil?
      @data[:categories] = @wiki.all_pages("14").select { |page|
        valid_category?(page)
      }
      @data[:empty] = []
      @data[:one] = []
      
      @changed = true
    else
      category = nil
      loop do
        category = categories.shift
        break if category.nil? or category !~ /^Utilisateur/
      end
      @changed = false
      process_category(category)
      if categories.empty?
        [
          [@data[:empty], "Catégories vides"],
          [@data[:one], "Catégories avec une seule page"],
        ].each do |pages, title|
          @wiki.post("Utilisateur:Piglobot/#{title}", pages.map { |page| "* [[:#{page}]]\n" }.join, "Mise à jour")
        end
        post_user_categories(@data[:users]) if @data[:users]
        @done = true
        @data[:done] = true
      elsif categories.size % 1000 == 0
        notice("#{categories.size} catégories à traiter (dernière : [[:#{category}]])")
      end
    end
  end
  
  def valid_category?(name)
    name !~ /^Catégorie:Utilisateur/
  end
  
  def process_category(name)
    if valid_category?(name)
      process_valid_category(name)
    else
      log("Catégorie ignorée : #{name}")
    end
  end
  
  def process_valid_category(name)
    pages = @wiki.category(name.split(":", 2).last)
    log("#{pages.size} pages dans #{name}")
    if pages.size == 0
      @data[:empty] << name
    elsif pages.size == 1
      @data[:one] << name
    end
    pages.delete_if { |page| page !~ /^Utilisateur:/ and page !~ /^Discussion Utilisateur:/ }
    if pages.empty?
      log "Aucune page utilisateur dans #{name}"
    else
      log "#{pages.size} pages utilisateur dans #{name}"
      add_user_category(name, pages)
    end
  end
  
  def post_user_categories(categories)
    list_page = "Utilisateur:Piglobot/Utilisateurs catégorisés dans main"
    text = ""
    categories.sort_by { |name, pages| name.downcase }.each do |name, pages|
      text << "== [[:#{name}]] ==\n" + pages.map { |page| "* [[:#{page}]]\n" }.join + "\n"
    end
    @wiki.append(list_page, text, "Mise à jour")
    @changed = true
  end
  
  def add_user_category(name, pages)
    @data[:users] ||= {}
    @data[:users][name] = pages
  end
end

user_category_spec.rb[modifier | modifier le code]

require 'user_category'

describe UserCategory do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @job = UserCategory.new(@bot)
  end
  
  it "should step 20 times at each process" do
    @job.should_receive(:step_and_sleep).with().exactly(20).times
    @job.process
  end
  
  it "should step and sleep" do
    @job.should_receive(:step).ordered
    @job.should_receive(:sleep).ordered.with(2)
    @job.step_and_sleep
  end
  
  it "should retreive categories" do
    @wiki.should_receive(:all_pages).with("14").and_return(["foo", "bar", "baz"])
    @job.should_receive(:valid_category?).ordered.with("foo").and_return(true)
    @job.should_receive(:valid_category?).ordered.with("bar").and_return(true)
    @job.should_receive(:valid_category?).ordered.with("baz").and_return(false)
    @job.step
    @job.data.should == { :categories => ["foo", "bar"], :empty => [], :one => [] }
    @job.changed?.should == true
    @job.done?.should == false
  end
  
  it "should process next category" do
    @job.data = { :categories => ["foo", "bar"] }
    @wiki.should_not_receive(:all_pages)
    @job.should_receive(:process_category).with("foo")
    @job.step
    @job.data.should == { :categories => ["bar"] }
    @job.changed?.should == false
    @job.done?.should == false
  end
  
  it "should be done and save special categories when out of category" do
    initial_data = { :categories => ["foo"], :empty => ["foo", "bar"], :one => ["baz", "bob"], :users => { "Catégorie:cat" => ["foo", "Utilisateur:bob/panda"], "bar" => ["baz"] } }
    @job.data = initial_data.dup
    @wiki.should_not_receive(:all_pages)
    @job.should_receive(:process_category).with("foo")
    @job.should_not_receive(:notice)
    @wiki.should_receive(:post).with("Utilisateur:Piglobot/Catégories vides", "* [[:foo]]\n* [[:bar]]\n", "Mise à jour")
    @wiki.should_receive(:post).with("Utilisateur:Piglobot/Catégories avec une seule page", "* [[:baz]]\n* [[:bob]]\n", "Mise à jour")
    page = "Utilisateur:Piglobot/Utilisateurs catégorisés dans main"
    text = [
      "== [[:bar]] ==",
      "* [[:baz]]",
      "",
      "== [[:Catégorie:cat]] ==",
      "* [[:foo]]",
      "* [[:Utilisateur:bob/panda]]",
      "",
    ].map { |x| x + "\n" }.join
    
    @wiki.should_receive(:append).with(page, text, "Mise à jour")
    @job.step
    
    done_data = initial_data.dup
    done_data[:done] = true
    
    @job.data.should == done_data
    @job.done?.should == true
  end
  
  it "should do nothing on step when done" do
    @job.data = { :done => true }
    @wiki.should_not_receive(:all_pages)
    @job.should_not_receive(:process_category)
    @job.should_receive(:log).with("Toutes les catégories ont été traitées")
    @job.step
    @job.done?.should == true
  end
  
  [
    [100, false],
    [1000, true],
    [1, false],
    [9, false],
    [10, false],
    [99, false],
    [999, false],
    [1001, false],
  ].each do |count, notice|
    it "should #{notice ? '' : 'not' } notice when on #{count} categories remaining" do
      @job.data = { :categories => ["foo", "bar"] + ["baz"] * (count - 1) }
      @job.should_receive(:process_category).with("foo")
      if notice
        @job.should_receive(:notice).with("#{count} catégories à traiter (dernière : [[:foo]])")
      else
        @job.should_not_receive(:notice)
      end
      @job.step
    end
  end
    
  
  [
    "Catégorie:Utilisateur/foo",
    "Catégorie:Utilisateur bar",
    "Catégorie:Utilisateur",
  ].each do |category|
    it "should know category #{category} is invalid" do
      @job.valid_category?(category).should == false
    end
  end
  
  [
    "Catégorie:foo",
    "Catégorie:bar",
    "Catégorie:foo Utilisateur",
  ].each do |category|
    it "should know category #{category} is valid" do
      @job.valid_category?(category).should == true
    end
  end
  
  it "should not process invalid category" do
    @job.should_receive(:valid_category?).with("Catégorie:cat").and_return(false)
    @job.should_receive(:log).with("Catégorie ignorée : Catégorie:cat")
    @job.process_category("Catégorie:cat")
    @job.changed?.should == false
  end
  
  it "should process valid category" do
    @job.should_receive(:valid_category?).with("Catégorie:cat").and_return(true)
    @job.should_receive(:process_valid_category).with("Catégorie:cat")
    @job.process_category("Catégorie:cat")
  end
  
  it "should detect user pages on valid category" do
    @wiki.should_receive(:category).with("cat").and_return(["foo", "Utilisateur:foo", "bar", "Utilisateur:bob/panda", "Discussion Utilisateur:test/test"])
    @job.should_receive(:log).with("5 pages dans Catégorie:cat")
    @job.should_receive(:log).with("3 pages utilisateur dans Catégorie:cat")
    @job.should_receive(:add_user_category).with("Catégorie:cat", ["Utilisateur:foo", "Utilisateur:bob/panda", "Discussion Utilisateur:test/test"])
    @job.process_valid_category("Catégorie:cat")
  end
  
  it "should do nothing when no user page in category" do
    @wiki.should_receive(:category).with("cat").and_return(["foo", "foo Utilisateur:foo", "bar"])
    @job.should_receive(:log).with("3 pages dans Catégorie:cat")
    @job.should_receive(:log).with("Aucune page utilisateur dans Catégorie:cat")
    @job.process_valid_category("Catégorie:cat")
  end
  
  it "should create user category data" do
    @job.data = {}
    @job.add_user_category("Catégorie:cat", ["foo", "Utilisateur:bob/panda"])
    @job.data.should == { :users => { "Catégorie:cat" => ["foo", "Utilisateur:bob/panda"] } }
    @job.changed?.should == false
  end
  
  it "should append new user category" do
    @job.data = { :users => { "foo" => ["bar"] }}
    @job.add_user_category("cat", ["baz", "bob"])
    @job.data.should == { :users => { "foo" => ["bar"], "cat" => ["baz", "bob"] } }
  end
  
  it "should save empty category" do
    @job.data = {}
    @job.data[:empty] = mock("empty list")
    @job.data[:empty].should_receive(:<<).with("Catégorie:foo")
    @wiki.should_receive(:category).with("foo").and_return([])
    @job.should_receive(:log).twice
    @job.process_valid_category("Catégorie:foo")
  end

  it "should save category with one item" do
    @job.data = {}
    @job.data[:one] = mock("empty list")
    @job.data[:one].should_receive(:<<).with("Catégorie:foo")
    @wiki.should_receive(:category).with("foo").and_return(["foo"])
    @job.should_receive(:log).twice
    @job.process_valid_category("Catégorie:foo")
  end
end

change.rb[modifier | modifier le code]

require 'job'
require 'ruby-mediawiki/lib/mediawiki/minibrowser'

class Change < Piglobot::Job
  attr_accessor :raw_data, :currencies

  def initialize(*args)
    super
    @name = "{{m|Change}}"
  end

  def process
    get_raw_data
    parse_raw_data
    publish_data
  end
  
  def get_raw_data
    uri = URI.parse("http://xurrency.com/")
    browser = MediaWiki::MiniBrowser.new(uri)
    @raw_data = browser.get_content("/usd/feed")
  end
  
  def parse_raw_data
    regexp = %r{<title xml:lang="en"><!\[CDATA\[1 USD = ([\d\.]+) ([A-Z]+)\]\]></title>}
    @currencies = {}
    @raw_data.scan(regexp).each do |value, name|
      @currencies[name] = value
    end
  end
  
  def publish_data
    missing = false
    names = %w( EUR GBP JPY CAD CHF )
    hash = {}
    names.each do |name|
      value = @currencies[name]
      if value
        hash[name] = value
      else
        notice("[[Modèle:Change/#{name}]] : Aucune donnée")
        missing = true
      end
    end
    if missing
      notice("Mise à jour annulée car il manque des données")
    else
      comment = "[[Utilisateur:Piglobot/Travail#Change|Mise à jour automatique]]"
      hash.each do |name, value|
        @wiki.post("Modèle:Change/#{name}", value, comment)
      end
      now = Time.now
      monthes = %w( janvier février mars avril mai juin juillet août septembre octobre novembre décembre )
      now = "#{now.day} #{monthes[now.month - 1]} #{now.year}"
      @wiki.post("Modèle:Change/Màj", now, comment)
    end

  end
end

change_spec.rb[modifier | modifier le code]

require 'change'

describe Change do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @job = Change.new(@bot)
  end
  
  it "should process" do
    @job.should_receive(:get_raw_data).with()
    @job.should_receive(:parse_raw_data).with()
    @job.should_receive(:publish_data).with()
    @job.process
  end
  
  it "should get_raw_data" do
    url = URI.parse("http://xurrency.com/")
    @browser = mock("browser")
    MediaWiki::MiniBrowser.should_receive(:new).with(url).and_return(@browser)
    @browser.should_receive(:get_content).with("/usd/feed").and_return("content")
    @job.get_raw_data
    @job.raw_data.should == "content"
  end
  
  it "should parse_raw_data" do
    @job.raw_data = File.read("sample_currency.xhtml")
    @job.parse_raw_data
    {
      "EUR" => "0.6818",
      "GBP" => "0.4747",
      "JPY" => "113.6364",
      "CAD" => "0.9293",
      "CHF" => "1.1320",
    }.each do |name, value|
      { name => @job.currencies[name] }.should == { name => value }
    end
  end
  
  it "should publish_data" do
    @job.currencies = {
      "EUR" => "0.6818",
      "GBP" => "0.4747",
      "JPY" => "113.6364",
      "CAD" => "0.9293",
      "CHF" => "1.1320",
    }
    @job.currencies.each do |name, value|
      @wiki.should_receive(:post).with("Modèle:Change/#{name}", value, "[[Utilisateur:Piglobot/Travail#Change|Mise à jour automatique]]")
    end
    Time.should_receive(:now).with().and_return(Time.local(2007, 11, 10, 1, 51, 12))
    @wiki.should_receive(:post).with("Modèle:Change/Màj", "10 novembre 2007", "[[Utilisateur:Piglobot/Travail#Change|Mise à jour automatique]]")
    @job.publish_data
  end
  
  it "should notice when a currency is missing" do
    @job.currencies = {}
    %w( EUR GBP JPY CAD CHF ).each do |name|
      @job.should_receive(:notice).with("[[Modèle:Change/#{name}]] : Aucune donnée")
    end
    @job.should_receive(:notice).with("Mise à jour annulée car il manque des données")
    @job.publish_data
  end
  
  it "should not update when one data is missing" do
    @job.currencies = {
      "EUR" => "0.6818",
      "JPY" => "113.6364",
      "CAD" => "0.9293",
      "CHF" => "1.1320",
    }
    %w( GBP ).each do |name|
      @job.should_receive(:notice).with("[[Modèle:Change/#{name}]] : Aucune donnée")
    end
    @job.should_receive(:notice).with("Mise à jour annulée car il manque des données")
    @job.publish_data
  end
  
  it "should always be done" do
    @job.done?.should == true
  end
  
  it "should have a better name" do
    @job.name.should == "{{m|Change}}"
  end
end

infobox_rewriter.rb[modifier | modifier le code]

class Piglobot
  class InfoboxRewriter < Job
    attr_accessor :links, :infobox
  
    def done?
      false
    end
    
    def data_id
      @infobox
    end
  
    def initialize(bot, infobox, links)
      super(bot)
      @infobox = infobox
      @links = links
      @editor = Piglobot::Editor.new(bot)
    end
    
    def process
      @editor.setup(@infobox)
      data = @data
      changes = false
      infobox = @infobox
      links = @links
      
      articles = data
  
      if articles and !articles.empty?
        article = articles.shift
        if article =~ /:/
          comment = "Article ignoré car il n'est pas dans le bon espace de nom"
          text = "[[#{article}]] : #{comment}"
          Piglobot::Tools.log(text)
        else
          text = @wiki.get(article)
          begin
            @editor.current_article = article
            box = @editor.parse_infobox(text)
            if box
              result = @editor.write_infobox(box)
              if result != text
                comment = "[[Utilisateur:Piglobot/Travail##{infobox}|Correction automatique]] de l'[[Modèle:#{infobox}|#{infobox}]]"
                @wiki.post(article,
                  result,
                  comment)
                changes = true
              else
                text = "[[#{article}]] : Aucun changement nécessaire dans l'#{infobox}"
                Piglobot::Tools.log(text)
              end
            else
              @bot.notice("#{infobox} non trouvée dans l'article", article)
              changes = true
            end
          rescue => e
            @bot.notice(e.message, article)
            changes = true
          end
        end
      else
        articles = []
        links.each do |link|
          articles += @wiki.links(link)
        end
        articles.uniq!
        articles.delete_if { |name| name =~ /:/ and name !~ /::/ }
        data = articles
        @bot.notice("#{articles.size} articles à traiter pour #{infobox}")
        changes = true
      end
      @changed = changes
      @data = data
    end
  end
  
  class InfoboxSoftware < InfoboxRewriter
    def initialize(bot)
      super(bot, "Infobox Logiciel", ["Modèle:Infobox Logiciel"])
    end
  end
  
  class InfoboxProtectedArea < InfoboxRewriter
    def initialize(bot)
      super(bot,
        "Infobox Aire protégée",
        ["Modèle:Infobox Aire protégée", "Modèle:Infobox aire protégée"]
      )
    end
  end
end

infobox_rewriter_spec.rb[modifier | modifier le code]

require 'infobox_rewriter'

describe Piglobot, " on rewriter jobs" do
  [
    ["Infobox Logiciel", Piglobot::InfoboxSoftware],
    ["Infobox Aire protégée", Piglobot::InfoboxProtectedArea],
  ].each do |job, klass|
    it "should find job class for job #{job.inspect}" do
      @wiki = mock("wiki")
      Piglobot::Wiki.should_receive(:new).with().and_return(@wiki)
      @bot = Piglobot.new
      @bot.job_class(job).should == klass
    end
  end
end

module RandomTemplate
  module_function
  def random_name
    chars = ((0..25).map { |i| [?a + i, ?A + i] }.flatten.map { |c| c.chr } + [" ", "_", "-"]).flatten
    @infobox_name ||= (1..(rand(20)+1)).map { chars[rand(chars.size)] }.join
  end
end

describe Piglobot::InfoboxRewriter do
  before do
    @data = nil
    @bot = mock("bot")
    @wiki = mock("wiki")
    @editor = mock("editor")
    @bot.should_receive(:wiki).and_return(@wiki)
    Piglobot::Editor.should_receive(:new).with(@bot).and_return(@editor)
    @name = RandomTemplate.random_name
    @link = "link"
    @links = [@link]
    @rewriter = Piglobot::InfoboxRewriter.new(@bot, @name, @links)
  end
  
  it "should never be done" do
    @rewriter.done?.should == false
  end
  
  it "should get infobox links when data is empty" do
    @wiki.should_receive(:links).with(@link).and_return(["Foo", "Bar", "Baz"])
    @bot.should_receive(:notice).with("3 articles à traiter pour #@name")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == ["Foo", "Bar", "Baz"]
  end
  
  it "should get infobox multiple links when data is empty" do
    @rewriter.links = ["First", "Second"]
    @wiki.should_receive(:links).with("First").and_return(["Foo", "Bar", "Baz"])
    @wiki.should_receive(:links).with("Second").and_return(["A", "Bar", "C", "D"])
    @bot.should_receive(:notice).with("6 articles à traiter pour #@name")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.sort.should == ["Foo", "Bar", "Baz", "A", "C", "D"].sort
  end
  
  it "should send infobox links to InfoboxEditor" do
    @rewriter.data = ["Article 1", "Article 2"]
    @wiki.should_receive(:get).with("Article 1").and_return("foo")
    infobox = mock("infobox")
    @editor.should_receive(:current_article=).with("Article 1").ordered
    @editor.should_receive(:parse_infobox).with("foo").ordered.and_return(infobox)
    @editor.should_receive(:write_infobox).with(infobox).ordered.and_return("result")
    comment = "[[Utilisateur:Piglobot/Travail##@name|Correction automatique]] de l'[[Modèle:#@name|#@name]]"
    @wiki.should_receive(:post).with("Article 1", "result", comment)
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == ["Article 2"]
  end
  
  it "should not write infobox if none found" do
    @rewriter.data = ["Article 1", "Article 2"]
    @wiki.should_receive(:get).with("Article 1").and_return("foo")
    @editor.should_receive(:current_article=).with("Article 1")
    @editor.should_receive(:parse_infobox).with("foo").and_return(nil)
    @bot.should_receive(:notice).with("#@name non trouvée dans l'article", "Article 1")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == ["Article 2"]
  end
  
  it "should not write infobox if nothing changed" do
    @rewriter.data = ["Article 1", "Article 2"]
    @wiki.should_receive(:get).with("Article 1").and_return("foo")
    infobox = mock("infobox")
    @editor.should_receive(:current_article=).with("Article 1")
    @editor.should_receive(:parse_infobox).with("foo").and_return(infobox)
    @editor.should_receive(:write_infobox).with(infobox).and_return("foo")
    text = "[[Article 1]] : Aucun changement nécessaire dans l'#@name"
    Piglobot::Tools.should_receive(:log).with(text).once
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == false
    @rewriter.data.should == ["Article 2"]
  end
  
  it "should log parsing error" do
    @rewriter.data = ["Article 1", "Article 2"]
    @wiki.should_receive(:get).with("Article 1").and_return("foo")
    infobox = mock("infobox")
    @editor.should_receive(:current_article=).with("Article 1")
    @editor.should_receive(:parse_infobox).with("foo").and_raise(Piglobot::ErrorPrevention.new("error message"))
    @bot.should_receive(:notice).with("error message", "Article 1")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == ["Article 2"]
  end
  
  it "should get infobox links when list is empty" do
    @rewriter.data = []
    @wiki.should_receive(:links).with(@link).and_return(["A", "B"])
    @bot.should_receive(:notice).with("2 articles à traiter pour #@name")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == ["A", "B"]
  end
  
  it "should ignore links in namespace" do
    @rewriter.data = []
    @wiki.should_receive(:links).with(@link).and_return(["A", "B", "C:D", "E:F", "G::H", "I:J"])
    expected = ["A", "B", "G::H"]
    @bot.should_receive(:notice).with("#{expected.size} articles à traiter pour #@name")
    @editor.should_receive(:setup).with(@name)
    @rewriter.process
    @rewriter.changed?.should == true
    @rewriter.data.should == expected
  end
end

describe Piglobot::InfoboxSoftware do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).and_return(@wiki, @wiki)
    @job = Piglobot::InfoboxSoftware.new(@bot)
  end
  
  it "should have infobox" do
    @job.infobox.should == "Infobox Logiciel"
  end
  
  it "should have links" do
    @job.links.should == ["Modèle:Infobox Logiciel"]
  end
end

describe Piglobot, " on real case" do
  it "should continue Infobox Logiciel" do
    @wiki = mock("wiki")
    Piglobot::Wiki.should_receive(:new).and_return(@wiki)
    @bot = Piglobot.new
    @bot.job = "Infobox Logiciel"
    
    File.should_receive(:read).with("data.yaml").and_return({
      "Foo" => "Bar",
      "Infobox Logiciel" => ["Blender", "GNU Emacs"],
      "Infobox Aire protégée" => ["Foo"],
    }.to_yaml)
    
    @wiki.should_receive(:get).with("Blender").and_return("{{Infobox Logiciel | name = Blender }}\nBlender...")
    @wiki.should_receive(:post) do |article, content, comment|
      article.should == "Blender"
      content.should == "{{Infobox Logiciel\n| nom = Blender\n}}\nBlender..."
      comment.should =~ /Correction automatique/
      comment.should =~ /Infobox Logiciel/
    end
    file = mock("file")
    File.should_receive(:open).with("data.yaml.new", "w").and_yield(file)
    file.should_receive(:write).with({
      "Foo" => "Bar",
      "Infobox Logiciel" => ["GNU Emacs"],
      "Infobox Aire protégée" => ["Foo"],
    }.to_yaml)
    File.should_receive(:rename).with("data.yaml.new", "data.yaml")
    
    @bot.process
  end
end

editor_spec.rb[modifier | modifier le code]

=begin
    Copyright (c) 2007 by Piglop
    This file is part of Piglobot.

    Piglobot is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Piglobot is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Piglobot.  If not, see <http://www.gnu.org/licenses/>.
=end

require 'piglobot'
require 'helper'

describe Piglobot::Editor, " with default values", :shared => true do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @editor = Piglobot::Editor.new(@bot)
    @editor.bot.should == @bot
    @editor.wiki.should == @wiki
    
    @template_names = []
    @filters = []
    @template_name = nil
    @name_changes = {}
    @removable_parameters = []
  end
  
  it "should have template_names" do
    @editor.template_names.should == @template_names
  end

  it "should have filters" do
    @editor.filters.should == @filters
  end

  it "should have template_name" do
    @editor.template_name.should == @template_name
  end
  
  it "should have name_changes" do
    @editor.name_changes.should == @name_changes
  end

  it "should have removable_parameters" do
    @editor.removable_parameters.should == @removable_parameters
  end
end

describe Piglobot::Editor, " with real default values" do
  it_should_behave_like "Piglobot::Editor with default values"
end

describe Piglobot::Editor, " working on Infobox Logiciel" do
  it_should_behave_like "Piglobot::Editor with default values"
  
  before do
    @editor.setup "Infobox Logiciel"
    @template_names = [
      "Infobox Logiciel",
      "Logiciel simple",
      "Logiciel_simple",
      "Logiciel",
      "Infobox Software",
      "Infobox_Software",
    ]
    @filters = [
      :rename_parameters,
      :remove_open_source,
      :remove_almost_empty,
      :remove_firefox,
      :rewrite_dates,
    ]
    @template_name = "Infobox Logiciel"
    @name_changes = {
      "dernière_version" => "dernière version",
      "date_de_dernière_version" => "date de dernière version",
      "version_avancée" => "version avancée",
      "date_de_version_avancée" => "date de version avancée",
      "os" => "environnement",
      "site_web" => "site web",
      "name" => "nom",
      "screenshot" => "image",
      "caption" => "description",
      "developer" => "développeur",
      "latest release version" => "dernière version",
      "latest release date" => "date de dernière version",
      "latest preview version" => "dernière version avancée",
      "latest preview date" => "date de dernière version avancée",
      "latest_release_version" => "dernière version",
      "latest_release_date" => "date de dernière version",
      "latest_preview_version" => "dernière version avancée",
      "latest_preview_date" => "date de dernière version avancée",
      "platform" => "environnement",
      "operating system" => "environnement",
      "operating_system" => "environnement",
      "language" => "langue",
      "genre" => "type",
      "license" => "licence",
      "website" => "site web",
    }
  end
end

describe Piglobot::Editor, " working on Infobox Aire protégée" do
  it_should_behave_like "Piglobot::Editor with default values"
  
  before do
    @editor.setup "Infobox Aire protégée"
    @template_names = [
      "Infobox Aire protégée",
      "Infobox aire protégée",
      "Infobox_aire protégée",
    ]
    @filters = [
      :rename_parameters,
      :remove_parameters,
      :rewrite_dates,
      :rename_image_protected_area,
      :rewrite_coordinates,
      :rewrite_area,
    ]
    @template_name = "Infobox Aire protégée"
    @name_changes = {
      "name" => "nom",
      "iucn_category" => "catégorie iucn",
      "locator_x" => "localisation x",
      "locator_y" => "localisation y",
      "top_caption" => "légende image",
      "location" => "situation",
      "localisation" => "situation",
      "nearest_city" => "ville proche",
      "area" => "superficie",
      "established" => "création",
      "visitation_num" => "visiteurs",
      "visitation_year" => "visiteurs année",
      "governing_body" => "administration",
      "web_site" => "site web",
      "comments" => "remarque",
      "caption" => "légende carte",
      "base_width" => "largeur carte",
      "bot_image" => "image pied",
      "bot_caption" => "légende pied",
    }
    @removable_parameters = ["back_color", "label"]
  end
  
  it "should parse and write real case" do
    text = File.read("parc_national_des_arches.txt")
    result = File.read("parc_national_des_arches_result.txt")
    infobox = @editor.parse_infobox(text)
    infobox[:parameters].should include(["name", "Arches"])
    @editor.write_infobox(infobox).should == result
  end
  
  it "should rewrite template name" do
    box = { :before => "", :after => "", :parameters => "" }
    @editor.write_infobox(box).should == "{{Infobox Aire protégée}}"
  end
end

describe Piglobot::Editor, " parsing Infobox Logiciel" do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @editor = Piglobot::Editor.new(@bot)
    @infobox = {
      :name => "Infobox Logiciel",
      :before => "",
      :after => "",
      :parameters => [],
    }
    @editor.template_names = ["Infobox Logiciel"]
  end
  
  it "should parse empty infobox" do
    @editor.parse_infobox("{{Infobox Logiciel}}").should == @infobox
  end
  
  it "should return nil on empty string" do
    @editor.parse_infobox("").should == nil
  end
  
  it "should return nil on 'foo'" do
    @editor.parse_infobox("foo").should == nil
  end
  
  it "should return nil on '{{foo}}'" do
    @editor.parse_infobox("{{foo}}").should == nil
  end
  
  it "should return nil on '{{Infobox Logiciel}'" do
    @editor.parse_infobox("{{Infobox Logiciel}").should == nil
  end
  
  it "should return nil on 'Infobox Logiciel'" do
    @editor.parse_infobox("Infobox Logiciel").should == nil
  end
  
  it "should keep text before infobox on parsing" do
    @infobox[:before] = "text before"
    @editor.parse_infobox("text before{{Infobox Logiciel}}").should == @infobox
  end
  
  it "should keep text after infobox on parsing" do
    @infobox[:after] = "text after"
    @editor.parse_infobox("{{Infobox Logiciel}}text after").should == @infobox
  end
  
  it "should allow line breaks before and after" do
    @infobox[:before] = "\nfoo\n\nbar\n"
    @infobox[:after] = "bob\n\nmock\n\n"
    text = "#{@infobox[:before]}{{Infobox Logiciel}}#{@infobox[:after]}"
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse simple parameter" do
    text = "{{Infobox Logiciel | nom = Nom }}"
    @infobox[:parameters] = [["nom", "Nom"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse multiple parameters" do
    text = "{{Infobox Logiciel | nom = Nom | foo = bar }}"
    @infobox[:parameters] = [["nom", "Nom"], ["foo", "bar"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters on multiple lines" do
    text = "{{Infobox Logiciel\n|\n  nom = \nNom\nsuite\n | foo\n = \nbar\n\nbaz\n\n }}"
    @infobox[:parameters] = [["nom", "Nom\nsuite"], ["foo", "bar\n\nbaz"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters with pipes" do
    text = "{{Infobox Logiciel | logo = [[Image:Logo.svg|80px]] | foo = bar }}"
    @infobox[:parameters] = [["logo", "[[Image:Logo.svg|80px]]"], ["foo", "bar"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters with template" do
    text = "{{Infobox Logiciel | date = {{Date|12|janvier|2008}} | foo = bar }}"
    @infobox[:parameters] = [["date", "{{Date|12|janvier|2008}}"], ["foo", "bar"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters with ref" do
    text = "{{Infobox Logiciel | dernière version = 1.12<ref>[http://foo.com/bar ref]</ref>}}"
    @infobox[:parameters] = [["dernière version", "1.12<ref>[http://foo.com/bar ref]</ref>"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters with new lines" do
    text = "{{Infobox Logiciel | nom = foo\n\n  bar\n | foo = bar }}"
    @infobox[:parameters] = [["nom", "foo\n\n  bar"], ["foo", "bar"]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  it "should parse parameters with weird new lines" do
    text = "{{Infobox Logiciel |\nnom = foo |\nimage = |\n}}"
    @infobox[:parameters] = [["nom", "foo"], ["image", ""]]
    @editor.parse_infobox(text).should == @infobox
  end
  
  [
    ["Logiciel simple", ["Logiciel simple"]],
    ["logiciel simple", ["Logiciel simple"]],
    ["foo", ["foo", "bar"]],
    ["foo", ["bar", "foo"]],
    ["f", ["f"]],
    ["f", ["F"]],
    ["foo", ["Foo"]],
    ["Foo", ["foo"]],
  ].each do |template, template_names|
    it "should find #{template.inspect} using template_names #{template_names.inspect}" do
      @editor.template_names = template_names
      text = "{{#{template} | bob = mock }}"
      @infobox[:parameters] = [["bob", "mock"]]
      @infobox[:name] = template
      @editor.parse_infobox(text).should == @infobox
    end
  end
  
  [
    ["Logiciel Simple", ["Logiciel simple"]],
    ["foo", ["bar"]],
    ["foo", ["bar", "baz"]],
    ["foo", ["fooo"]],
    ["foo", ["fo"]],
    ["foo", ["foO"]],
    ["foO", ["foo"]],
  ].each do |template, template_names|
    it "should not find #{template.inspect} using template_names #{template_names.inspect}" do
      @editor.template_names = template_names
      text = "{{#{template} | bob = mock }}"
      @infobox[:parameters] = [["bob", "mock"]]
      @editor.parse_infobox(text).should == nil
    end
  end
  
  it "should have default template_names" do
    @editor.setup
    @editor.template_names.should == []
  end
  
  it "should parse mono.sample" do
    text = File.read("mono.sample")
    @infobox[:parameters] = [
      ["nom", "Mono"],
      ["logo", "[[Image:Mono project logo.svg|80px]]"],
      ["image", ""],
      ["description", ""],
      ["développeur", "[[Novell]]"],
      ["dernière version", "1.2.5.1"],
      ["date de dernière version", "{{Date|20|septembre|2007}}"],
      ["version avancée", ""],
      ["date de version avancée", ""],
      ["environnement", "[[Multiplate-forme]]"],
      ["langue", ""],
      ["type", ""],
      ["licence", "[[Licence Publique Générale|GPL]], [[Licence publique générale limitée GNU|LGPL]] ou [[X11]]"],
      ["site web", "[http://www.mono-project.com www.mono-project.com]"],
    ]
    @editor.parse_infobox(text)[:parameters].should == @infobox[:parameters]
  end

  it "should parse limewire.sample" do
    text = File.read("limewire.sample")
    @editor.template_names = ["Logiciel_simple"]
    box = @editor.parse_infobox(text)
    box.should_not == nil
    @editor.write_infobox(box).should_not == text
  end
  
  it "should raise an error when an html comment is over parameters (name => name)" do
    text = "{{Infobox Logiciel |\nnom = foo \n |\n <!-- image = | --> a = b\n}}"
    lambda { @editor.parse_infobox(text) }.should raise_error(Piglobot::ErrorPrevention,
      "L'infobox contient un commentaire qui dépasse un paramètre")
  end

  it "should raise an error when an html comment is over parameters (value => value)" do
    text = "{{Infobox Logiciel |\nnom = foo \n<!-- |\nimage = --> | a = b\n}}"
    lambda { @editor.parse_infobox(text) }.should raise_error(Piglobot::ErrorPrevention,
      "L'infobox contient un commentaire qui dépasse un paramètre")
  end

  it "should not raise an error when an html comment is only in value" do
    text = "{{Infobox Logiciel |\nnom= foo \n |\nimage = <!-- comment --> | <!-- a --> = b\n}}"
    lambda { @editor.parse_infobox(text) }.should_not raise_error
  end
  
  it "should raise an error when an parameter has no name" do
    text = "{{Infobox Logiciel |\nnom = foo \n |\n bar | a = b\n}}"
    lambda { @editor.parse_infobox(text) }.should raise_error(Piglobot::ErrorPrevention,
      "L'infobox contient un paramètre sans nom")
  end
end

describe Piglobot::Editor, " writing Infobox Logiciel" do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @editor = Piglobot::Editor.new(@bot)
    @infobox = {
      :before => "",
      :after => "",
      :parameters => [],
    }
    @editor.template_name = "Infobox Logiciel"
  end
  
  it "should write empty infobox" do
    @editor.write_infobox(@infobox).should == "{{Infobox Logiciel}}"
  end
  
  it "should write infobox with surrounding text" do
    @infobox[:before] = "before"
    @infobox[:after] = "after"
    @editor.write_infobox(@infobox).should == "before{{Infobox Logiciel}}after"
  end
  
  it "should write infobox with parameters" do
    @infobox[:parameters] = [["nom", "value"], ["other name", "other value"]]
    @editor.write_infobox(@infobox).should ==
      "{{Infobox Logiciel\n| nom = value\n| other name = other value\n}}"
  end
  
  it "should write infobox with new lines in parameter" do
    @infobox[:parameters] = [["nom", "first line\n  second line\nthird line"]]
    @editor.write_infobox(@infobox).should ==
      "{{Infobox Logiciel\n| nom = first line\n  second line\nthird line\n}}"
  end
  
  it "should remove [[open source]] from type" do
    params = [["type", "foo ([[open source]])"]]
    @editor.remove_open_source(params)
    params.should == [["type", "foo"]]
  end
  
  it "should remove [[open source]] and spaces from type" do
    params = [["type", "foo   ([[open source]])"]]
    @editor.remove_open_source(params)
    params.should == [["type", "foo"]]
  end
  
  [
    "?",
    "??",
    "-",
  ].each do |text|
    it "should remove values containing only #{text.inspect}" do
      params = [["foo", text], ["bar", "uh?"], ["baz", "--"]]
      @editor.remove_almost_empty(params)
      params.should == [["foo", ""], ["bar", "uh?"], ["baz", "--"]]
    end
  end
  
  it "should write unnammed parameters" do
    @infobox[:parameters] = [["foo", "foo"], ["", "bar"], [nil, "baz"]]
    @editor.write_infobox(@infobox).should ==
      "{{Infobox Logiciel\n| foo = foo\n| = bar\n| baz\n}}"
  end
  
  it "should remove values like {{{latest preview date|}}}" do
    params = [["foo", "{{{foo bar|}}}"], ["bar", "{{{bar}}}"], ["baz", "foo {{{bar|}}}"]]
    @editor.remove_almost_empty(params)
    params.should == [["foo", ""], ["bar", "{{{bar}}}"], ["baz", "foo {{{bar|}}}"]]
  end
  
  it "should remove notice about firefox screenshot" do
    params = [["image", "foo <!-- Ne pas changer la capture d'écran, sauf grand changement. Et utilisez la page d'accueil de Wikipédia pour la capture, pas la page de Firefox. Prenez une capture à une taille « normale » (de 800*600 à 1024*780), désactiver les extensions et prenez le thème par défaut. -->bar"]]
    @editor.remove_firefox(params)
    params.should == [["image", "foo bar"]]
  end
  
  it "should remove notice about firefox screenshot with newline and spaces" do
    params = [["image", "<!-- 
                             * Ne pas changer la capture d'écran, sauf grand changement.
                             * Utiliser la page d'accueil de Wikipédia pour la capture, pas la page de Firefox.
                             * Prendre une capture à une taille « normale » (de 800*600 à 1024*780).
                             * Désactiver les extensions et prendre le thème par défaut.
                             -->bar"]]
    @editor.remove_firefox(params)
    params.should == [["image", "bar"]]
  end
  
  %w(janvier février mars avril mai juin
     juillet août septembre octobre novembre décembre).map { |month|
      [month, month.capitalize]
  }.flatten.each do |month|
    emonth = month.downcase
    {
      "[[1er #{month}]] [[1998]]" => "{{date|1|#{emonth}|1998}}",
      "[[18 #{month}]] [[2005]]" => "{{date|18|#{emonth}|2005}}",
      "[[31 #{month}]] [[2036]]" => "{{date|31|#{emonth}|2036}}",
      "[[04 #{month}]] [[1950]]" => "{{date|4|#{emonth}|1950}}",
      "a[[04 #{month}]] [[1950]]" => "a[[04 #{month}]] [[1950]]",
      "[[04 #{month}]] [[1950]]b" => "[[04 #{month}]] [[1950]]b",
      "04 #{month} 1950" => "{{date|4|#{emonth}|1950}}",
      "04 #{month}? 1950" => "04 #{month}? 1950",
      "le 04 #{month} 1950" => "le 04 #{month} 1950",
      "[[04 fevrier]] [[1950]]" => "[[04 fevrier]] [[1950]]",
      "[[004 #{month}]] [[1950]]" => "[[004 #{month}]] [[1950]]",
      "[[4 #{month}]] [[19510]]" => "[[4 #{month}]] [[19510]]",
      "4 #{month} [[1951]]" => "{{date|4|#{emonth}|1951}}",
      "[[18 #{month}]], [[2005]]" => "{{date|18|#{emonth}|2005}}",
      "[[18 #{month}]] foo [[2005]]" => "[[18 #{month}]] foo [[2005]]",
      "01 [[#{month}]] [[2005]]" => "{{date|1|#{emonth}|2005}}",
      "07 [[#{month} (mois)|#{month}]] [[2005]]" => "{{date|7|#{emonth}|2005}}",
      "[[#{month}]] [[2003]]" => "[[#{month}]] [[2003]]",
      "[[#{month} (mois)|#{month}]] [[2003]]" => "[[#{month} (mois)|#{month}]] [[2003]]",
      "{{1er #{month}}} [[2007]]" => "{{date|1|#{emonth}|2007}}",
      "1{{er}} #{month} 1928" => "{{date|1|#{emonth}|1928}}",
      "{{Date|1|#{emonth}|1928}}" => "{{Date|1|#{emonth}|1928}}",
      "{{date|1|#{emonth}|1928}}" => "{{date|1|#{emonth}|1928}}",
    }.each do |text, result|
      it "should rewrite_date #{text.inspect} to #{result.inspect}" do
        @editor.rewrite_date(text).should == result
      end
    end
  end
  
  it "should apply filters" do
    params = [["foo", "bar"]]
    @editor.should_receive(:fake_filter).with(params) do |parameters|
      @editor.infobox.should == @infobox
      parameters.replace [["a", "b"]]
    end
    
    @editor.filters = [:fake_filter]
    @infobox[:parameters] = params
    @editor.write_infobox(@infobox).should ==
      "{{Infobox Logiciel\n| a = b\n}}"
  end
  
  it "should call rewrite_date with all values and replace with result" do
    params = [
      ["foo", "bar"],
      ["baz", "baz2"],
    ]
    @editor.should_receive(:rewrite_date).with("bar").and_return("1")
    @editor.should_receive(:rewrite_date).with("baz2").and_return("baz2")
    @editor.rewrite_dates(params)
    params.should == [
      ["foo", "1"],
      ["baz", "baz2"],
    ]
  end
  
  it "should rename_parameters with name_changes" do
    params = [["foo", "foo"], ["bar baz", "value"]]
    @editor.name_changes = { "foo" => "new foo", "bar baz" => "bob" }
    @editor.rename_parameters(params)
    params.should == [["new foo", "foo"], ["bob", "value"]]
  end
  
  ["infobox aire protégée", "Infobox aire protégée", "infobox_aire protégée", "Infobox_aire protégée"].each do |name|
    it "should rename image on protected_area when template is #{name}" do
      params = [["image", "map"], ["top_image", "illustration"]]
      @editor.infobox = { :name => name }
      @editor.rename_image_protected_area(params)
      params.should == [["carte", "map"], ["image", "illustration"]]
    end
  
    it "should rename image on protected_area when template is #{name}, inverted order" do
      params = [["top_image", "illustration"], ["image", "map"]]
      @editor.infobox = { :name => name }
      @editor.rename_image_protected_area(params)
      params.should == [["image", "illustration"], ["carte", "map"]]
    end
  end

  it "should not rename image on protected_area when already done" do
    params = [["carte", "map"], ["image", "illustration"]]
    @editor.infobox = { :name => "Infobox Aire protégée" }
    @editor.rename_image_protected_area(params)
    params.should == [["carte", "map"], ["image", "illustration"]]
  end

  it "should not rename image on protected_area when already done, inverted order" do
    params = [["image", "illustration"], ["carte", "map"]]
    @editor.infobox = { :name => "Infobox Aire protégée" }
    @editor.rename_image_protected_area(params)
    params.should == [["image", "illustration"], ["carte", "map"]]
  end
  
  it "should rewrite coordinates" do
    params = [
      ["foo", "bar"],
      ["lat_degrees", "1"],
      ["lat_minutes", "2"],
      ["lat_seconds", "3"],
      ["lat_direction", "S"],
      ["long_degrees", "5"],
      ["long_minutes", "6"],
      ["long_seconds", "7"],
      ["long_direction", "E"],
      ["bar", "baz"]
    ]
    @editor.rewrite_coordinates(params)
    params.should == [["foo", "bar"], ["coordonnées", "{{coord|1|2|3|S|5|6|7|E}}"], ["bar", "baz"]]
  end
  
  it "should rewrite coordinates without seconds" do
    params = [
      ["foo", "bar"],
      ["lat_degrees", "1"],
      ["lat_minutes", "2"],
      ["lat_seconds", ""],
      ["lat_direction", "N"],
      ["long_degrees", "5"],
      ["long_minutes", "6"],
      ["long_seconds", ""],
      ["long_direction", "W"],
      ["bar", "baz"]
    ]
    @editor.rewrite_coordinates(params)
    params.should == [["foo", "bar"], ["coordonnées", "{{coord|1|2|N|5|6|W}}"], ["bar", "baz"]]
  end
  
  it "should rewrite coordinates without data" do
    params = [
      ["foo", "bar"],
      ["lat_degrees", ""],
      ["lat_minutes", ""],
      ["lat_seconds", ""],
      ["lat_direction", ""],
      ["long_degrees", ""],
      ["long_minutes", ""],
      ["long_seconds", ""],
      ["long_direction", ""],
      ["bar", "baz"]
    ]
    @editor.rewrite_coordinates(params)
    params.should == [["foo", "bar"], ["coordonnées", "<!-- {{coord|...}} -->"], ["bar", "baz"]]
  end
  
  it "should not rewrite anything when no coordinates" do
    params = [
      ["alat_degrees", "1"],
      ["alat_minutes", "2"],
      ["alat_seconds", "3"],
      ["alat_direction", "4"],
      ["along_degrees", "5"],
      ["along_minutes", "6"],
      ["along_seconds", "7"],
      ["along_direction", "8"],
    ]
    @editor.rewrite_coordinates(params)
    params.should == [
      ["alat_degrees", "1"],
      ["alat_minutes", "2"],
      ["alat_seconds", "3"],
      ["alat_direction", "4"],
      ["along_degrees", "5"],
      ["along_minutes", "6"],
      ["along_seconds", "7"],
      ["along_direction", "8"],
    ]
  end
  
  [
    { "lat_direction" => "" },
    { "long_direction" => "" },
    { "lat_minutes" => "" },
    { "lat_direction" => "", "long_direction" => "" },
    { "lat_direction" => "O" },
    { "long_direction" => "O" },
    { "lat_direction" => "1" },
    { "long_direction" => "F" },
  ].each do |new_values|
    it "should notice on invalid coordinates #{new_values.inspect}" do
      params = [
        ["lat_degrees", "1"],
        ["lat_minutes", "2"],
        ["lat_seconds", "3"],
        ["lat_direction", "4"],
        ["long_degrees", "5"],
        ["long_minutes", "6"],
        ["long_seconds", "7"],
        ["long_direction", "8"],
      ]
      new_values.each do |k,v|
        params.map! do |name, value|
          if name == k
            [name, v]
          else
            [name, value]
          end
        end
      end
      old_params = YAML.load(params.to_yaml)
      @bot.should_receive(:notice).with("Coordonnées invalides")
      @editor.rewrite_coordinates(params)
      params.should == old_params
    end
  end
  
  %w( area superficie ).each do |name|

    [
      ["{{unité|4800|km|2}}", "4800"],
      ["{{unité|150|km|2}}", "150"],
      ["{{formatnum:2219799}} acres<br />{{formatnum:8983}} km²", "8983"],
      ["{{unité|761266|acres}}<br />{{unité|3081|km|2}}", "3081"],
      ["76 519 acres<br />310 km²", "310"],
      ["741,5 km²", "741.5"],
      ["35 835 acres<br />145 km²", "145"],
      ["10 878 km²", "10878"],
      ["337 598 acres<br />1 366,21 km²", "1366.21"],
      ["789 745 acres<br />3 196 km²", "3196"],
      ["{{formatnum:922561}} acres ({{formatnum:3734}} km²)", "3734"],
      ["112 511 acres<br />455 km²", "455"],
      ["163 513 ha (1,635 km²)", "1635"],
      ["{{formatnum:3400}} km²", "3400"],
      ["{{formatnum:13762}} km²", "13762"],
      ["244 km²", "244"],
      ["22 470 ha", "224.7"],
      ["590 ha", "5.9"],
      ["1 438 km<sup>2<sup>", "1438"],
      ["12345", "12345"],
      ["123.45", "123.45"],
      ["181,414 ha", "1814.14"],
      ["12.000 ha", "120"],
      ["565,69 ha", "5.66"],
      ["{{unité|5.6569|km|2}}", "5.66"],
      ["105 447 ha (cœur)", "1054.47", " (cœur)"],
      ["6.7 km² de terres", "6.7", " de terres"],
      ["497,3 km² en 2005", "497.3", " en 2005"],
      ["591 [[km²]]", "591"],
      ["{{formatnum:458}} km{{2}}", "458"],
      ["{{unité|0.12|km|2}}", "0.12"],
      ["12 ha", "0.12"],
    ].each do |value, result, extra|
      expected = "{{unité|#{result}|km|2}}#{extra}"
      it "should rewrite #{name} with #{value.inspect} to #{expected.inspect}" do
        params = [[name, value], ["foo", "bar"]]
        @editor.rewrite_area(params)
        params.should == [[name, expected], ["foo", "bar"]]
      end
    end
    
    [
      ["8,9 ha", "89000"],
      ["3 ha", "30000"],
      ["4,67 ha", "46700"],
      ["{{unité|0.0467|km|2}}", "46700"],
      ["{{unité|0.023|km|2}}", "23000"],
      ["{{unité|34567|m|2}}", "34567"],
    ].each do |value, result, extra|
      expected = "{{unité|#{result}|m|2}}#{extra}"
      it "should rewrite #{name} with #{value.inspect} to #{expected.inspect}" do
        params = [[name, value], ["foo", "bar"]]
        @editor.rewrite_area(params)
        params.should == [[name, expected], ["foo", "bar"]]
      end
    end
    
    [
      ["17 300 ha (zone centrale)<br/>16 200 ha (zone périphérique)",
       "{{unité|173.0|km|2}} (zone centrale)<br/>{{unité|162.0|km|2}} (zone périphérique)"],
      ["", "<!-- {{unité|...|km|2}} -->"],
      ["[[km²]]", "<!-- {{unité|...|km|2}} -->"],
      ["<!-- {{unité|...|km|2}} -->", "<!-- {{unité|...|km|2}} -->"],
      #["22 015 km² <br>''parc:'' 5 900 km²<br>''réserve de parc:''16 115 km²",
      # "{{unité|22015|km|2}} <br>''parc:'' {{unité|5900|km|2}}<br>''réserve de parc:''{{unité|16115|km|2}}"],
      #["{{unité|22015|km|2}} <br>''parc:'' 5 900 km²<br>''réserve de parc:''16 115 km²",
      # "{{unité|22015|km|2}} <br>''parc:'' {{unité|5900|km|2}}<br>''réserve de parc:''{{unité|16115|km|2}}"],
    ].each do |value, result|
      it "should rewrite #{name} with #{value.inspect} to #{result.inspect}" do
        params = [[name, value], ["foo", "bar"]]
        @editor.rewrite_area(params)
        params.should == [[name, result], ["foo", "bar"]]
      end
    end
    
    [
      "foo",
      "?",
      "36 m",
      "foo {{formatnum:12}} km²",
      "36 hab",
      "foo 3 ha",
    ].each do |value|
      it "should raise an ErrorPrevention on rewrite #{name} with #{value.inspect}" do
        params = [[name, value]]
        @bot.should_receive(:notice).with("Superficie non gérée : <nowiki>#{value}</nowiki>")
        @editor.rewrite_area(params)
        params.should == [[name, value]]
      end
    end
  end

  it "should have default name_changes" do
    @editor.name_changes.should == {}
  end
  
  it "should use template_name" do
    @infobox[:parameters] = [
      ["foo", "bar"],
    ]
    @editor.template_name = "foo"
    @editor.write_infobox(@infobox).should ==
      "{{foo\n| foo = bar\n}}"
  end
  
  it "should have a default template_name" do
    @editor.template_name.should == "Infobox Logiciel"
  end
  
  it "should remove parameters" do
    @editor.removable_parameters = ["foo", "baz", "bob"]
    params = [["foo", "bar"], ["bar", "baz"], ["baz", ""]]
    @editor.remove_parameters(params)
    params.should == [["bar", "baz"]]
  end
end

editor.rb[modifier | modifier le code]

=begin
    Copyright (c) 2007 by Piglop
    This file is part of Piglobot.

    Piglobot is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Piglobot is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Piglobot.  If not, see <http://www.gnu.org/licenses/>.
=end

class Piglobot::Editor
  attr_accessor :name_changes, :template_names, :template_name, :filters, :removable_parameters

  attr_accessor :infobox, :bot, :wiki, :current_article

  def initialize(bot)
    @bot = bot
    @wiki = bot.wiki
  
    @name_changes = {}
    @template_names = []
    @template_name = nil
    @filters = []
    @removable_parameters = []
  end
  
  def setup(action = nil)
    case action
    when "Infobox Logiciel"
      @template_names = ["Infobox Logiciel",
        "Logiciel simple",
        "Logiciel_simple",
        "Logiciel",
        "Infobox Software",
        "Infobox_Software",
      ]
      @name_changes = {
        "dernière_version" => "dernière version",
        "date_de_dernière_version" => "date de dernière version",
        "version_avancée" => "version avancée",
        "date_de_version_avancée" => "date de version avancée",
        "os" => "environnement",
        "site_web" => "site web",
        "name" => "nom",
        "screenshot" => "image",
        "caption" => "description",
        "developer" => "développeur",
        "latest release version" => "dernière version",
        "latest release date" => "date de dernière version",
        "latest preview version" => "dernière version avancée",
        "latest preview date" => "date de dernière version avancée",
        "latest_release_version" => "dernière version",
        "latest_release_date" => "date de dernière version",
        "latest_preview_version" => "dernière version avancée",
        "latest_preview_date" => "date de dernière version avancée",
        "platform" => "environnement",
        "operating system" => "environnement",
        "operating_system" => "environnement",
        "language" => "langue",
        "genre" => "type",
        "license" => "licence",
        "website" => "site web",
      }
      @filters = [
        :rename_parameters,
        :remove_open_source,
        :remove_almost_empty,
        :remove_firefox,
        :rewrite_dates,
      ]

      @template_name = "Infobox Logiciel"
    when "Infobox Aire protégée"
      @template_names = [
        "Infobox Aire protégée",
        "Infobox aire protégée",
        "Infobox_aire protégée",
      ]
      @name_changes = {
      }
      @filters = [
        :rename_parameters,
        :remove_parameters,
        :rewrite_dates,
        :rename_image_protected_area,
        :rewrite_coordinates,
        :rewrite_area,
      ]
      @template_name = "Infobox Aire protégée"
      @name_changes = {
        "name" => "nom",
        "iucn_category" => "catégorie iucn",
        "locator_x" => "localisation x",
        "locator_y" => "localisation y",
        "top_caption" => "légende image",
        "location" => "situation",
        "localisation" => "situation",
        "nearest_city" => "ville proche",
        "area" => "superficie",
        "established" => "création",
        "visitation_num" => "visiteurs",
        "visitation_year" => "visiteurs année",
        "governing_body" => "administration",
        "web_site" => "site web",
        "comments" => "remarque",
        "caption" => "légende carte",
        "base_width" => "largeur carte",
        "bot_image" => "image pied",
        "bot_caption" => "légende pied",
      }
      @removable_parameters = ["back_color", "label"]
    else
      @template_names = []
    end
  end
  
  def parse_infobox(text)
    parser = Piglobot::Parser.new
    parser.template_names = @template_names.map { |name|
      [name, name[0].chr.swapcase + name[1..-1]]
    }.flatten
    parser.find_template(text)
  end
  
  def rename_parameters(parameters)
    changes = @name_changes
    parameters.map! { |name, value|
      if changes.has_key? name
        name = changes[name]
      end
      [name, value]
    }
  end
  
  def rename_image_protected_area(parameters)
    if @infobox[:name] == "Infobox aire protégée" or @infobox[:name] == "infobox aire protégée" or
      @infobox[:name] == "Infobox_aire protégée" or @infobox[:name] == "infobox_aire protégée"
      parameters.map! { |name, value|
        name = "carte" if name == "image"
        [name, value]
      }
      parameters.map! { |name, value|
        name = "image" if name == "top_image"
        [name, value]
      }
    end
  end
  
  def rewrite_coordinates(params)
    names = %w(lat_degrees lat_minutes lat_seconds lat_direction long_degrees long_minutes long_seconds long_direction)
    hash = {}
    found_any = false
    names.each do |name|
      arg = params.find { |n, v| n == name }
      if arg
        arg = arg.last
        arg = nil if arg.empty?
        hash[name.intern] = arg
        found_any = true
      end
    end
    
    if found_any
      coord = nil
      if hash[:lat_degrees] and hash[:lat_minutes] and %w(N S).include?(hash[:lat_direction]) and
          hash[:long_degrees] and hash[:long_minutes] and %w(E W).include?(hash[:long_direction])
        if hash[:lat_seconds] and hash[:long_seconds]
          coord = "{{" + [
            "coord",
            hash[:lat_degrees],
            hash[:lat_minutes],
            hash[:lat_seconds],
            hash[:lat_direction],
            hash[:long_degrees],
            hash[:long_minutes],
            hash[:long_seconds],
            hash[:long_direction],
          ].join("|") + "}}"
        else
          coord = "{{" + [
            "coord",
            hash[:lat_degrees],
            hash[:lat_minutes],
            hash[:lat_direction],
            hash[:long_degrees],
            hash[:long_minutes],
            hash[:long_direction],
          ].join("|") + "}}"
        end
      elsif hash.values.all? { |arg| arg == nil }
        coord = "<!-- {{coord|...}} -->"
      else
        @bot.notice("Coordonnées invalides")
      end
    
      if coord
        done = false
        params.map! { |n, v|
          if names.include? n and !done
            done = true
            ["coordonnées", coord]
          else
            [n, v]
          end
        }
        params.delete_if { |n, v| names.include? n }
      end
    end
  end
  
  def rewrite_area(params)
    params.map! do |name, value|
      if name == "area" or name == "superficie"
        extra = nil
        found = true
        n = /[\d,.\s]+/
        case value
        when "", "[[km²]]", "<!-- {{unité|...|km|2}} -->"
          value = "<!-- {{unité|...|km|2}} -->"
          found = false
        when /\A([\d\.]+)\Z/
          value = $1
        when /\A(#{n}) km<sup>2<\/?sup>\Z/
          value = $1
        when /\A\{\{formatnum:(#{n})\}\} km²\Z/
          value = $1
        when /\A#{n} ha \((#{n}) km²\)\Z/
          value = $1
        when /\A#{n} acres<br \/>(#{n}) km²\Z/
          value = $1
        when /\A\{\{unité\|(#{n})\|km\|2\}\}\Z/
          value = $1.to_f
          value = (value * 100).round / 100.0 unless value < 0.1
          if value == value.to_i
            value = value.to_i
          end
        when /\A\{\{unité\|(#{n})\|m\|2\}\}\Z/
          value = $1.to_f / 1000000
        when /\A\{\{unité\|#{n}\|acres\}\}<br \/>\{\{unité\|(#{n})\|km\|2\}\}\Z/
          value = $1
        when /\A\{\{formatnum:#{n}\}\} acres \(\{\{formatnum:(#{n})\}\} km²\)\Z/
          value = $1
        when /\A\{\{formatnum:#{n}\}\} acres<br \/>\{\{formatnum:(#{n})\}\} km²\Z/
          value = $1
        when /\A(#{n}) ha (.+?)<br\/>(#{n}) ha (.+?)\Z/
          v1 = $1
          t1 = $2
          v2 = $3
          t2 = $4
          v1, v2 = [v1, v2].map { |v|
            v = v.tr(" ", "").to_i * 0.01
            v = v.to_s
            v.gsub!(/,(\d{3})/, "\\1")
            v.sub!(/,/, ".")
            v.gsub!(/ /, "")
            v = "{{unité|#{v}|km|2}}"
          }
          value = "#{v1} #{t1}<br/>#{v2} #{t2}"
          found = false
        when /\A(#{n}) ha( .+)?\Z/
          extra = $2
          value = $1.tr(" ", "").gsub(/,(\d{3})/, "\\1").gsub(/\.(\d{3})/, "\\1").sub(/,/, ".").to_f * 0.01
          value = (value * 100).round / 100.0 unless value < 0.1
          if value == value.to_i
            value = value.to_i
          end
        when /\A(#{n}) km²( .+)?\Z/
          value = $1
          extra = $2
        when /\A(#{n}) \[\[km²\]\]\Z/
          value = $1
        when /\A\{\{formatnum:(#{n})\}\} km\{\{2\}\}\Z/
          value = $1
        else
          @bot.notice("Superficie non gérée : <nowiki>#{value}</nowiki>")
          found = false
        end
        if found
          if value.to_f < 0.1
            value *= 1000000
            value = value.to_i if value.to_s =~ /\.0\Z/
            unit = "m"
          else
            unit = "km"
          end
          value = value.to_s
          value.gsub!(/,(\d{3})/, "\\1")
          value.sub!(/,/, ".")
          value.gsub!(/ /, "")
          value = "{{unité|#{value}|#{unit}|2}}"
          value << extra if extra
        end
      end
      [name, value]
    end
  end
  
  def remove_parameters(params)
    params.delete_if { |name, value|
      @removable_parameters.include? name
    }
  end
  
  def gsub_value(parameters, param_name, regexp, replacement)
    parameters.map! { |name, value|
      if param_name == :any or name == param_name
        value = value.gsub regexp, replacement
      end
      [name, value]
    }
  end
  
  def rewrite_date(value)
    if value =~ /\A\{\{(1er) (.+)\}\} \[\[(\d{4})\]\]\Z/ or
      value =~ /\A(1)\{\{er\}\} (.+) (\d{4})\Z/ or
      value =~ /\A\[\[(.+) (.+)\]\],? \[\[(\d{4})\]\]\Z/ or
      value =~ /\A(.+) (.+) (\d{4})\Z/ or
      value =~ /\A(.+) \[\[(.+) \(mois\)\|.+\]\] \[\[(\d{4})\]\]\Z/ or
      value =~ /\A(.+) \[\[(.+)\]\] \[\[(\d{4})\]\]\Z/ or
      value =~ /\A(.+) (.+) \[\[(\d{4})\]\]\Z/
      if $3
        day = $1
        month = $2
        year = $3
      else
        day = ""
        month = $1
        year = $2
      end
      if ((day =~ /\A\d+\Z/ and day.size <= 2) or day == "1er" or day.empty?) and
        %w(janvier février mars avril mai juin juillet août septembre
        octobre novembre décembre).map { |m|
          [m, m.capitalize]
        }.flatten.include? month
        day = "1" if day == "1er"
        day.sub! /\A0+/, ""
        value = "{{date|#{day}|#{month.downcase}|#{year}}}"
      end
    end
    value
  end
  
  def rewrite_dates(parameters)
    parameters.map! { |name, value|
      value = rewrite_date(value)
      [name, value]
    }
  end
  
  def remove_firefox(parameters)
    firefox_text = "<!-- Ne pas changer la capture d'écran, sauf grand changement. Et utilisez la page d'accueil de Wikipédia pour la capture, pas la page de Firefox. Prenez une capture à une taille « normale » (de 800*600 à 1024*780), désactiver les extensions et prenez le thème par défaut. -->"
    gsub_value(parameters, "image", /\A(.*)#{Regexp.escape(firefox_text)}(.*)\Z/, '\1\2')
    firefox_text = "<!-- 
                             * Ne pas changer la capture d'écran, sauf grand changement.
                             * Utiliser la page d'accueil de Wikipédia pour la capture, pas la page de Firefox.
                             * Prendre une capture à une taille « normale » (de 800*600 à 1024*780).
                             * Désactiver les extensions et prendre le thème par défaut.
                             -->"
    gsub_value(parameters, "image", /\A#{Regexp.escape(firefox_text)}(.*)\Z/, '\1')
  end
  
  def remove_open_source(parameters)
    gsub_value(parameters, "type", /(.+?) +\(\[\[open source\]\]\)$/, '\1')
  end
  
  def remove_almost_empty(parameters)
    gsub_value(parameters, :any, /\A\?\??\Z/, "")
    gsub_value(parameters, :any, /\A-\Z/, "")
    gsub_value(parameters, :any, /\A\{\{\{.+\|\}\}\}\Z/, "")
  end
  
  def write_infobox(box)
    if box[:parameters].empty?
      args = ""
    else
      parameters = box[:parameters]
      
      @infobox = box
      @filters.each do |method|
        send(method, parameters)
      end
      
      args = "\n" + parameters.map { |name, value|
        if name.nil?
          "| #{value}\n"
        elsif name.empty?
          "| = #{value}\n"
        else
          "| #{name} = #{value}\n"
        end
      }.join
    end
    "#{box[:before]}{{#{@template_name}#{args}}}#{box[:after]}"
  end
end

suivi_portail_informatique.rb[modifier | modifier le code]

class SuiviPortailInformatique < Piglobot::Job
  def initialize(*args)
    super
    @name = "[[Projet:Informatique/Suivi]]"
  end

  def process
    pages = @wiki.links("Modèle:Portail informatique")
    now = Time.now
    date = Piglobot::Tools.write_date(now)
    bot = "{{u|Piglobot}}"
    text = "<noinclude><small>''Liste des articles référencés par le projet « Informatique ». Mise à jour le #{date} par #{bot}.''</small></noinclude>\n"
    text << pages.map { |page| "* [[:#{page}]]\n" }.join
    @wiki.post("Projet:Informatique/Suivi", text, "Mise à jour automatique")
  end
end

suivi_portail_informatique_spec.rb[modifier | modifier le code]

require 'suivi_portail_informatique'

describe SuiviPortailInformatique do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).and_return(@wiki)
    
    @job = SuiviPortailInformatique.new(@bot)
  end
  
  it "should be a job" do
    @job.should be_kind_of(Piglobot::Job)
  end
  
  it "should have a better name" do
    @job.name.should == "[[Projet:Informatique/Suivi]]"
  end
  
  it "should retreive links and post them" do
    @wiki.should_receive(:links).with("Modèle:Portail informatique").and_return(["foo", "bar"])
    time = mock("time")
    Time.should_receive(:now).with().and_return(time)
    Piglobot::Tools.should_receive(:write_date).with(time).and_return("<date>")
    text = ""
    text << "<noinclude><small>''"
    text << "Liste des articles référencés par le projet « Informatique ». "
    text << "Mise à jour le <date> par {{u|Piglobot}}."
    text << "''</small></noinclude>\n"
    text << "* [[:foo]]\n"
    text << "* [[:bar]]\n"
    @wiki.should_receive(:post) do |page, content, comment|
      page.should == "Projet:Informatique/Suivi"
      content.should == text
      comment.should == "Mise à jour automatique"
    end
    @job.process
    @job.done?.should == true
  end
end

job_lann_spec.rb[modifier | modifier le code]

require 'job_lann'

describe Piglobot, " on clean job" do
  it "should know LANN job" do
    @wiki = mock("wiki")
    Piglobot::Wiki.should_receive(:new).with().and_return(@wiki)
    @bot = Piglobot.new
    @bot.job_class("LANN").should == LANN
  end

  it "should know AàC job" do
    @wiki = mock("wiki")
    Piglobot::Wiki.should_receive(:new).with().and_return(@wiki)
    @bot = Piglobot.new
    @bot.job_class("AàC").should == AaC
  end
end

describe "Page cleaner", :shared => true do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @parser = mock("parser")
    @bot.should_receive(:wiki) { @wiki }
  end
  
  it "should be a job" do
    @job.should be_kind_of(Piglobot::Job)
  end
  
  it "should have a name" do
    @job.name.should == @name
  end
  
  it "should have a category" do
    @job.category.should == @category
  end
  
  it "should have a title" do
    @job.title.should == @title
  end
  
  it "should have a done_model" do
    @job.done_model.should == @done_model
  end
  
  it "should have a empty comment" do
    @job.empty_comment.should == @empty_comment
  end
  
  [
    nil,
    { :pages => [] },
  ].each do |initial_data|
    it "should get pages when data is #{initial_data.inspect}" do
      @job.data = initial_data
      @job.should_receive(:get_pages).with()
      @job.should_receive(:remove_bad_names).with()
      @job.should_receive(:remove_cited).with()
      @job.should_receive(:remove_already_done).with() do
        @job.pages = ["foo", "bar", "baz"]
      end
      @job.should_receive(:notice).with("3 pages à traiter")
      @job.process
      @job.data[:pages].should == ["foo", "bar", "baz"]
      @job.data[:done].should == []
      @job.done?.should == false
    end
  end
  
  it "should process first page if pages filled" do
    @job.data = { :pages => ["foo", "bar"] }
    @job.should_receive(:process_page).with("foo")
    @job.process
    @job.data.should == { :pages => ["bar"], :done => [] }
    @job.done?.should == false
  end
  
  it "should be done when pages are empty" do
    @job.data = { :pages => ["bar"] }
    @job.should_receive(:process_page).with("bar")
    @job.should_receive(:notice).with("Aucune page blanchie")
    @job.process
    @job.data.should == { :pages => [], :done => [] }
    @job.done?.should == true
  end
  
  it "should notice pages done" do
    @job.data = { :pages => ["bar"], :done => ["foo", "Discussion foo", "baz"] }
    @job.should_receive(:process_page).with("bar")
    @job.should_receive(:notice).with("Pages blanchies : [[foo]], [[Discussion foo]], [[baz]]")
    @job.process
    @job.data.should == { :pages => [], :done => [] }
    @job.done?.should == true
  end
  
  it "should get pages" do
    items = ["Foo", "Bar", "Baz:Baz"]
    @wiki.should_receive(:category).with(@category).and_return(items)
    @job.should_receive(:log).with("3 articles dans la catégorie")
    @job.should_receive(:notice).with("3 pages dans la [[:Catégorie:#@category]]")
    @job.get_pages
    @job.pages.should == items
  end
  
  it "should remove bad names" do
    @job.pages = [
      "#{@title}Foo",
      "Foo",
      "Liste des articles non neutres/Foo",
      "#{@title}Baz",
      "#{@title}",
      "#{@title}Bar",
      "Modèle:Wikipédia:Liste des articles non neutres/Foo",
    ]
    @job.should_receive(:log).with("3 articles avec un nom valide")
    @job.should_receive(:notice).with("3 pages avec un nom valide")
    @job.remove_bad_names
    @job.pages.should == [
      "#{@title}Foo",
      "#{@title}Baz",
      "#{@title}Bar",
    ]
  end
  
  def parser_should_return(content, links)
    parser = mock("parser")
    Piglobot::Parser.should_receive(:new).with().and_return(parser)
    parser.should_receive(:internal_links).with(content).and_return(links)
  end
  
  it "should remove already done" do
    @job.pages = ["Foo", "Bar", "Baz"]
    @wiki.should_receive(:links).with(@done_model).and_return(["Foo", "bar", "Baz"])
    @job.should_receive(:log).with("1 articles non traités")
    @job.should_receive(:notice).with("1 pages ne contenant pas le [[#{@done_model}]]")
    @job.remove_already_done
    @job.pages.should == ["Bar"]
  end
  
  def time_travel(*now)
    Time.should_receive(:now).and_return(Time.local(*now))
  end
  
  def next_history_date(page, *now)
    @wiki.should_receive(:history).with(page, 1).and_return([
      { :author => "author2", :date => Time.local(*now), :oldid => "oldid2" }
    ])
  end
  
  def next_history_empty(page)
    @wiki.should_receive(:history).with(page, 1).and_return([])
  end
  
  it "should detect active page when page history is recent" do
    time_travel(2007, 10, 3, 23, 56, 12)
    next_history_date("foo", 2007, 9, 26, 23, 57, 0)
    @job.active?("foo").should == true
  end
  
  it "should detect active page when page history is old but talk history is recent" do
    time_travel(2007, 10, 3, 23, 56, 12)
    next_history_date("foo", 2007, 9, 26, 23, 56, 0)
    next_history_date("Discussion foo", 2007, 9, 26, 23, 57, 0)
    @job.active?("foo").should == true
  end
  
  it "should detect inactive page when both histories are old" do
    time_travel(2007, 10, 3, 23, 56, 12)
    next_history_date("foo", 2007, 9, 26, 23, 56, 0)
    next_history_date("Discussion foo", 2007, 9, 25, 23, 57, 0)
    @job.active?("foo").should == false
  end
  
  it "should detect inactive page when page history is old and no talk page" do
    time_travel(2007, 10, 3, 23, 56, 12)
    next_history_date("foo", 2007, 9, 26, 23, 56, 0)
    next_history_empty("Discussion foo")
    @job.active?("foo").should == false
  end
  
  it "should raise an error if page history is empty (but shouldn't happend)" do
    time_travel(2007, 10, 3, 23, 56, 12)
    next_history_empty("foo")
    lambda { @job.active?("foo") }.should raise_error(RuntimeError, "La page n'existe pas")
  end
  
  it "should not empty page if active" do
    @job.should_receive(:active?).with("foo").and_return(true)
    @job.should_not_receive(:notice).with("[[foo]] non blanchie car active")
    @job.should_receive(:log).with("[[foo]] ignorée car active")
    @job.should_not_receive(:empty_page)
    @job.process_page("foo")
    @job.changed?.should == false
  end

  it "should not empty page on error" do
    e = RuntimeError.new("error")
    e.set_backtrace(["foo", "bar"])
    @job.should_receive(:active?).with("foo").and_raise(e)
    @job.should_receive(:notice).with("[[foo]] non blanchie car une erreur s'est produite : error")
    @job.should_receive(:log).with("Erreur pour [[foo]] : error\nfoo\nbar")
    @job.should_not_receive(:empty_page)
    @job.process_page("foo")
    @job.changed?.should == true
  end

  it "should empty page if inactive" do
    @job.should_receive(:active?).with("foo").and_return(false)
    @job.should_receive(:empty_page).with("foo").and_return(true)
    @job.should_receive(:empty_talk_page).with("foo").and_return(true)
    @job.process_page("foo")
    @job.changed?.should == true
    @job.data[:done].should == ["foo", "Discussion foo"]
  end
  
  it "should append emptied pages to done" do
    @job.data = { :done => ["bar"] }
    @job.should_receive(:active?).and_return(false)
    @job.should_receive(:empty_page).with("foo").and_return(true)
    @job.should_receive(:empty_talk_page).with("foo").and_return(false)
    @job.process_page("foo")
    @job.data[:done].should == ["bar", "foo"]
  end
  
  it "should empty talk page" do
    page = "Wikipédia:Liste des articles non neutres/Bar"
    
    @wiki.should_receive(:history).with("Discussion " + page, 1).and_return([
      { :author => "author2", :date => Time.now, :oldid => "123456" }
    ])
    
    content = "{{Blanchiment de courtoisie}}"
    comment = @empty_comment
    
    @job.should_receive(:log).with("Blanchiment de [[Discussion #{page}]]")
    @wiki.should_receive(:post).with("Discussion " + page, content, comment)
    @job.empty_talk_page(page).should == true
  end
  
  it "should not empty inexistant talk page" do
    page = "Wikipédia:Liste des articles non neutres/Bar"
    
    @wiki.should_receive(:history).with("Discussion " + page, 1).and_return([])
    
    @job.should_receive(:log).with("Blanchiment inutile de [[Discussion #{page}]]")
    @job.empty_talk_page(page).should == false
  end
end

describe LANN do
  it_should_behave_like "Page cleaner"
  
  before do
    @job = LANN.new(@bot)
    @name = "[[WP:LANN]]"
    @category = "Wikipédia:Archives Articles non neutres"
    @title = "Wikipédia:Liste des articles non neutres/"
    @done_model = "Modèle:Archive LANN"
    @empty_comment = "[[Utilisateur:Piglobot/Travail#Blanchiment LANN|Blanchiment automatique de courtoisie]]"
  end

  it "should remove cited" do
    links = ["/Foo", "Bar"]
    @job.pages = ["Wikipédia:Liste des articles non neutres/Foo", "Wikipédia:Liste des articles non neutres/Bar"]
    
    @wiki.should_receive(:get).with("Wikipédia:Liste des articles non neutres").and_return("content")
    parser_should_return("content", links)
    @job.should_receive(:log).with("1 articles non cités")
    @job.should_receive(:notice).with("1 pages non mentionnées dans [[WP:LANN]]")
    @job.remove_cited
    @job.pages.should == ["Wikipédia:Liste des articles non neutres/Bar"]
  end
  
  it "should raise an error if none are cited" do
    @job.pages = ["Foo", "Bar"]
    @wiki.should_receive(:get).with("Wikipédia:Liste des articles non neutres").and_return("content")
    parser_should_return("content", ["Baz", "Bob"])
    lambda { @job.remove_cited }.should raise_error(Piglobot::ErrorPrevention, "Aucune page de la catégorie n'est cité dans [[WP:LANN]]")
  end
  
  it "should empty page with oldid" do
    page = "Wikipédia:Liste des articles non neutres/Bar"
    
    @wiki.should_receive(:history).with(page, 1).and_return([
      { :author => "author2", :date => Time.now, :oldid => "123456" }
    ])
    
    content = "{{subst:Blanchiment LANN | article = [[:Bar]] | oldid = 123456 }}"
    comment = "[[Utilisateur:Piglobot/Travail#Blanchiment LANN|Blanchiment automatique de courtoisie]]"
    
    @job.should_receive(:log).with("Blanchiment de [[#{page}]]")
    @wiki.should_receive(:post).with(page, content, comment)
    @job.empty_page(page).should == true
  end
  
end

describe AaC do
  it_should_behave_like "Page cleaner"
  
  before do
    @job = AaC.new(@bot)
    @name = "[[WP:AàC]]"
    @category = "Archives Appel à commentaires"
    @title = "Wikipédia:Appel à commentaires/"
    @done_model = "Modèle:Blanchiment de courtoisie"
    @empty_comment = "[[Utilisateur:Piglobot/Travail#Blanchiment AàC|Blanchiment automatique de courtoisie]]"
  end

  it "should remove cited" do
    parser = mock("parser")
    Piglobot::Parser.should_receive(:new).with().and_return(parser)
    
    links1 = ["page 1", "page 23"]
    links2 = ["page 5", "page 3"]
    @job.pages = ["page 1", "page 2", "page 3", "page 4"]
    
    @wiki.should_receive(:get).with("Wikipédia:Appel à commentaires/Article").and_return("content")
    parser.should_receive(:internal_links).with("content").and_return(links1)
    
    @wiki.should_receive(:get).with("Wikipédia:Appel à commentaires/Utilisateur").and_return("content2")
    parser.should_receive(:internal_links).with("content2").and_return(links2)
    
    @job.should_receive(:log).with("2 articles non cités")
    @job.should_receive(:notice).with("2 pages non mentionnées dans les pages de maintenance")
    @job.remove_cited
    @job.pages.should == ["page 2", "page 4"]
  end
  
  it "should empty page" do
    page = "Wikipédia:Appel à commentaire/Utilisateur/Bar"
    
    content = "{{subst:Blanchiment Appel à Commentaire}}"
    comment = "[[Utilisateur:Piglobot/Travail#Blanchiment AàC|Blanchiment automatique de courtoisie]]"
    
    @job.should_receive(:log).with("Blanchiment de [[#{page}]]")
    @wiki.should_receive(:post).with(page, content, comment)
    @job.empty_page(page).should == true
  end
end

job_lann.rb[modifier | modifier le code]

class LANN < Piglobot::Job
  attr_accessor :pages, :category, :title, :cite_pages, :done_model, :empty_comment

  def initialize(*args)
    super
    @name = "[[WP:LANN]]"
    @category = "Wikipédia:Archives Articles non neutres"
    @title = "Wikipédia:Liste des articles non neutres/"
    @done_model = "Modèle:Archive LANN"
    @empty_comment = "[[Utilisateur:Piglobot/Travail#Blanchiment LANN|Blanchiment automatique de courtoisie]]"
  end

  def done?
    @done
  end

  def process
    if @data.nil? or @data[:pages].empty?
      get_pages
      remove_bad_names
      remove_cited
      remove_already_done
      notice("#{@pages.size} pages à traiter")
    else
      @pages = @data[:pages]
      page = @pages.shift
      process_page page
    end
    if @pages.empty?
      @done = true
      if @data and @data[:done] and !@data[:done].empty?
        pages = @data[:done].map { |page| "[[#{page}]]" }.join(", ")
        notice("Pages blanchies : " + pages)
        @data[:done] = []
      else
        notice("Aucune page blanchie")
      end
    else
      @done = false
    end
    done_pages = []
    done_pages = (@data[:done] || []) if @data
    @data = { :pages => @pages, :done => done_pages }
  end
  
  def get_pages
    @pages = @wiki.category(@category)
    log "#{@pages.size} articles dans la catégorie"
    notice("#{@pages.size} pages dans la [[:Catégorie:#@category]]")
  end
  
  def remove_bad_names
    @pages.delete_if { |name| name !~ /\A#{Regexp.escape @title}./ }
    log "#{pages.size} articles avec un nom valide"
    notice("#{@pages.size} pages avec un nom valide")
  end
  
  def remove_cited
    lann = @wiki.get("Wikipédia:Liste des articles non neutres")
    parser = Piglobot::Parser.new
    links = parser.internal_links(lann)
    links.map! do |link|
      if link =~ %r{\A/}
        "Wikipédia:Liste des articles non neutres" + link
      else
        nil
      end
    end
    old_pages = @pages
    @pages -= links
    if @pages.size == old_pages.size
      raise Piglobot::ErrorPrevention, "Aucune page de la catégorie n'est cité dans [[WP:LANN]]"
    end
    log "#{pages.size} articles non cités"
    notice("#{@pages.size} pages non mentionnées dans [[WP:LANN]]")
  end
  
  def remove_already_done
    links = @wiki.links(@done_model)
    @pages -= links
    log "#{pages.size} articles non traités"
    notice("#{@pages.size} pages ne contenant pas le [[#{@done_model}]]")
  end
  
  def active?(page)
    now = Time.now
    limit = now - 7 * 24 * 3600
    history = @wiki.history(page, 1)
    raise "La page n'existe pas" if history.empty?
    
    date = history.first[:date]
    if date > limit
      true
    else
      talk_history = @wiki.history("Discussion " + page, 1)
      return false if talk_history.empty?
      
      talk_date = talk_history.first[:date]
      talk_date > limit
    end
  end
  
  def process_page(page)
    begin
      if active? page
        log("[[#{page}]] ignorée car active")
        @changed = false
      else
        done = []
        done = @data[:done] if @data and @data[:done]
        if empty_page(page)
          done << page
        end
        if empty_talk_page(page)
          done << "Discussion #{page}"
        end
        @data ||= {}
        @data[:done] = done
        @changed = true
      end
    rescue => e
      log("Erreur pour [[#{page}]] : #{e.message}\n#{e.backtrace.join("\n")}")
      notice("[[#{page}]] non blanchie car une erreur s'est produite : #{e.message}")
      @changed = true
    end
  end
  
  def empty_page(page)
    log("Blanchiment de [[#{page}]]")
    history = @wiki.history(page, 1)
    oldid = history.first[:oldid]
    article = page.sub(%r{\A.+?/}, "")
    content = "{{subst:Blanchiment LANN | article = [[:#{article}]] | oldid = #{oldid} }}"
    comment = "[[Utilisateur:Piglobot/Travail#Blanchiment LANN|Blanchiment automatique de courtoisie]]"
    @wiki.post(page, content, comment)
    true
  end

  def empty_talk_page(page)
    talk_page = "Discussion " + page
    history = @wiki.history(talk_page, 1)
    if history.empty?
      log("Blanchiment inutile de [[#{talk_page}]]")
      false
    else
      log("Blanchiment de [[#{talk_page}]]")
      oldid = history.first[:oldid]
      content = "{{Blanchiment de courtoisie}}"
      comment = @empty_comment
      @wiki.post(talk_page, content, comment)
      true
    end
  end
end

class AaC < LANN
  def initialize(*args)
    super
    @name = "[[WP:AàC]]"
    @category = "Archives Appel à commentaires"
    @title = "Wikipédia:Appel à commentaires/"
    @done_model = "Modèle:Blanchiment de courtoisie"
    @empty_comment = "[[Utilisateur:Piglobot/Travail#Blanchiment AàC|Blanchiment automatique de courtoisie]]"
  end

  def remove_cited
    parser = Piglobot::Parser.new
    
    content = @wiki.get("Wikipédia:Appel à commentaires/Article")
    links = parser.internal_links(content)
    
    content = @wiki.get("Wikipédia:Appel à commentaires/Utilisateur")
    links += parser.internal_links(content)
    
    old_pages = @pages
    @pages -= links
    log "#{pages.size} articles non cités"
    notice("#{@pages.size} pages non mentionnées dans les pages de maintenance")
  end
  
  def empty_page(page)
    log("Blanchiment de [[#{page}]]")
    content = "{{subst:Blanchiment Appel à Commentaire}}"
    comment = "[[Utilisateur:Piglobot/Travail#Blanchiment AàC|Blanchiment automatique de courtoisie]]"
    @wiki.post(page, content, comment)
    true
  end

end

homonym_prevention.rb[modifier | modifier le code]

class Piglobot
  class HomonymPrevention < Job
    def data_id
      "Homonymes"
    end
  
    def done?
      false
    end
    
    def process
      changes = false
      data = @data
      data ||= {}
      china = data["Chine"] || {}
      china = {} if china.is_a?(Array)
      last = china["Last"] || {}
      new = china["New"] || []
      
      if last.empty?
        last = @wiki.links("Chine")
        Piglobot::Tools.log("#{last.size} liens vers la page d'homonymie [[Chine]]")
      else
        current = @wiki.links("Chine")
        
        new.delete_if do |old_new|
          if current.include? old_new
            false
          else
            Piglobot::Tools.log("Le lien vers [[Chine]] dans [[#{old_new}]] a été supprimé avant d'être traité")
            true
          end
        end
        
        current_new = current - last
        last = current
        current_new.each do |new_name|
          Piglobot::Tools.log("Un lien vers [[Chine]] a été ajouté dans [[#{new_name}]]")
        end
        new += current_new
      end
      china["Last"] = last
      china["New"] = new if new
      data["Chine"] = china
      @changed = changes
      @data = data
    end
  end
end

homonym_prevention_spec.rb[modifier | modifier le code]

require 'homonym_prevention'

describe Piglobot, " on HomonymPrevention job" do
  it "should know job" do
    @wiki = mock("wiki")
    Piglobot::Wiki.should_receive(:new).with().and_return(@wiki)
    @bot = Piglobot.new
    @bot.job_class("Homonymes").should == Piglobot::HomonymPrevention
  end
end

describe Piglobot::HomonymPrevention do
  before do
    @bot = mock("bot")
    @wiki = mock("wiki")
    @bot.should_receive(:wiki).with().and_return(@wiki)
    @job = Piglobot::HomonymPrevention.new(@bot)
  end
  
  it "should never be done" do
    @job.done?.should == false
  end
  
  it "should find new links" do
    @job.data = { "Chine" => {"Last" => ["a", "b", "c"], "New" => [] }}
    @wiki.should_receive(:links, "Chine").and_return(["a", "b", "d", "c", "e"])
    Piglobot::Tools.should_receive(:log).with("Un lien vers [[Chine]] a été ajouté dans [[d]]")
    Piglobot::Tools.should_receive(:log).with("Un lien vers [[Chine]] a été ajouté dans [[e]]")
    @job.process
    @job.changed?.should == false
    @job.data.should == { "Chine" => {"Last" => ["a", "b", "d", "c", "e"], "New" => ["d", "e"] }}
  end
  
  it "should keep new links" do
    @job.data = { "Chine" => {"Last" => ["a", "b"], "New" => ["b"] }}
    @wiki.should_receive(:links, "Chine").and_return(["a", "b"])
    Piglobot::Tools.should_not_receive(:log)
    @job.process
    @job.data.should == { "Chine" => {"Last" => ["a", "b"], "New" => ["b"] }}
  end
  
  it "should ignore removed pending links" do
    @job.data = { "Chine" => {"Last" => ["a", "b"], "New" => ["b"] }}
    @wiki.should_receive(:links, "Chine").and_return(["a"])
    Piglobot::Tools.should_receive(:log).with("Le lien vers [[Chine]] dans [[b]] a été supprimé avant d'être traité")
    @job.process
    @job.data.should == { "Chine" => {"Last" => ["a"], "New" => [] }}
  end
end

tools_spec.rb[modifier | modifier le code]

require 'piglobot'
require 'helper'
=begin
    Copyright (c) 2007 by Piglop
    This file is part of Piglobot.

    Piglobot is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Piglobot is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Piglobot.  If not, see <http://www.gnu.org/licenses/>.
=end


describe Piglobot::Tools do
  it "should convert file to wiki" do
    result = Piglobot::Tools.file_to_wiki("filename", "file\ncontent", "lang")
    result.should == ([
      "== filename ==",
      '<syntaxhighlight lang="lang">',
      'file',
      'content',
      '<' + '/source>',
      '',
    ].map { |line| line + "\n" }.join)
  end
  
  it "should use Kernel.puts and append to piglobot.log on log" do
    time = mock("time")
    Time.should_receive(:now).with().and_return(time)
    time.should_receive(:strftime).with("%Y-%m-%d %H:%M:%S").and_return("time string")
    log_line = "time string: text"
    Kernel.should_receive(:puts).with(log_line)
    
    f = mock("file")
    File.should_receive(:open).with("piglobot.log", "a").and_yield(f)
    f.should_receive(:puts).with(log_line).once
    
    Piglobot::Tools.log("text")
  end
  
  [
    ["25 octobre 2007 à 17:17", Time.local(2007, 10, 25, 17, 17, 0)],
    ["25 août 2006 à 09:16", Time.local(2006, 8, 25, 9, 16, 0)],
    ["12 juillet 2006 à 08:40", Time.local(2006, 7, 12, 8, 40, 0)],
    ["10 novembre 2002 à 19:12", Time.local(2002, 11, 10, 19, 12, 0)],
    ["1 décembre 2002 à 11:39", Time.local(2002, 12, 1, 11, 39, 0)],
  ].each do |text, time|
    it "should parse time #{text.inspect}" do
      Piglobot::Tools.parse_time(text).should == time
    end
  end

  [
    "décembre 2002 à 11:39",
    "10 novembre 2002 19:12",
    "10 plop 2002 à 19:12",
    "10 octobre 2002",
    "foo 1 décembre 2002 à 11:39",
    "1 décembre 2002 à 11:39 foo",
  ].each do |text|
    it "should not parse time #{text.inspect}" do
      lambda { Piglobot::Tools.parse_time(text) }.should raise_error(ArgumentError, "Invalid time: #{text.inspect}")
    end
  end
  
  months = %w(janvier février mars avril mai juin juillet août septembre octobre novembre décembre)
  months.each_with_index do |month, i|
    expected_month = i + 1
    it "should parse month #{month.inspect}" do
      Piglobot::Tools.parse_time("25 #{month} 2007 à 17:17").should ==
        Time.local(2007, expected_month, 25, 17, 17, 0)
    end
  end
  
  months.each_with_index do |month, i|
    time = Time.local(2007, i + 1, 3, 11, 23, 3)
    Piglobot::Tools.write_date(time).should == "{{date|3|#{month}|2007}}"
  end
end

tools.rb[modifier | modifier le code]

=begin
    Copyright (c) 2007 by Piglop
    This file is part of Piglobot.

    Piglobot is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Piglobot is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Piglobot.  If not, see <http://www.gnu.org/licenses/>.
=end

module Piglobot::Tools
  module_function
  
  def file_to_wiki(filename, content, lang)
    result = ""
    result << "== #{filename} ==\n"
    result << "<syntaxhighlight lang=\"#{lang}\">\n"
    result << content + "\n"
    result << '<' + "/source>\n"
    result << "\n"
    result
  end
  
  def log(text)
    time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    line = "#{time}: #{text}"
    Kernel.puts line
    File.open("piglobot.log", "a") { |f|
      f.puts line
    }
  end
  
  MONTHS = %w(janvier février mars avril mai juin
              juillet août septembre octobre novembre décembre)
  
  def parse_time(text)
    if text =~ /\A(\d+) (\S+) (\d{4}) à (\d{2}):(\d{2})\Z/
      month = MONTHS.index($2)
      if month
        return Time.local($3.to_i, month + 1, $1.to_i, $4.to_i, $5.to_i, 0)
      end
    end
    raise ArgumentError, "Invalid time: #{text.inspect}"
  end
  
  def write_date(time)
    day = time.day
    month = MONTHS[time.month - 1]
    year = time.year
    "{{date|#{day}|#{month}|#{year}}}"
  end
end