Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatické postovanie do fóra. #7

Merged
merged 14 commits into from
Feb 4, 2016
Merged
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source 'https://rubygems.org'
gem 'nokogiri'
gem 'curb'
gem 'dotenv'
gem 'discourse_api'

group :development do
gem 'rspec'
Expand Down
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@ GEM
simplecov (>= 0.7.1, < 1.0.0)
curb (0.9.1)
diff-lcs (1.2.5)
discourse_api (0.7.0)
faraday (~> 0.9.0)
faraday_middleware (~> 0.9)
rack (~> 1.5)
docile (1.1.5)
dotenv (2.1.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0)
faraday (>= 0.7.4, < 0.10)
json (1.8.3)
mini_portile2 (2.0.0)
multipart-post (2.0.0)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
parser (2.3.0.2)
ast (~> 2.2)
powerpack (0.1.1)
rack (1.6.4)
rainbow (2.1.0)
rake (10.4.2)
rspec (3.4.0)
Expand Down Expand Up @@ -48,6 +58,7 @@ PLATFORMS
DEPENDENCIES
codeclimate-test-reporter
curb
discourse_api
dotenv
nokogiri
rake
Expand Down
18 changes: 18 additions & 0 deletions lib/discourse_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require 'discourse_api'

class DiscourseClient < DiscourseApi::Client
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toto nerozumiem co riesi.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chcel som aby bol Discourse notifier zavisly iba na interface injektnuteho klienta. A tento koncept narusal fakt, ze musim chytat custom exception class z discourse gemu. Tak som urobil nas discource klient, ktory momentalne len wrapne ten z gemu a takisto prelozi ich exception na exception nasho klienta.

A tu exception potrebujem na to, aby som vedel odfiltrovat discourse api errory od ostatnych.

class Error < StandardError
attr_reader :wrapped_exception

def initialize(exception = $ERROR_INFO)
@wrapped_exception = exception
exception.respond_to?(:message) ? super(exception.message) : super(exception.to_s)
end
end

def create_topic(args = {})
super(args)
rescue DiscourseApi::Error => e
raise Error.new(e)
end
end
6 changes: 6 additions & 0 deletions lib/notifiers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require_relative 'notifiers/base'
require_relative 'notifiers/slack'
require_relative 'notifiers/discourse'

module Notifiers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toto sa mne moc neosvedcilo, resp. ak namespace tak potom Notifications::SlackNotifier atd. Tiez rozmyslam ci to cele (vsetko co male) nezabalit do Uvobot:: namespacu. Hm?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neosvedcilo v akom zmysle, ake problemy, alebo komplikacie tam hrozia?

Na namespacingu sa najprv ujednotme, ten sa mi nechce prerabat iterativne :).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Za mna to schovajme do Uvobot vsetko a notifikacie do Uvobot::Notifications a tam Uvobot::Notifications::SlackNotifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moze byt. Upravim.

end
15 changes: 15 additions & 0 deletions lib/notifiers/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Notifiers
class Base
def matching_announcements_found(page_info, announcements)
fail 'Interface method not implemented!'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

na toto je fajn gem abstract_type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A chceme dalsi gem? Nemam problem to prehodit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dajme, mozeme potom vyrazit tie testy na metody. Druha moznost je shared test. it_behaves_like "notifier"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dame gem.

end

def no_announcements_found
fail 'Interface method not implemented!'
end

def new_issue_not_published
fail 'Interface method not implemented!'
end
end
end
45 changes: 45 additions & 0 deletions lib/notifiers/discourse.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require_relative './base'

module Notifiers
class Discourse < Base
def initialize(discourse_client, category = 'Štátne projekty')
@client = discourse_client
@category = category
end

def no_announcements_found
# Does nothing for now.
end

def new_issue_not_published
# Does nothing for now.
end

def matching_announcements_found(page_info, announcements)
announcements.each do |a|
begin
topic = announcement_to_topic(a)
@client.create_topic(title: topic[:title],
raw: topic[:body],
category: @category)
rescue @client.class::Error => e
# discourse api/faraday bug - most probably
next if e.message == "757: unexpected token at 'null'"
puts "API Error: #{e.message}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toto je zhltnuta exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ano. Ta prva z faraday je odignorovana uplne. Api errors sa len vypisuju zatial, kym sa nedoriesi unikatnost, exception reporting a validacie.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Co keby ten create_topic proste vratil null ked sa to nepodari vytvorit? Exception ostatne mozu vyletavat, odchytit to nejaky handler hore - logovanie mame na heroku.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Takze v nasom kliente by som hltal discourse_api gem exceptions a vracal nil ak sa nepodari vytvorit topic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jo, DiscourseClient je aj tak zviazany s tym gemom, tak moze vediet ake exceptions to hadze.

end
end
end

private

def announcement_to_topic(announcement)
{
title: announcement[:procurement_subject].to_s,
body: "**Obstarávateľ:** #{announcement[:procurer]} \n" \
"**Predmet obstarávania:** #{announcement[:procurement_subject]} \n" \
"**Cena:** #{announcement[:amount]} EUR \n" \
"**Zdroj:** [#{announcement[:link][:text]}](#{announcement[:link][:href]})"
}
end
end
end
43 changes: 43 additions & 0 deletions lib/notifiers/slack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'json'
require 'curb'
require_relative './base'

module Notifiers
class Slack < Base
def initialize(slack_webhook, http_client = Curl)
@slack_webhook = slack_webhook
@http_client = http_client
end

def new_issue_not_published
send_message('*Fíha, dnes na ÚVO nevyšlo nové vydanie vestníka?*')
end

def matching_announcements_found(page, announcements)
send_message("Našiel som niečo nové na ÚVO! (#{page})")

announcements.each do |a|
send_message("<#{a[:link][:href]}|#{a[:link][:text]}>: *#{a[:procurer]}* #{a[:procurement_subject]}")
end
end

def no_announcements_found
send_message('Dnes som nenašiel žiadne nové IT zákazky.')
end

private

def send_message(text)
@http_client.post(@slack_webhook, payload(text))
end

def payload(text)
{
text: text,
channel: '#general',
username: 'uvobot',
icon_emoji: ':mag_right:'
}.to_json
end
end
end
40 changes: 0 additions & 40 deletions lib/slack_notifier.rb

This file was deleted.

20 changes: 16 additions & 4 deletions lib/uvo_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,30 @@ def self.parse_announcements(html)

doc(html).css('.oznamenie').each do |a|
link = a.css('.ozn1 a').first
customer = a.css('.ozn2').text.strip
description = a.css('.ozn3').text.strip
procurer = a.css('.ozn2').text.strip
procurement_subject = a.css('.ozn3').text.strip

announcements << {
link: { text: link.text, href: link['href'] },
customer: customer,
description: description
procurer: procurer,
procurement_subject: procurement_subject
}
end
announcements
end

def self.parse_detail(html)
detail = { amount: 'Extrakcia sa nepodarila.' }
begin
h_doc = doc(html)
# unstable, there are multiple formats of detail page
detail[:amount] = h_doc.xpath('//div[text()="Hodnota "]').css('span').first.text
rescue StandardError => e
puts "Detail parsing exception: #{e.message}"
end
detail
end

def self.parse_page_info(html)
doc(html).css('.search-results').first.text.strip
end
Expand Down
13 changes: 13 additions & 0 deletions lib/uvo_scraper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,17 @@ def get_announcements(release_date)

[@parser.parse_page_info(html), @parser.parse_announcements(html)]
end

def get_announcements_details(announcements)
announcements.map do |a|
html = @html_client.get(a[:link][:href]).body
detail = @parser.parse_detail(html)
detail.merge(a)
end
end

def get_full_announcements(release_date)
page_info, announcements = get_announcements(release_date)
[page_info, get_announcements_details(announcements)]
end
end
12 changes: 6 additions & 6 deletions lib/uvobot.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
class Uvobot
def initialize(notifier, scraper)
@notifier = notifier
def initialize(notifiers, scraper)
@notifiers = notifiers
@scraper = scraper
end

def run(release_date)
if @scraper.issue_ready?(release_date)
notify_announcements(release_date)
else
@notifier.new_issue_not_published
@notifiers.each { |n| n.send(:new_issue_not_published) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preco send ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lebo som truba :D. Omg. Opravim.

end
end

private

def notify_announcements(release_date)
page_info, announcements = @scraper.get_announcements(release_date)
page_info, announcements = @scraper.get_full_announcements(release_date)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toto je zbytocne pridavanie roboty. DiscourseNotifier by mal zabezpecit aj to stahovanie detailov. Tak to vieme aj pripadne hodit do async queue a sme v pohode. Totu ked padne jedno stahovanie, tak sme hotovi.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nj, povodne to bolo rozdelene prave kvoli tomu aby detail parsing nevyradil vsetko.

Notifier by potom bral v konstruktore aj scraper a dotahoval by si detail. Co je z funkcneho hladiska OK, akurat mi to uplne nesedi co tyka SRP.

Keby sme mu posunuli scraper spolu s ostatnymi datami ako nejaky proc object predchystany. Dotahovanie detailu by bolo potom lazy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Podla mna vsetko info co na stiahnutie detailu potrebujes sa uz posiela. Mas tam v matching_announcements_found parameter announcements kde je url detailu - z toho to vies vsetko potiahnut. Nie?

Violation SRP nevidim. Resp. na SRP v OOP mam taky dost kontroverzny nazor. Podla mna je to bullshit ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ano je tam vsetko na stiahnutie okrem scrapera.
Nieco stvorim.


if announcements.count > 0
@notifier.matching_announcements_found(page_info, announcements)
@notifiers.each { |n| n.send(:matching_announcements_found, page_info, announcements) }
else
@notifier.no_announcements_found
@notifiers.each { |n| n.send(:no_announcements_found) }
end
end
end
23 changes: 23 additions & 0 deletions spec/notifiers/base_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require './lib/notifiers/base'

RSpec.describe Notifiers::Base do
let(:base) { Notifiers::Base.new }

describe '.matching_announcements_found' do
it 'fails if not overriden' do
expect { base.matching_announcements_found('test', 'test') }.to raise_error 'Interface method not implemented!'
end
end

describe '.no_announcements_found' do
it 'fails if not overriden' do
expect { base.no_announcements_found }.to raise_error 'Interface method not implemented!'
end
end

describe '.new_issue_not_published' do
it 'fails if not overriden' do
expect { base.new_issue_not_published }.to raise_error 'Interface method not implemented!'
end
end
end
34 changes: 34 additions & 0 deletions spec/notifiers/discourse_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require './lib/notifiers/discourse'

RSpec.describe Notifiers::Discourse do
let(:client_double) { double }
let(:client_exception_class_double) { double }
let(:publisher) { Notifiers::Discourse.new(client_double, 'dummy category') }

describe '.match_announcements_found' do
it 'creates new topic for each announcement' do
allow(client_double).to receive_message_chain('create_topic') { true }
announcements = [{ link: { href: 'href', text: 'text' },
procurer: 'procurer', procurement_subject: 'subject', amount: '1000' }]

params = {
title: 'subject',
raw: "**Obstarávateľ:** procurer \n**Predmet obstarávania:** subject" \
" \n**Cena:** 1000 EUR \n**Zdroj:** [text](href)",
category: 'dummy category'
}
expect(client_double).to receive(:create_topic).with(params)

publisher.matching_announcements_found('page info', announcements)
end

it 'handles validations errors' do
end
end

describe '.no_announcements_found' do
end

describe '.new_issue_not_published' do
end
end
11 changes: 5 additions & 6 deletions spec/slack_notifier_spec.rb → spec/notifiers/slack_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
require 'spec_helper'
require './lib/slack_notifier'
require './lib/notifiers/slack'

RSpec.describe SlackNotifier do
RSpec.describe Notifiers::Slack do
let(:curl_double) { double }
let(:notifier) { SlackNotifier.new('slack.com', curl_double) }
let(:notifier) { Notifiers::Slack.new('slack.com', curl_double) }

describe '.new_issue_not_published' do
it 'sends correct payload to slack' do
Expand All @@ -14,12 +13,12 @@

describe '.matching_announcements_found' do
let(:announcements) do
[{ link: { text: 'text 1', href: 'href 1' }, customer: 'customer 1', description: 'desc 1' }]
[{ link: { text: 'text 1', href: 'href 1' }, procurer: 'procurer 1', procurement_subject: 'desc 1' }]
end

it 'sends correct payloads to slack' do
expect(curl_double).to receive(:post).with(*params('Našiel som niečo nové na ÚVO! (Found 1 record)'))
expect(curl_double).to receive(:post).with(*params('<href 1|text 1>: *customer 1* desc 1'))
expect(curl_double).to receive(:post).with(*params('<href 1|text 1>: *procurer 1* desc 1'))
notifier.matching_announcements_found('Found 1 record', announcements)
end
end
Expand Down
Loading