Skip to content

Commit

Permalink
#1169 tags
Browse files Browse the repository at this point in the history
  • Loading branch information
yegor256 committed Jan 16, 2024
1 parent c818c81 commit 4738e6c
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ A user can (both via web interface and RESTful JSON API):
* Post an immutable **message** to a bout (can't edit or delete it)
* Attach a **flag** to a message
* Drop a flag from a message
* Attach an immutable **tag** to a bout with a value (can't detach or modify)
* Put an immutable **tag** to a bout with a value (can't remove or modify)
* List messages/bouts by search string

A search string is similar to what GitHub uses:
Expand Down
8 changes: 3 additions & 5 deletions liquibase/2024/001-initial-schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,14 @@ SOFTWARE.
</sql>
<sql>
CREATE TABLE tag (
id SERIAL PRIMARY KEY,
bout INT NOT NULL REFERENCES bout(id),
author VARCHAR(32) NOT NULL REFERENCES human(identity),
name VARCHAR(256) NOT NULL,
value VARCHAR(1024) NOT NULL,
created TIMESTAMPTZ DEFAULT now() NOT NULL
author VARCHAR(32) NOT NULL REFERENCES human(identity),
created TIMESTAMPTZ DEFAULT now() NOT NULL,
PRIMARY KEY(bout, name)
);
CREATE INDEX idx_tag1 ON tag (bout);
CREATE INDEX idx_tag2 ON tag (author);
CREATE INDEX idx_tag3 ON tag (name);
CREATE INDEX idx_tag4 ON tag (value);
CREATE INDEX idx_tag5 ON tag (created);
</sql>
Expand Down
20 changes: 16 additions & 4 deletions netbout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@
get '/inbox' do
offset = [(params[:offset] || '0').to_i, 0].max
limit = (params[:limit] || '10').to_i
query = params[:q] || ''
query = Nb::Query.new(params[:q] || '')
query.predicate.if_bout do |b|
bout = current_human.bouts.take(b)
raise Nb::Urror, "The bout ##{b.id} doesn't exist" unless bout.exists?
end
haml :inbox, locals: merged(
title: '/inbox',
query: Nb::Query.new(query),
query: query,
limit: limit,
offset: offset
)
Expand All @@ -131,7 +135,7 @@
flash(iri.cut('/start'), "The title can't be empty") if title.nil?
bout = current_human.bouts.start(title)
response.headers['X-Netbout-Bout'] = bout.id.to_s
flash(iri.cut('/b').append(bout.id), "Bout ##{bout.id} was started")
flash(iri.cut('/b').append(bout.id), "The bout ##{bout.id} started")
end

get '/b/{id}' do
Expand All @@ -157,7 +161,15 @@
text = params[:text]
msg = bout.post(text)
response.headers['X-Netbout-Message'] = msg.id.to_s
flash(iri.cut('/b').append(bout.id), "Message posted to the bout ##{msg.id}")
flash(iri.cut('/b').append(bout.id), "Message ##{msg.id} posted to the bout ##{bout.id}")
end

post '/tag' do
bout = current_human.bouts.take(params[:bout].to_i)
name = params[:name]
value = params[:value]
bout.tags.put(name, value)
flash(iri.cut('/b').append(bout.id), "Tag '##{name}' put to the bout ##{bout.id}")
end

get '/terms' do
Expand Down
9 changes: 9 additions & 0 deletions objects/bout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def initialize(pgsql, identity, id)
@id = id
end

def exists?
!@pgsql.exec('SELECT * FROM bout WHERE id = $1', [@id]).empty?
end

def post(text)
rows = @pgsql.exec(
'INSERT INTO message (author, bout, text) VALUES ($1, $2, $3) RETURNING id',
Expand All @@ -48,4 +52,9 @@ def post(text)
require_relative 'message'
Nb::Message.new(@pgsql, @identity, id)
end

def tags
require_relative 'tags'
Nb::Tags.new(@pgsql, @identity, self)
end
end
10 changes: 10 additions & 0 deletions objects/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ def text
@pgsql.exec('SELECT text FROM message WHERE id = $1', [@id])[0]['text']
end

def created
@pgsql.exec('SELECT created FROM message WHERE id = $1', [@id])[0]['created']
end

def author
author = @pgsql.exec('SELECT author FROM message WHERE id = $1', [@id])[0]['author']
require_relative 'humans'
Nb::Humans.new(@pgsql).take(author)
end

def bout
bout = @pgsql.exec('SELECT bout FROM message WHERE id = $1', [@id])[0]['bout'].to_i
require_relative 'bout'
Expand Down
36 changes: 29 additions & 7 deletions objects/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,25 @@ def initialize(text)
def predicate
preds = @text.split(/\s+and\s+/i).map do |t|
(left, right) = t.split('=')
Eq.new(left.downcase, right)
if right.nil?
Contains.new(left)
else
Eq.new(left.downcase, right)
end
end
And.new(preds)
end

# AND
class And
def initialize(preds)
@preds = preds
end

def if_bout
def if_bout(&block)
@preds.each do |t|
if t.is_a?(Eq)
t.if_bout do |bout|
yield bout
end
end
next unless t.is_a?(Eq)
t.if_bout(&block)
end
end

Expand All @@ -71,6 +73,7 @@ def to_sql
end
end

# EQ
class Eq
def initialize(left, right)
@left = left
Expand All @@ -89,4 +92,23 @@ def to_sql
"#{@left} = #{@right}"
end
end

# CONTAINS
class Contains
def initialize(text)
@text = text
end

def if_bout
false
end

def to_s
@text.to_s
end

def to_sql
"message.text LIKE '%#{@text.gsub('\'', '\\\'')}%'"
end
end
end
52 changes: 52 additions & 0 deletions objects/tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

# (The MIT License)
#
# Copyright (c) 2009-2024 Yegor Bugayenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

require_relative 'nb'
require_relative 'urror'

# Tag of a bout.
# Author:: Yegor Bugayenko ([email protected])
# Copyright:: Copyright (c) 2009-2024 Yegor Bugayenko
# License:: MIT
class Nb::Tag
attr_reader :name

def initialize(pgsql, bout, name)
@pgsql = pgsql
raise 'Bout is NULL' if bout.nil?
@bout = bout
raise 'Name is NULL' if name.nil?
@name = name
end

def exists?
!@pgsql.exec('SELECT * FROM tag WHERE bout=$1 AND name=$2', [@bout.id, @name]).empty?
end

def value
row = @pgsql.exec('SELECT value FROM tag WHERE bout=$1 AND name=$2', [@bout.id, @name])[0]
raise Nb::Urror, "Tag '#{@name}' not found in the bout ##{@bout.id}" if row.nil?
row['value']
end
end
60 changes: 60 additions & 0 deletions objects/tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

# (The MIT License)
#
# Copyright (c) 2009-2024 Yegor Bugayenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

require_relative 'nb'
require_relative 'urror'

# Tags of a bout.
# Author:: Yegor Bugayenko ([email protected])
# Copyright:: Copyright (c) 2009-2024 Yegor Bugayenko
# License:: MIT
class Nb::Tags
def initialize(pgsql, identity, bout)
@pgsql = pgsql
raise 'Identity is NULL' if identity.nil?
@identity = identity
raise 'Bout is NULL' if bout.nil?
@bout = bout
end

def take(name)
require_relative 'tag'
Nb::Tag.new(@pgsql, @bout, name)
end

def put(name, value)
@pgsql.exec(
'INSERT INTO tag (bout, name, author, value) VALUES ($1, $2, $3, $4)',
[@bout.id, name, @identity, value]
)
take(name)
end

def each
require_relative 'tag'
@pgsql.exec('SELECT * FROM tag WHERE bout=$1', [@bout.id]).each do |row|
yield take(row['name'])
end
end
end
48 changes: 48 additions & 0 deletions test/test_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

# (The MIT License)
#
# Copyright (c) 2009-2024 Yegor Bugayenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

require 'minitest/autorun'
require_relative 'test__helper'
require_relative '../objects/nb'
require_relative '../objects/humans'
require_relative '../objects/bouts'

# Test of Tags.
# Author:: Yegor Bugayenko ([email protected])
# Copyright:: Copyright (c) 2009-2024 Yegor Bugayenko
# License:: MIT
class Nb::TagsTest < Minitest::Test
def test_puts_tag_to_bout
human = Nb::Humans.new(test_pgsql).take(test_name).create
bouts = human.bouts
bout = bouts.start('hi')
bout.tags.put('a', 'Hello, друг!')
tag = bout.tags.take('a')
assert(tag.exists?)
assert(tag.value.start_with?('Hello'))
bout.tags.each do |t|
assert(t.exists?)
end
end
end
29 changes: 24 additions & 5 deletions views/inbox.haml
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
- the_bout = nil
- query.predicate.if_bout { |b| the_bout = b }

%form{action: iri, method: 'get'}
%fieldset
%input{name: 'q', type: 'text', value: query.predicate.to_s}
%button{type: 'submit'}
Search

- query.predicate.if_bout do |b|
- unless the_bout.nil?
%form{action: iri.cut('/post'), method: 'post'}
%fieldset
%input{name: 'bout', type: 'hidden', value: b}
%input{name: 'bout', type: 'hidden', value: the_bout}
%textarea{name: 'text', placeholder: 'Post a message...', tabindex: 1, autofocus: 1}
%br
%button{type: 'submit', tabindex: 2}
Post

- unless the_bout.nil?
%form{action: iri.cut('/tag'), method: 'post'}
%fieldset
%input{name: 'bout', type: 'hidden', value: the_bout}
%input{name: 'name', type: 'text', size: 8, placeholder: 'Tag', tabindex: 3}
%input{name: 'value', type: 'text', size: 20, placeholder: 'Value', tabindex: 4}
%button{type: 'submit', tabindex: 5}
Put
%p
Tags:
- current_human.bouts.take(the_bout).tags.each do |tag|
%code= "#{tag.name}=#{tag.value}"

- total = 0
- current_human.search(query, offset, limit).each do |msg|
%p
%a{href: iri.cut('/b').append(msg.bout.id)}
= "##{msg.bout.id}"
= '/'
- if the_bout.nil?
%a{href: iri.cut('/b').append(msg.bout.id)}= "##{msg.bout.id}"
= '/'
= "##{msg.id}"
= "by @#{msg.author.identity}"
= msg.created
%br
= msg.text
- total += 1

Expand Down
3 changes: 1 addition & 2 deletions views/index.haml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
%p
You have to
%a{href: github_login_link}
login
%a{href: github_login_link}= 'login'
first.

0 comments on commit 4738e6c

Please sign in to comment.