Initial commit
This commit is contained in:
commit
03399ab51c
38 changed files with 964 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
db/*.db
|
||||||
|
config/app.yml
|
37
.gitlab-ci.yml
Normal file
37
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- apt-get update -qy
|
||||||
|
- apt-get install -y nodejs libqtwebkit-dev qt4-qmake sqlite3 libsqlite3-dev xvfb
|
||||||
|
- bundle install --path /cache
|
||||||
|
- RACK_ENV=test bundle exec rake db:create
|
||||||
|
- RACK_ENV=test bundle exec rake db:migrate
|
||||||
|
- RACK_ENV=test xvfb-run -a bundle exec rspec
|
||||||
|
tags:
|
||||||
|
- rails
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- docker build -t atomaka/link-share .
|
||||||
|
except:
|
||||||
|
- tags
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
script:
|
||||||
|
- VERSION=$(git describe --tags)
|
||||||
|
- docker build -t atomaka/link-share .
|
||||||
|
- docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:$VERSION
|
||||||
|
- docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:latest
|
||||||
|
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com
|
||||||
|
- docker push docker.atomaka.com/atomaka/link-share
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
tags:
|
||||||
|
- docker
|
1
.ruby-version
Normal file
1
.ruby-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
2.2.2
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN export LANG=en_US.UTF-8 && \
|
||||||
|
export LANGUAGE=en_US.UTF-8 && \
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add build-base ruby-dev sqlite-dev \
|
||||||
|
&& apk add ruby ruby-bundler ruby-io-console \
|
||||||
|
&& rm -rf /var/cache/apk*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Gemfile* ./
|
||||||
|
RUN bundle install --path=vendor/bundle --jobs=4 --without=development test
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
CMD bundle exec rackup -o 0.0.0.0
|
26
Gemfile
Normal file
26
Gemfile
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'activerecord'
|
||||||
|
gem 'sinatra'
|
||||||
|
gem 'sqlite3'
|
||||||
|
|
||||||
|
gem 'sinatra-activerecord'
|
||||||
|
gem 'sinatra-contrib', require: false
|
||||||
|
gem 'sinatra-flash'
|
||||||
|
gem 'validate_url'
|
||||||
|
|
||||||
|
gem 'slim'
|
||||||
|
|
||||||
|
gem 'bigdecimal'
|
||||||
|
# alpine linux does not include a method to determine the timezone
|
||||||
|
gem 'tzinfo-data'
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'rspec'
|
||||||
|
gem 'capybara-webkit'
|
||||||
|
gem 'factory_girl'
|
||||||
|
gem 'database_cleaner'
|
||||||
|
gem 'launchy'
|
||||||
|
gem 'pry'
|
||||||
|
gem 'rerun'
|
||||||
|
end
|
143
Gemfile.lock
Normal file
143
Gemfile.lock
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
activemodel (4.2.3)
|
||||||
|
activesupport (= 4.2.3)
|
||||||
|
builder (~> 3.1)
|
||||||
|
activerecord (4.2.3)
|
||||||
|
activemodel (= 4.2.3)
|
||||||
|
activesupport (= 4.2.3)
|
||||||
|
arel (~> 6.0)
|
||||||
|
activesupport (4.2.3)
|
||||||
|
i18n (~> 0.7)
|
||||||
|
json (~> 1.7, >= 1.7.7)
|
||||||
|
minitest (~> 5.1)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.4)
|
||||||
|
tzinfo (~> 1.1)
|
||||||
|
addressable (2.3.8)
|
||||||
|
arel (6.0.2)
|
||||||
|
backports (3.6.6)
|
||||||
|
bigdecimal (1.2.7)
|
||||||
|
builder (3.2.2)
|
||||||
|
capybara (2.5.0)
|
||||||
|
mime-types (>= 1.16)
|
||||||
|
nokogiri (>= 1.3.3)
|
||||||
|
rack (>= 1.0.0)
|
||||||
|
rack-test (>= 0.5.4)
|
||||||
|
xpath (~> 2.0)
|
||||||
|
capybara-webkit (1.7.1)
|
||||||
|
capybara (>= 2.3.0, < 2.6.0)
|
||||||
|
json
|
||||||
|
celluloid (0.16.0)
|
||||||
|
timers (~> 4.0.0)
|
||||||
|
coderay (1.1.0)
|
||||||
|
database_cleaner (1.4.1)
|
||||||
|
diff-lcs (1.2.5)
|
||||||
|
factory_girl (4.5.0)
|
||||||
|
activesupport (>= 3.0.0)
|
||||||
|
ffi (1.9.10)
|
||||||
|
hitimes (1.2.2)
|
||||||
|
i18n (0.7.0)
|
||||||
|
json (1.8.3)
|
||||||
|
launchy (2.4.3)
|
||||||
|
addressable (~> 2.3)
|
||||||
|
listen (2.10.1)
|
||||||
|
celluloid (~> 0.16.0)
|
||||||
|
rb-fsevent (>= 0.9.3)
|
||||||
|
rb-inotify (>= 0.9)
|
||||||
|
method_source (0.8.2)
|
||||||
|
mime-types (3.0)
|
||||||
|
mime-types-data (~> 3.2015)
|
||||||
|
mime-types-data (3.2015.1120)
|
||||||
|
mini_portile2 (2.0.0)
|
||||||
|
minitest (5.7.0)
|
||||||
|
multi_json (1.11.2)
|
||||||
|
nokogiri (1.6.7)
|
||||||
|
mini_portile2 (~> 2.0.0.rc2)
|
||||||
|
pry (0.10.1)
|
||||||
|
coderay (~> 1.1.0)
|
||||||
|
method_source (~> 0.8.1)
|
||||||
|
slop (~> 3.4)
|
||||||
|
rack (1.6.4)
|
||||||
|
rack-protection (1.5.3)
|
||||||
|
rack
|
||||||
|
rack-test (0.6.3)
|
||||||
|
rack (>= 1.0)
|
||||||
|
rb-fsevent (0.9.5)
|
||||||
|
rb-inotify (0.9.5)
|
||||||
|
ffi (>= 0.5.0)
|
||||||
|
rerun (0.10.0)
|
||||||
|
listen (~> 2.7, >= 2.7.3)
|
||||||
|
rspec (3.4.0)
|
||||||
|
rspec-core (~> 3.4.0)
|
||||||
|
rspec-expectations (~> 3.4.0)
|
||||||
|
rspec-mocks (~> 3.4.0)
|
||||||
|
rspec-core (3.4.1)
|
||||||
|
rspec-support (~> 3.4.0)
|
||||||
|
rspec-expectations (3.4.0)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.4.0)
|
||||||
|
rspec-mocks (3.4.0)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.4.0)
|
||||||
|
rspec-support (3.4.1)
|
||||||
|
sinatra (1.4.6)
|
||||||
|
rack (~> 1.4)
|
||||||
|
rack-protection (~> 1.4)
|
||||||
|
tilt (>= 1.3, < 3)
|
||||||
|
sinatra-activerecord (2.0.6)
|
||||||
|
activerecord (>= 3.2)
|
||||||
|
sinatra (~> 1.0)
|
||||||
|
sinatra-contrib (1.4.6)
|
||||||
|
backports (>= 2.0)
|
||||||
|
multi_json
|
||||||
|
rack-protection
|
||||||
|
rack-test
|
||||||
|
sinatra (~> 1.4.0)
|
||||||
|
tilt (>= 1.3, < 3)
|
||||||
|
sinatra-flash (0.3.0)
|
||||||
|
sinatra (>= 1.0.0)
|
||||||
|
slim (3.0.6)
|
||||||
|
temple (~> 0.7.3)
|
||||||
|
tilt (>= 1.3.3, < 2.1)
|
||||||
|
slop (3.6.0)
|
||||||
|
sqlite3 (1.3.10)
|
||||||
|
temple (0.7.6)
|
||||||
|
thread_safe (0.3.5)
|
||||||
|
tilt (2.0.1)
|
||||||
|
timers (4.0.1)
|
||||||
|
hitimes
|
||||||
|
tzinfo (1.2.2)
|
||||||
|
thread_safe (~> 0.1)
|
||||||
|
tzinfo-data (1.2015.7)
|
||||||
|
tzinfo (>= 1.0.0)
|
||||||
|
validate_url (1.0.2)
|
||||||
|
activemodel (>= 3.0.0)
|
||||||
|
addressable
|
||||||
|
xpath (2.0.0)
|
||||||
|
nokogiri (~> 1.3)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
activerecord
|
||||||
|
bigdecimal
|
||||||
|
capybara-webkit
|
||||||
|
database_cleaner
|
||||||
|
factory_girl
|
||||||
|
launchy
|
||||||
|
pry
|
||||||
|
rerun
|
||||||
|
rspec
|
||||||
|
sinatra
|
||||||
|
sinatra-activerecord
|
||||||
|
sinatra-contrib
|
||||||
|
sinatra-flash
|
||||||
|
slim
|
||||||
|
sqlite3
|
||||||
|
tzinfo-data
|
||||||
|
validate_url
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
1.10.6
|
21
Makefile
Normal file
21
Makefile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
NAME = atomaka/docker-linkshare
|
||||||
|
CONTAINER = sinatra-linkshare
|
||||||
|
DOMAIN = linkshare.atomaka.com
|
||||||
|
PORT = 10081
|
||||||
|
DATABASE = /home/atomaka/linkshare.db
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker build -t $(NAME) .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
docker rm -f $(CONTAINER)
|
||||||
|
|
||||||
|
deploy: clean
|
||||||
|
touch $(DATABASE)
|
||||||
|
docker run -e VIRTUAL_HOST=$(DOMAIN) -d -p $(PORT):9292 -v $(DATABASE):/app/db/linkshare.db --name=$(CONTAINER) --restart=always $(NAME)
|
||||||
|
docker exec $(CONTAINER) rake db:migrate
|
||||||
|
|
||||||
|
update:
|
||||||
|
git pull origin master
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[![build status](https://git.atomaka.com/ci/projects/2/status.png?ref=master)](https://git.atomaka.com/ci/projects/2?ref=master)
|
||||||
|
|
||||||
|
# link-share
|
7
Rakefile
Normal file
7
Rakefile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require './app'
|
||||||
|
require 'sinatra/activerecord/rake'
|
||||||
|
|
||||||
|
desc "Starts the development server"
|
||||||
|
task :serve do
|
||||||
|
`bundle exec rerun -b rackup`
|
||||||
|
end
|
89
app.rb
Normal file
89
app.rb
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
require './config/init'
|
||||||
|
|
||||||
|
get '/manage' do
|
||||||
|
protected!
|
||||||
|
|
||||||
|
@links = get_links(params[:status])
|
||||||
|
|
||||||
|
slim :manage
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/new' do
|
||||||
|
protected!
|
||||||
|
|
||||||
|
@link = Link.new
|
||||||
|
|
||||||
|
slim :new
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/' do
|
||||||
|
protected!
|
||||||
|
|
||||||
|
@link = Link.new(params[:link])
|
||||||
|
|
||||||
|
if @link.save
|
||||||
|
flash[:success] = 'Link has been created'
|
||||||
|
redirect '/manage'
|
||||||
|
else
|
||||||
|
flash.now[:danger] = 'Did not pass validations'
|
||||||
|
slim :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/send' do
|
||||||
|
protected!
|
||||||
|
|
||||||
|
@link = Link.find(params[:id])
|
||||||
|
|
||||||
|
@link.mark_sent
|
||||||
|
|
||||||
|
flash[:success] = 'Link has been marked as sent'
|
||||||
|
redirect '/manage'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/destroy' do
|
||||||
|
protected!
|
||||||
|
|
||||||
|
@link = Link.find(params[:id])
|
||||||
|
|
||||||
|
if @link.sent?
|
||||||
|
flash[:warning] = 'Cannot delete sent link'
|
||||||
|
else
|
||||||
|
@link.destroy
|
||||||
|
flash[:success] = 'Link has been deleted'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect '/manage'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/' do
|
||||||
|
slim :calendar
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/events' do
|
||||||
|
start = params[:start]
|
||||||
|
finish = params[:end]
|
||||||
|
|
||||||
|
json serialize(Link.calendar(start, finish))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def get_links(status)
|
||||||
|
if status == 'sent'
|
||||||
|
Link.sent
|
||||||
|
elsif status == 'all'
|
||||||
|
Link.all
|
||||||
|
else
|
||||||
|
Link.unsent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(events)
|
||||||
|
events.map do |event|
|
||||||
|
{
|
||||||
|
title: event.title,
|
||||||
|
url: event.url,
|
||||||
|
start: event.sent_at.in_time_zone('Eastern Time (US & Canada)')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
2
config.ru
Normal file
2
config.ru
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
require './app'
|
||||||
|
run Sinatra::Application
|
3
config/app.yml.sample
Normal file
3
config/app.yml.sample
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
secret: YOUR_SECRET
|
||||||
|
users:
|
||||||
|
USERNAME: PASSWORD
|
15
config/environments.rb
Normal file
15
config/environments.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
set :database, 'sqlite3:db/linkshare.db'
|
||||||
|
|
||||||
|
configure :production do
|
||||||
|
config_file 'config/app.yml'
|
||||||
|
end
|
||||||
|
|
||||||
|
configure :development do
|
||||||
|
set :show_exceptions, true
|
||||||
|
config_file 'config/app.yml'
|
||||||
|
end
|
||||||
|
|
||||||
|
configure :test do
|
||||||
|
set :database, 'sqlite3:db/test.db'
|
||||||
|
config_file 'spec/fixtures/app.yml'
|
||||||
|
end
|
17
config/init.rb
Normal file
17
config/init.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
require 'rubygems'
|
||||||
|
require 'bundler'
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'sinatra/config_file'
|
||||||
|
require 'sinatra/json'
|
||||||
|
Bundler.require
|
||||||
|
|
||||||
|
set :root, File.dirname('..')
|
||||||
|
|
||||||
|
require_relative 'environments'
|
||||||
|
|
||||||
|
require_relative '../models/link'
|
||||||
|
require_relative '../helpers/application_helper'
|
||||||
|
|
||||||
|
config_file 'config/app.yml'
|
||||||
|
|
||||||
|
use Rack::Session::Cookie, secret: settings.secret
|
0
db/.gitkeep
Normal file
0
db/.gitkeep
Normal file
11
db/migrate/20150721142956_add_links.rb
Normal file
11
db/migrate/20150721142956_add_links.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class AddLinks < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :links do |t|
|
||||||
|
t.string :title
|
||||||
|
t.string :link
|
||||||
|
t.datetime :sent_at
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
db/migrate/20150721201947_change_link_to_url.rb
Normal file
5
db/migrate/20150721201947_change_link_to_url.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class ChangeLinkToUrl < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
rename_column :links, :link, :url
|
||||||
|
end
|
||||||
|
end
|
24
db/schema.rb
Normal file
24
db/schema.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
# This file is auto-generated from the current state of the database. Instead
|
||||||
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
|
#
|
||||||
|
# Note that this schema.rb definition is the authoritative source for your
|
||||||
|
# database schema. If you need to create the application database on another
|
||||||
|
# system, you should be using db:schema:load, not running all the migrations
|
||||||
|
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
||||||
|
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
||||||
|
#
|
||||||
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
|
ActiveRecord::Schema.define(version: 20150721201947) do
|
||||||
|
|
||||||
|
create_table "links", force: :cascade do |t|
|
||||||
|
t.string "title"
|
||||||
|
t.string "url"
|
||||||
|
t.datetime "sent_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
20
helpers/application_helper.rb
Normal file
20
helpers/application_helper.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
helpers do
|
||||||
|
def protected!
|
||||||
|
return if authorized?
|
||||||
|
headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
|
||||||
|
halt 401, "Not authorized\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorized?
|
||||||
|
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
||||||
|
return unless @auth.provided? and @auth.basic?
|
||||||
|
|
||||||
|
username, password = @auth.credentials
|
||||||
|
|
||||||
|
user_exists?(username) && settings.users[username] == password
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_exists?(username)
|
||||||
|
settings.users.keys.include?(username)
|
||||||
|
end
|
||||||
|
end
|
27
models/link.rb
Normal file
27
models/link.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class Link < ActiveRecord::Base
|
||||||
|
validates :title,
|
||||||
|
presence: true
|
||||||
|
validates :url,
|
||||||
|
presence: true,
|
||||||
|
url: true,
|
||||||
|
uniqueness: true
|
||||||
|
|
||||||
|
scope :sent, -> { where('sent_at IS NOT NULL').order('sent_at DESC') }
|
||||||
|
scope :unsent, -> { where('sent_at IS NULL').order('created_at ASC') }
|
||||||
|
scope :sent_after, ->(date) { where('sent_at > ?', date) }
|
||||||
|
scope :sent_before, ->(date) { where('sent_at < ?', date) }
|
||||||
|
scope :calendar, ->(start, finish) { sent_after(start).sent_before(finish) }
|
||||||
|
|
||||||
|
def mark_sent
|
||||||
|
update_attribute(:sent_at, Time.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sent?
|
||||||
|
!!self.sent_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def url=(url)
|
||||||
|
url.chomp!('/') if url.respond_to?(:chomp)
|
||||||
|
write_attribute(:url, url)
|
||||||
|
end
|
||||||
|
end
|
17
public/custom.css
Normal file
17
public/custom.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.link {
|
||||||
|
padding: 5px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:nth-of-type(odd) {
|
||||||
|
background: #e7e7e7
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-time {
|
||||||
|
display: none;
|
||||||
|
}
|
10
public/custom.js
Normal file
10
public/custom.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#calendar').fullCalendar({
|
||||||
|
header: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: ''
|
||||||
|
},
|
||||||
|
events: '/events',
|
||||||
|
});
|
||||||
|
});
|
6
spec/factories/link_factory.rb
Normal file
6
spec/factories/link_factory.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :link do
|
||||||
|
sequence(:title) { |n| "Sequenced Title (#{n})" }
|
||||||
|
sequence(:url) { |n| "http://www.#{n}example#{n}.com/#{n}" }
|
||||||
|
end
|
||||||
|
end
|
49
spec/features/admin/create_link_spec.rb
Normal file
49
spec/features/admin/create_link_spec.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Admin Create Links' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
it 'should not allow access to new link form' do
|
||||||
|
visit '/new'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
before(:each) { basic_auth 'admin', 'password' }
|
||||||
|
|
||||||
|
context 'with valid data' do
|
||||||
|
let(:link) { build(:link) }
|
||||||
|
|
||||||
|
it 'should allow creation of a link' do
|
||||||
|
visit '/new'
|
||||||
|
fill_in :link_title, with: link.title
|
||||||
|
fill_in :link_url, with: link.url
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
expect(page).to have_content 'Link has been created'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should list the new link' do
|
||||||
|
visit '/new'
|
||||||
|
fill_in :link_title, with: link.title
|
||||||
|
fill_in :link_url, with: link.url
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
expect(page).to have_content link.title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid data' do
|
||||||
|
let(:link) { build(:link, title: '') }
|
||||||
|
|
||||||
|
it 'should not allow link with invalid data' do
|
||||||
|
visit '/new'
|
||||||
|
fill_in :link_title, with: link.title
|
||||||
|
fill_in :link_url, with: link.url
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
expect(page).to have_content 'Did not pass validations'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
spec/features/admin/delete_link_spec.rb
Normal file
36
spec/features/admin/delete_link_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Admin Delete Links' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
it 'should not allow access to delete links' do
|
||||||
|
visit '/send'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
let!(:links) { 10.times.collect { create(:link) } }
|
||||||
|
before(:each) { basic_auth 'admin', 'password' }
|
||||||
|
|
||||||
|
it 'should allow deleting of a link' do
|
||||||
|
visit '/manage'
|
||||||
|
first('a', text: 'Delete').click
|
||||||
|
expect(page).to have_content('Link has been deleted')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should remove deleted link all lists' do
|
||||||
|
link = create(:link)
|
||||||
|
|
||||||
|
visit '/manage'
|
||||||
|
|
||||||
|
find('a', text: link.title)
|
||||||
|
.find(:xpath, '../..')
|
||||||
|
.find('a', text: 'Delete')
|
||||||
|
.click
|
||||||
|
|
||||||
|
visit '/manage?status=all'
|
||||||
|
|
||||||
|
expect(page).to_not have_content(link.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
53
spec/features/admin/list_links_spec.rb
Normal file
53
spec/features/admin/list_links_spec.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Admin List Links' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
it 'should not allow access to list links' do
|
||||||
|
visit '/manage'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
let!(:sent_link) { create(:link, sent_at: Time.now) }
|
||||||
|
let!(:links) { 10.times.collect { create(:link) } }
|
||||||
|
before(:each) { basic_auth 'admin', 'password' }
|
||||||
|
|
||||||
|
context 'unsent' do
|
||||||
|
it 'should show unsent links by default' do
|
||||||
|
visit '/manage'
|
||||||
|
|
||||||
|
expect(page).to_not have_content(sent_link.title)
|
||||||
|
links.each { |link| expect(page).to have_content(link.title) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should only list unsent links' do
|
||||||
|
visit '/manage'
|
||||||
|
first('a', text: 'Unsent Links').click
|
||||||
|
|
||||||
|
expect(page).to_not have_content(sent_link.title)
|
||||||
|
links.each { |link| expect(page).to have_content(link.title) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'sent' do
|
||||||
|
it 'should only list sent links' do
|
||||||
|
visit '/manage'
|
||||||
|
first('a', text: 'Sent Links').click
|
||||||
|
|
||||||
|
expect(page).to have_content(sent_link.title)
|
||||||
|
links.each { |link| expect(page).to_not have_content(link.title) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'all' do
|
||||||
|
it 'should list all links' do
|
||||||
|
visit '/manage'
|
||||||
|
first('a', text: 'All Links').click
|
||||||
|
|
||||||
|
expect(page).to have_content(sent_link.title)
|
||||||
|
links.each { |link| expect(page).to have_content(link.title) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
34
spec/features/admin/send_link_spec.rb
Normal file
34
spec/features/admin/send_link_spec.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Admin Send Links' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
it 'should not allow access to send links' do
|
||||||
|
visit '/send'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
let!(:links) { 10.times.collect { create(:link) } }
|
||||||
|
before(:each) { basic_auth 'admin', 'password' }
|
||||||
|
|
||||||
|
it 'should allow sending of a link' do
|
||||||
|
visit '/manage'
|
||||||
|
first('a', text: 'Send').click
|
||||||
|
expect(page).to have_content('Link has been marked as sent')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should remove sent link from unsent list' do
|
||||||
|
link = create(:link)
|
||||||
|
|
||||||
|
visit '/manage'
|
||||||
|
|
||||||
|
find('a', text: link.title)
|
||||||
|
.find(:xpath, '../..')
|
||||||
|
.find('a', text: 'Send')
|
||||||
|
.click
|
||||||
|
|
||||||
|
expect(page).to_not have_content(link.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
56
spec/features/authorization_spec.rb
Normal file
56
spec/features/authorization_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Authorization' do
|
||||||
|
context 'when not logged in' do
|
||||||
|
it 'should not allow access to manage' do
|
||||||
|
visit '/manage'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow access to the create form' do
|
||||||
|
visit '/new'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow access to send' do
|
||||||
|
visit '/send'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow access to delete' do
|
||||||
|
visit '/destroy'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should allow accessing the home page' do
|
||||||
|
visit '/'
|
||||||
|
expect(page.status_code).to be(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should allow accessing the events page' do
|
||||||
|
visit '/events'
|
||||||
|
expect(page.status_code).to be(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logging in' do
|
||||||
|
context 'with incorrect credentials' do
|
||||||
|
before(:each) { basic_auth 'baduser', 'badpassword' }
|
||||||
|
|
||||||
|
it 'should allow not allow access' do
|
||||||
|
visit '/manage'
|
||||||
|
expect(page.status_code).to be(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with correct credentials' do
|
||||||
|
before(:each) { basic_auth 'admin', 'password' }
|
||||||
|
|
||||||
|
it 'should allow access' do
|
||||||
|
visit '/manage'
|
||||||
|
|
||||||
|
expect(page.status_code).to be(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
spec/features/calendar_spec.rb
Normal file
17
spec/features/calendar_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'Calendar View' do
|
||||||
|
OVER_ONE_MONTH = 60*60*24*40
|
||||||
|
|
||||||
|
let!(:showing_link) { create(:link, sent_at: Time.now) }
|
||||||
|
let!(:before_link) { create(:link, sent_at: Time.now - OVER_ONE_MONTH) }
|
||||||
|
let!(:after_link) { create(:link, sent_at: Time.now + OVER_ONE_MONTH) }
|
||||||
|
|
||||||
|
it 'should only show links for the current month', js: true do
|
||||||
|
visit '/'
|
||||||
|
|
||||||
|
expect(page).to have_content(showing_link.title)
|
||||||
|
expect(page).to_not have_content(before_link.title)
|
||||||
|
expect(page).to_not have_content(after_link.title)
|
||||||
|
end
|
||||||
|
end
|
3
spec/fixtures/app.yml
vendored
Normal file
3
spec/fixtures/app.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
secret: my_secret_something_blan
|
||||||
|
users:
|
||||||
|
admin: password
|
65
spec/models/link_spec.rb
Normal file
65
spec/models/link_spec.rb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Link do
|
||||||
|
let(:link) { build(:link) }
|
||||||
|
|
||||||
|
context 'with valid data' do
|
||||||
|
it 'should be valid' do
|
||||||
|
expect(link).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid data' do
|
||||||
|
it 'should not be valid with blank title' do
|
||||||
|
link.title = ''
|
||||||
|
|
||||||
|
expect(link).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be valid with a blank url' do
|
||||||
|
link.url = ''
|
||||||
|
|
||||||
|
expect(link).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be valid with a bad url' do
|
||||||
|
link.url = 'bad'
|
||||||
|
|
||||||
|
expect(link).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be valid with a duplicate url' do
|
||||||
|
create(:link, url: link.url)
|
||||||
|
|
||||||
|
expect(link).to be_invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#mark_sent' do
|
||||||
|
it 'should set a time to sent_at' do
|
||||||
|
link.mark_sent
|
||||||
|
|
||||||
|
expect(link.sent_at).to_not be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#sent?' do
|
||||||
|
context 'when already sent' do
|
||||||
|
it 'should respond with true' do
|
||||||
|
link.mark_sent
|
||||||
|
expect(link.sent?).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should respond with false' do
|
||||||
|
expect(link.sent?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#url' do
|
||||||
|
it 'removes a trailing slash' do
|
||||||
|
link.url = 'http://www.url.com/'
|
||||||
|
expect(link.url).to eq('http://www.url.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
72
spec/spec_helper.rb
Normal file
72
spec/spec_helper.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
require 'rack/test'
|
||||||
|
require 'rspec'
|
||||||
|
require 'capybara/rspec'
|
||||||
|
require 'capybara/webkit'
|
||||||
|
require 'factory_girl'
|
||||||
|
require 'database_cleaner'
|
||||||
|
|
||||||
|
ENV['RACK_ENV'] = 'test'
|
||||||
|
|
||||||
|
require File.expand_path '../../app.rb', __FILE__
|
||||||
|
|
||||||
|
ActiveRecord::Migration.maintain_test_schema!
|
||||||
|
|
||||||
|
module TestingMixin
|
||||||
|
include Rack::Test::Methods
|
||||||
|
include RSpec::Matchers
|
||||||
|
include Capybara::DSL
|
||||||
|
include FactoryGirl::Syntax::Methods
|
||||||
|
|
||||||
|
Capybara.app = Sinatra::Application
|
||||||
|
Capybara.javascript_driver = :webkit
|
||||||
|
Capybara.asset_host = 'http://localhost:3000'
|
||||||
|
|
||||||
|
FactoryGirl.definition_file_paths = %w{./factories ./test/factories ./spec/factories}
|
||||||
|
FactoryGirl.find_definitions
|
||||||
|
|
||||||
|
def app() Sinatra::Application end
|
||||||
|
|
||||||
|
def basic_auth(username, password)
|
||||||
|
if page.driver.respond_to?(:basic_auth)
|
||||||
|
page.driver.basic_auth(username, password)
|
||||||
|
elsif page.driver.respond_to?(:basic_authorize)
|
||||||
|
page.driver.basic_authorize(username, password)
|
||||||
|
elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize)
|
||||||
|
page.driver.browser.basic_authorize(username, password)
|
||||||
|
else
|
||||||
|
raise "I don't know how to log in!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Capybara::Webkit.configure do |config|
|
||||||
|
config.allow_unknown_urls
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include TestingMixin
|
||||||
|
|
||||||
|
config.before(:suite) do
|
||||||
|
DatabaseCleaner.clean_with(:truncation)
|
||||||
|
end
|
||||||
|
|
||||||
|
config.before(:each) do
|
||||||
|
DatabaseCleaner.strategy = :truncation
|
||||||
|
end
|
||||||
|
|
||||||
|
config.before(:each, :js => true) do
|
||||||
|
DatabaseCleaner.strategy = :truncation
|
||||||
|
end
|
||||||
|
|
||||||
|
config.before(:each) do
|
||||||
|
DatabaseCleaner.start
|
||||||
|
end
|
||||||
|
|
||||||
|
config.append_after(:each) do
|
||||||
|
DatabaseCleaner.clean
|
||||||
|
Capybara.reset_sessions!
|
||||||
|
end
|
||||||
|
|
||||||
|
config.order = :random
|
||||||
|
Kernel.srand config.seed
|
||||||
|
end
|
1
views/calendar.slim
Normal file
1
views/calendar.slim
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#calendar
|
4
views/form_errors.slim
Normal file
4
views/form_errors.slim
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- if @link.errors.any?
|
||||||
|
ul
|
||||||
|
- @link.errors.full_messages.each do |error|
|
||||||
|
li= error
|
33
views/layout.slim
Normal file
33
views/layout.slim
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title Links App
|
||||||
|
link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
|
||||||
|
link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.css"
|
||||||
|
link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.print.css" media="print"
|
||||||
|
link rel="stylesheet" href="/custom.css"
|
||||||
|
body
|
||||||
|
.navbar.navbar-default.navbar-static-top.navbar-custom
|
||||||
|
.container
|
||||||
|
.navbar-header
|
||||||
|
button.navbar-toggle.collapsed type='button' data-toggle='collapse' data-target='.navbar-collapse'
|
||||||
|
span.sr-only Toggle navigation
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
span.icon-bar
|
||||||
|
a href="/" class="navbar-brand" Links
|
||||||
|
.collapse.navbar-collapse
|
||||||
|
ul.nav.navbar-nav
|
||||||
|
li
|
||||||
|
a href="/manage" Manage
|
||||||
|
ul.nav.navbar-nav.navbar-right
|
||||||
|
.container
|
||||||
|
- unless flash.empty?
|
||||||
|
- flash.each_key do |key|
|
||||||
|
.alert class="alert-#{key}" role="alert"= flash[key]
|
||||||
|
== yield
|
||||||
|
script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"
|
||||||
|
script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"
|
||||||
|
script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"
|
||||||
|
script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.js"
|
||||||
|
script src="custom.js"
|
14
views/link_list.slim
Normal file
14
views/link_list.slim
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.links
|
||||||
|
- @links.each do |link|
|
||||||
|
.row.link
|
||||||
|
.col-md-9
|
||||||
|
a href="#{link.url}" #{link.title}
|
||||||
|
.col-md-3
|
||||||
|
- if link.sent_at
|
||||||
|
= link.sent_at
|
||||||
|
- else
|
||||||
|
ul.list-inline style="margin: 0"
|
||||||
|
li
|
||||||
|
a href="/send?id=#{link.id}" Send
|
||||||
|
li
|
||||||
|
a href="/destroy?id=#{link.id}" Delete
|
15
views/manage.slim
Normal file
15
views/manage.slim
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.row.controls
|
||||||
|
.btn-toolbar.pull-right
|
||||||
|
a href="/manage?status=unsent" class="btn btn-primary" Unsent Links
|
||||||
|
a href="/manage?status=sent" class="btn btn-primary" Sent Links
|
||||||
|
a href="/manage?status=all" class="btn btn-primary" All Links
|
||||||
|
a href="/new" class="btn btn-primary" New Link
|
||||||
|
|
||||||
|
== slim :link_list
|
||||||
|
|
||||||
|
.row.controls
|
||||||
|
.btn-toolbar
|
||||||
|
a href="/manage?status=unsent" class="btn btn-primary" Unsent Links
|
||||||
|
a href="/manage?status=sent" class="btn btn-primary" Sent Links
|
||||||
|
a href="/manage?status=all" class="btn btn-primary" All Links
|
||||||
|
a href="/new" class="btn btn-primary" New Link
|
9
views/new.slim
Normal file
9
views/new.slim
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
== slim :form_errors
|
||||||
|
form action="/" method="post"
|
||||||
|
.form-group
|
||||||
|
label for="link_title" Title
|
||||||
|
input type="text" name="link[title]" id="link_title" class="form-control" value="#{@link.title}"
|
||||||
|
.form-group
|
||||||
|
label for="link_url" Link
|
||||||
|
input type="text" name="link[url]" id="link_url" class="form-control" value="#{@link.url}"
|
||||||
|
button type="submit" class="btn btn-primary" Submit
|
Loading…
Reference in a new issue