ruby_pocket 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +191 -0
- data/Rakefile +51 -0
- data/config/config.rb +14 -0
- data/config/database.rb +14 -0
- data/db/migrations/001_create_tags.rb +15 -0
- data/db/migrations/002_create_favorites.rb +17 -0
- data/db/migrations/003_create_favorites_tags.rb +20 -0
- data/db/pocket_development.db +0 -0
- data/db/pocket_test.db +0 -0
- data/lib/ruby_pocket/all.rb +5 -0
- data/lib/ruby_pocket/cli/add_action.rb +12 -0
- data/lib/ruby_pocket/cli/delete_action.rb +16 -0
- data/lib/ruby_pocket/cli/list_action.rb +39 -0
- data/lib/ruby_pocket/cli/open_action.rb +12 -0
- data/lib/ruby_pocket/cli/options.rb +23 -0
- data/lib/ruby_pocket/cli.rb +8 -0
- data/lib/ruby_pocket/environment.rb +11 -0
- data/lib/ruby_pocket/favorite.rb +36 -0
- data/lib/ruby_pocket/favorite_creator.rb +62 -0
- data/lib/ruby_pocket/favorite_query.rb +34 -0
- data/lib/ruby_pocket/tag.rb +32 -0
- data/lib/ruby_pocket/version.rb +3 -0
- data/lib/ruby_pocket/web_page.rb +33 -0
- data/lib/ruby_pocket.rb +37 -0
- data/test/ruby_pocket/cli/options_test.rb +49 -0
- data/test/ruby_pocket/favorite_creator_test.rb +80 -0
- data/test/ruby_pocket/favorite_query_test.rb +98 -0
- data/test/ruby_pocket/favorite_test.rb +76 -0
- data/test/ruby_pocket/features/add_test.rb +40 -0
- data/test/ruby_pocket/features/delete_test.rb +45 -0
- data/test/ruby_pocket/features/list_test.rb +75 -0
- data/test/ruby_pocket/tag_test.rb +53 -0
- data/test/ruby_pocket/web_page_test.rb +68 -0
- data/test/support/add_feature_mocker.rb +37 -0
- data/test/support/custom_assertions.rb +9 -0
- data/test/support/database_test_case.rb +11 -0
- data/test/support/default_test_case.rb +9 -0
- data/test/support/favorite_factory.rb +14 -0
- data/test/support/feature_assertions.rb +22 -0
- data/test/support/feature_test_case.rb +32 -0
- data/test/support/webmock.rb +3 -0
- data/test/test_helper.rb +19 -0
- metadata +305 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f563c1c1e135a1c47d268ecfdb1756b725024d90
|
4
|
+
data.tar.gz: d0d152035dcbd56324cc78c95b6c67595f8449ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c51b5f705315b878fccc9791acf391e57e5f916eefb71fd2f6b8af84a6d77e1419d4d7d78dbca4c660f2e21b4a05479808c1af0896539ac282b6ae735f0e996c
|
7
|
+
data.tar.gz: 550e7671b2f70a3a98e5f307f90b12451b1b6df892711fae04941e4026a7d4876ff91dd895fedf81c52a6bf027d44f07540902537d39fcc33dca0840465f9bc5
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# Ruby Pocket
|
2
|
+
|
3
|
+
[![Code Climate](https://codeclimate.com/github/thiagoa/ruby_pocket/badges/gpa.svg)](https://codeclimate.com/github/thiagoa/ruby_pocket)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/thiagoa/ruby_pocket/badges/coverage.svg)](https://codeclimate.com/github/thiagoa/ruby_pocket/coverage)
|
5
|
+
[![Travis CI](https://travis-ci.org/thiagoa/ruby_pocket.svg)](https://travis-ci.org/thiagoa/ruby_pocket)
|
6
|
+
|
7
|
+
*A warm place to store your precious development references.*
|
8
|
+
|
9
|
+
Although it has *pocket* in its name, **Ruby Pocket** is not a **Pocket** nor a
|
10
|
+
**Readability** clone. It is a tool designed to store useful all-around
|
11
|
+
development articles and reference URLs. You can store any kind of URL, but its
|
12
|
+
main appeal is for developers, or for those who need to store technical
|
13
|
+
articles or references of any kind.
|
14
|
+
|
15
|
+
Why would you want to use something like that? Having made this tool for
|
16
|
+
myself, here are some reasons why you might want to use it too:
|
17
|
+
|
18
|
+
- You feel like work and technical references should not be mixed with other
|
19
|
+
content types.
|
20
|
+
- You don't want to clutter your bookmarks with technical references.
|
21
|
+
- You don't want to clutter your **Pocket** or **Readability** account; they're
|
22
|
+
meant to be read later lists, not permanent article stores.
|
23
|
+
- **You want to be the real owner, and have full control over your own data**.
|
24
|
+
**Ruby Pocket** uses **SQLite3**; that means you're not limited to weird
|
25
|
+
export formats, or to no export formats at all; you can do whatever you want
|
26
|
+
with your data.
|
27
|
+
- You are a heavy command line user and lover, and don't want to break the flow
|
28
|
+
when looking for a reference.
|
29
|
+
- You enjoy a clean, text-based, clutter-free list of favorites.
|
30
|
+
- You want to have your URLs at your disposal, in all your computers, or even
|
31
|
+
have a private URL server. With a private URL server hosted in some place
|
32
|
+
like **Heroku**, you can also consume your content on mobile devices (*not
|
33
|
+
ready yet*).
|
34
|
+
- You want power searching (*not ready yet*).
|
35
|
+
|
36
|
+
If you're fine with your current method, whatever it is, just go with it :-)
|
37
|
+
|
38
|
+
## Current Features
|
39
|
+
|
40
|
+
Currently the app has a very simple feature set:
|
41
|
+
|
42
|
+
- Simple and delightful command line interface.
|
43
|
+
- Tagging support. Each favorite can have one or more tags.
|
44
|
+
- Add favorites.
|
45
|
+
- List favorites.
|
46
|
+
- Search favorites by tags.
|
47
|
+
- Delete favorites
|
48
|
+
- Open favorites in the default browser (currently for **OS X**)
|
49
|
+
|
50
|
+
## How to install
|
51
|
+
|
52
|
+
There is no RubyGem yet. Just clone the repo and install the dependencies:
|
53
|
+
|
54
|
+
```sh
|
55
|
+
bundle install
|
56
|
+
```
|
57
|
+
|
58
|
+
The binary is located in `bin/pocket`.
|
59
|
+
|
60
|
+
## Usage
|
61
|
+
|
62
|
+
Note: there are verbose versions for all commands presented here. See all
|
63
|
+
available options with the following command:
|
64
|
+
|
65
|
+
```sh
|
66
|
+
$ pocket -h
|
67
|
+
```
|
68
|
+
|
69
|
+
Add a favorite, fetching its name over the internet:
|
70
|
+
|
71
|
+
```sh
|
72
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line
|
73
|
+
|
74
|
+
Favorite 'jlevy/the-art-of-command-line · GitHub' created!
|
75
|
+
```
|
76
|
+
|
77
|
+
Add a favorite, but specify its name:
|
78
|
+
|
79
|
+
```sh
|
80
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line -t 'The Art of Command Line'
|
81
|
+
|
82
|
+
Favorite 'The Art of Command Line' created!
|
83
|
+
```
|
84
|
+
|
85
|
+
Add a favorite with tags. If more than one tag, separate with commas:
|
86
|
+
|
87
|
+
```sh
|
88
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line -t command-line
|
89
|
+
|
90
|
+
Favorite 'jlevy/the-art-of-command-line · GitHub' created!
|
91
|
+
```
|
92
|
+
|
93
|
+
List all favorites:
|
94
|
+
|
95
|
+
```sh
|
96
|
+
$ pocket -l
|
97
|
+
|
98
|
+
+----+------------------------------------------------------------------+--------------------+
|
99
|
+
| ID | Name | Tags |
|
100
|
+
+----+------------------------------------------------------------------+--------------------+
|
101
|
+
| 1 | Stronger Shell | shell-script |
|
102
|
+
| 2 | The Art of Command Line | command-line |
|
103
|
+
+----+------------------------------------------------------------------+--------------------+
|
104
|
+
```
|
105
|
+
|
106
|
+
Filter favorites by tag. Can filter by more than one tag:
|
107
|
+
|
108
|
+
```sh
|
109
|
+
$ pocket -l -t command-line
|
110
|
+
|
111
|
+
+----+------------------------------------------------------------------+--------------------+
|
112
|
+
| ID | Name | Tags |
|
113
|
+
+----+------------------------------------------------------------------+--------------------+
|
114
|
+
| 2 | The Art of Command Line | command-line |
|
115
|
+
+----+------------------------------------------------------------------+--------------------+
|
116
|
+
```
|
117
|
+
|
118
|
+
Open a favorite in your default browser. Use the ID of the favorite:
|
119
|
+
|
120
|
+
```sh
|
121
|
+
$ pocket -o 2
|
122
|
+
```
|
123
|
+
|
124
|
+
Delete favorites. Use the IDs of the favorites you want to delete, separated by
|
125
|
+
commas:
|
126
|
+
|
127
|
+
```sh
|
128
|
+
$ pocket -d 2
|
129
|
+
|
130
|
+
Favorite 'The Art of Command Line' deleted
|
131
|
+
```
|
132
|
+
|
133
|
+
## Planned Features
|
134
|
+
|
135
|
+
- Scrape more web page information: probably the main content of the page.
|
136
|
+
- Edit favorites.
|
137
|
+
- List available tags.
|
138
|
+
- Interactive command mode.
|
139
|
+
- Search favorites with partial tag matching.
|
140
|
+
- Search favorites by name or by content, with regex support.
|
141
|
+
- Group favorites by tag, and other useful view modes.
|
142
|
+
- Link database to another folder, such as **Dropbox**. That action shall be
|
143
|
+
automated with a command line flag.
|
144
|
+
- Remote backup support.
|
145
|
+
- **Linux** support for opening favorites.
|
146
|
+
- **Sinatra** web app to consume the content on mobile devices (as another project, which uses the gem).
|
147
|
+
- **Sinatra** API to consume the content (as another project, which uses the gem).
|
148
|
+
- **Alfred** workflow (as another project).
|
149
|
+
|
150
|
+
## Developer information
|
151
|
+
|
152
|
+
This project is built with **Ruby**. The name **Ruby**, from **Ruby
|
153
|
+
Pocket**, doesn't necessarily refer to the language itself :-)
|
154
|
+
|
155
|
+
### Running the tests
|
156
|
+
|
157
|
+
Unit tests:
|
158
|
+
|
159
|
+
```sh
|
160
|
+
rake test
|
161
|
+
```
|
162
|
+
|
163
|
+
Feature tests:
|
164
|
+
|
165
|
+
```sh
|
166
|
+
rake test:feature
|
167
|
+
```
|
168
|
+
|
169
|
+
All tests:
|
170
|
+
|
171
|
+
```sh
|
172
|
+
rake test:all
|
173
|
+
|
174
|
+
# Works too
|
175
|
+
rake
|
176
|
+
```
|
177
|
+
|
178
|
+
Run an IRB console:
|
179
|
+
|
180
|
+
```sh
|
181
|
+
rake console
|
182
|
+
```
|
183
|
+
|
184
|
+
## Contributing
|
185
|
+
|
186
|
+
- Fork the project
|
187
|
+
- Create a feature branch
|
188
|
+
- Make your code changes with tests
|
189
|
+
- Make a Pull-Request
|
190
|
+
|
191
|
+
This project uses MIT\_LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
%w(config lib test).each do |dir|
|
5
|
+
$LOAD_PATH << Pathname(__dir__).join(dir)
|
6
|
+
end
|
7
|
+
|
8
|
+
def run_tests(test_files)
|
9
|
+
require 'minitest'
|
10
|
+
|
11
|
+
test_files.each { |file | require file.expand_path }
|
12
|
+
|
13
|
+
Minitest.run
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Run unit tests'
|
17
|
+
task :test do
|
18
|
+
all = Pathname.glob('test/**/*_test.rb')
|
19
|
+
|
20
|
+
run_tests(all.reject { |f| f.to_s =~ /features/ })
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :test do
|
24
|
+
desc 'Run feature tests'
|
25
|
+
task :feature do
|
26
|
+
feature = Pathname.glob('test/ruby_pocket/features/**/*_test.rb')
|
27
|
+
|
28
|
+
run_tests(feature)
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Run all tests'
|
32
|
+
task :all do
|
33
|
+
all = Pathname.glob('test/**/*_test.rb')
|
34
|
+
|
35
|
+
run_tests(all)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc 'Run an IRB console'
|
40
|
+
task :console do |t|
|
41
|
+
require 'irb'
|
42
|
+
require 'ruby_pocket'
|
43
|
+
require 'config'
|
44
|
+
require 'ruby_pocket/all'
|
45
|
+
|
46
|
+
ARGV.clear
|
47
|
+
|
48
|
+
IRB.start
|
49
|
+
end
|
50
|
+
|
51
|
+
task default: 'test:all'
|
data/config/config.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
RubyPocket.environment = ENV['RUBY_POCKET_ENV'] if ENV['RUBY_POCKET_ENV']
|
2
|
+
RubyPocket.setup_data_dir
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'sequel'
|
7
|
+
require 'terminal-table'
|
8
|
+
|
9
|
+
if RubyPocket.environment.test? && ENV['MOCK_FEATURE'] == 'add'
|
10
|
+
require_relative '../test/support/add_feature_mocker'
|
11
|
+
AddFeatureMocker.new.run
|
12
|
+
end
|
13
|
+
|
14
|
+
require_relative 'database'
|
data/config/database.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
db_path = if RubyPocket.environment.production?
|
2
|
+
[RubyPocket.data_dir, 'pocket_production.db']
|
3
|
+
else
|
4
|
+
['db', "pocket_#{RubyPocket.environment.downcase}.db"]
|
5
|
+
end
|
6
|
+
|
7
|
+
DB = Sequel.connect("sqlite://#{File.join(*db_path)}")
|
8
|
+
|
9
|
+
Sequel::Model :validation_helpers
|
10
|
+
Sequel.extension :migration
|
11
|
+
|
12
|
+
unless Sequel::Migrator.is_current?(DB, 'db/migrations')
|
13
|
+
Sequel::Migrator.run(DB, 'db/migrations')
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
unless table_exists?(:favorites)
|
4
|
+
create_table :favorites do
|
5
|
+
primary_key :id
|
6
|
+
|
7
|
+
String :name, text: true
|
8
|
+
String :summary, text: true
|
9
|
+
String :url, text: true, unique: true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
down do
|
15
|
+
drop_table :favorites
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
run <<-SQL
|
4
|
+
CREATE TABLE IF NOT EXISTS favorites_tags(
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
6
|
+
tag_id INTEGER,
|
7
|
+
favorite_id INTEGER,
|
8
|
+
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
9
|
+
FOREIGN KEY(favorite_id) REFERENCES favorites(id) ON DELETE CASCADE,
|
10
|
+
UNIQUE(tag_id, favorite_id) ON CONFLICT REPLACE
|
11
|
+
)
|
12
|
+
SQL
|
13
|
+
end
|
14
|
+
|
15
|
+
down do
|
16
|
+
run <<-SQL
|
17
|
+
DROP TABLE favorites_tags
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
end
|
Binary file
|
data/db/pocket_test.db
ADDED
Binary file
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
module Cli
|
3
|
+
class DeleteAction
|
4
|
+
def call(options)
|
5
|
+
options.values[:ids].each do |id|
|
6
|
+
favorite = Favorite[id]
|
7
|
+
|
8
|
+
next puts "Favorite with ID #{id} not found!" unless favorite
|
9
|
+
|
10
|
+
favorite.destroy
|
11
|
+
puts "Favorite '#{favorite.name}' deleted"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'ruby_pocket/favorite_query'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
module Cli
|
5
|
+
class ListAction
|
6
|
+
def call(options)
|
7
|
+
favorites = FavoriteQuery.where(options.values).all
|
8
|
+
render favorites
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def render(favorites)
|
14
|
+
return puts 'Your Ruby Pocket is empty' if favorites.empty?
|
15
|
+
|
16
|
+
headings = %w(ID Name Tags)
|
17
|
+
rows = table_rows(favorites)
|
18
|
+
|
19
|
+
puts Terminal::Table.new headings: headings, rows: rows
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_rows(favorites)
|
23
|
+
rows = favorites.map do |f|
|
24
|
+
[f.id, f.name, f.tags.map(&:name).join(',')]
|
25
|
+
end
|
26
|
+
|
27
|
+
add_placeholders(rows)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_placeholders(rows)
|
31
|
+
rows.each do |row|
|
32
|
+
row.map! do |value|
|
33
|
+
(value.to_s.empty? && '-') || value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
module Cli
|
3
|
+
class Options
|
4
|
+
attr_reader :action, :values
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@values = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def action=(action)
|
11
|
+
if @action
|
12
|
+
fail ArgumentError, "Can't #{action} and #{@action} at the same time"
|
13
|
+
end
|
14
|
+
|
15
|
+
@action = action
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!
|
19
|
+
fail ArgumentError, 'You need to supply an action' unless action
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'ruby_pocket/tag'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
class Favorite < Sequel::Model
|
5
|
+
plugin :validation_helpers
|
6
|
+
|
7
|
+
many_to_many :tags
|
8
|
+
|
9
|
+
attr_writer :tag_names
|
10
|
+
|
11
|
+
def tag_names
|
12
|
+
@tag_names ||= [*@tag_names]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate
|
18
|
+
validates_presence :url
|
19
|
+
validates_unique :url
|
20
|
+
end
|
21
|
+
|
22
|
+
def before_save
|
23
|
+
name.strip! if name
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_create
|
27
|
+
create_tags
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_tags
|
31
|
+
tag_names.each do |name|
|
32
|
+
add_tag Tag.find_or_create(name: name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'ruby_pocket/favorite'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module RubyPocket
|
5
|
+
ValidationError = Class.new RubyPocketError
|
6
|
+
|
7
|
+
class FavoriteCreator
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_accessor :web_page
|
11
|
+
|
12
|
+
delegate :name => :favorite
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
@params = params
|
16
|
+
end
|
17
|
+
|
18
|
+
def favorite=(favorite)
|
19
|
+
fail 'Must be a new favorite' unless favorite.new?
|
20
|
+
@favorite = favorite
|
21
|
+
end
|
22
|
+
|
23
|
+
def save
|
24
|
+
fetch_missing_data
|
25
|
+
assign_params
|
26
|
+
save_favorite
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def fetch_missing_data
|
32
|
+
return if @params[:name]
|
33
|
+
return unless web_page_contents
|
34
|
+
|
35
|
+
@params[:name] = web_page_contents.title
|
36
|
+
end
|
37
|
+
|
38
|
+
def web_page_contents
|
39
|
+
return unless @params[:url]
|
40
|
+
|
41
|
+
@page ||= web_page.for(@params[:url])
|
42
|
+
end
|
43
|
+
|
44
|
+
def assign_params
|
45
|
+
favorite.set_all @params
|
46
|
+
end
|
47
|
+
|
48
|
+
def save_favorite
|
49
|
+
favorite.save
|
50
|
+
rescue Sequel::ValidationFailed => e
|
51
|
+
raise ValidationError, e.message
|
52
|
+
end
|
53
|
+
|
54
|
+
def favorite
|
55
|
+
@favorite ||= Favorite.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def web_page
|
59
|
+
@web_page ||= WebPage
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'ruby_pocket/favorite'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
class FavoriteQuery
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def self.where(options)
|
8
|
+
new.where(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
delegate :all => :@scope
|
12
|
+
|
13
|
+
def initialize(scope = nil)
|
14
|
+
@scope = scope || Favorite
|
15
|
+
end
|
16
|
+
|
17
|
+
def where(options)
|
18
|
+
if options[:tag_names]
|
19
|
+
tags = Tag.find_all(options[:tag_names])
|
20
|
+
@scope = where_tags(tags)
|
21
|
+
end
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def where_tags(tags)
|
29
|
+
tags.reduce(@scope) do |scope, tag|
|
30
|
+
scope.where(tags: tag)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
class Tag < Sequel::Model
|
3
|
+
plugin :validation_helpers
|
4
|
+
|
5
|
+
def self.find_all(names)
|
6
|
+
names.map do |name|
|
7
|
+
find(name: name).tap do |tag|
|
8
|
+
fail ArgumentError, "Tag #{name} not found" unless tag
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def name=(name)
|
14
|
+
super parameterize_name(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parameterize_name(name)
|
20
|
+
name
|
21
|
+
.downcase
|
22
|
+
.strip
|
23
|
+
.gsub(/\s+/, ' ')
|
24
|
+
.gsub(/[^a-z]/, '-')
|
25
|
+
.gsub(/-+/, '-')
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate
|
29
|
+
validates_unique :name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|