Skip to content

Instantly share code, notes, and snippets.

@elizabrock
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elizabrock/9212713 to your computer and use it in GitHub Desktop.
Save elizabrock/9212713 to your computer and use it in GitHub Desktop.
elizabrocksoftware.com's Freckle Integration. This drives: http://elizabrocksoftware.com/what_are_they_doing
# 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: 20131113162700) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "active_admin_comments", force: true do |t|
t.string "namespace"
t.text "body"
t.string "resource_id", null: false
t.string "resource_type", null: false
t.integer "author_id"
t.string "author_type"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "active_admin_comments", ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id", using: :btree
add_index "active_admin_comments", ["namespace"], name: "index_active_admin_comments_on_namespace", using: :btree
add_index "active_admin_comments", ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id", using: :btree
create_table "admin_users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "admin_users", ["email"], name: "index_admin_users_on_email", unique: true, using: :btree
add_index "admin_users", ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true, using: :btree
create_table "messages", force: true do |t|
t.string "from"
t.string "name"
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "project_screenshots", force: true do |t|
t.integer "project_id"
t.string "image"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "project_services", force: true do |t|
t.integer "project_id"
t.integer "service_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "projects", force: true do |t|
t.string "color"
t.integer "freckle_id"
t.string "logo"
t.string "freckle_name"
t.string "time_entry_name"
t.string "public_project_name"
t.text "client_description"
t.text "project_goal"
t.text "results"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "published"
t.boolean "testimonial_requested"
end
create_table "proposals", force: true do |t|
t.text "client_company_overview"
t.text "client_needs_overview"
t.text "executive_summary"
t.text "project_summary"
t.text "proposed_schedule"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "service_categories", force: true do |t|
t.text "description"
t.string "name"
t.integer "position"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "services", force: true do |t|
t.text "service_description"
t.integer "position"
t.text "process_description"
t.integer "service_category_id"
t.string "slug"
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.string "summary"
end
create_table "testimonials", force: true do |t|
t.integer "project_id"
t.text "testimonial"
t.string "client_name"
t.string "client_company"
t.string "client_link"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "time_entries", force: true do |t|
t.text "description_text"
t.date "entry_date"
t.integer "freckle_id"
t.integer "freckle_project_id"
t.integer "minutes"
t.text "tags"
t.text "url"
t.datetime "created_at"
t.datetime "updated_at"
end
end
require 'freckle'
class Cron
def self.run!
Freckle.import_projects!
(0..7).each do |i|
Freckle.import_time_entries!(Time.zone.today - i)
end
end
end
require 'httparty'
class Freckle
include HTTParty
base_uri 'https://elizabrock.letsfreckle.com/'
FRECKLE_PROJECT_MAPPINGS = {
:id => :freckle_id,
:color_hex => :color,
:name => :freckle_name
}
FRECKLE_TIME_ENTRY_MAPPINGS = {
:id => :freckle_id,
:project_id => :freckle_project_id,
:minutes => :minutes,
:description_text => :description_text,
:url => :url,
:date => :entry_date
}
def self.import_projects!
Freckle.new().import_projects
end
def self.import_time_entries! date = nil
date = Time.zone.today unless date
raise "Date cannot be in the future" if date.future?
Freckle.new().import_time_entries(date)
end
def import_time_entries date
options = default_options.merge( 'search[from]' => date.to_s(:freckle), 'search[to]' => date.to_s(:freckle) )
time_entries = self.class.get('/api/entries.json', :query => options)
time_entries.collect { |te| import_time_entry(te['entry']) }
end
def import_projects
options = default_options
projects = self.class.get('/api/projects.json', :query => options)
projects.collect { |project| import_project(project['project']) }
end
private
def import_time_entry te
time_entry = TimeEntry.find_or_create_by(freckle_id: te["id"])
updates = FRECKLE_TIME_ENTRY_MAPPINGS.each_pair do |freckle_attr, time_entry_attr|
time_entry.send("#{time_entry_attr}=", te[freckle_attr.to_s] || "")
end
time_entry.tags = te["tags"].map{|t| t["name"]}.join(", ")
time_entry.save
time_entry
end
def import_project p
project = Project.find_or_create_by(freckle_id: p["id"])
updates = FRECKLE_PROJECT_MAPPINGS.each_pair do |freckle_attr, project_attr|
project.send("#{project_attr}=", p[freckle_attr.to_s] || "")
end
project.save
project
end
def default_options
{:token => "[my_freckle_api_token]" }
end
end
require 'cron'
desc "Run cron job"
task :cron => :environment do
Cron.run!
end
require 'freckle'
require 'spec_helper'
describe Freckle do
describe ".import_time_entries!", :vcr do
context "no arguments" do
it "calls itself with today's date" do
Freckle.any_instance.should_receive(:import_time_entries).with(Date.today)
Freckle.stub(:get).and_return([])
Freckle.import_time_entries!
end
end
context "with a date argument in the future" do
it "returns raises an error" do
expect{ Freckle.import_time_entries!(Date.today + 3) }.to raise_error
end
it "does not hit freckle" do
Freckle.should_not_receive(:import_time_entries)
expect{ Freckle.import_time_entries!(Date.today + 3) }.to raise_error
end
end
context "with a date argument in the present/past" do
let(:date){ Date.parse("2011-07-04") }
let(:existing_project){ Fabricate(:project, freckle_id: 59449) }
let(:existing_time_entry){ Fabricate(:time_entry, freckle_id: 1103491, freckle_project_id: 59449, minutes: 20, description_text: "Add cucumber compass and rspec.", url: "https://github.com/elizabrock/watchelizago/commit/c426c7872766a68fcbc51809b2f27c89f2eca618", tags: "development", entry_date: Date.parse("2011-07-04")) }
let(:changed_time_entry){ Fabricate(:time_entry, freckle_id: 1103788, minutes: 345) } #actual time: 10
before do
existing_project
existing_time_entry
changed_time_entry
end
def do_action
Freckle.import_time_entries!(date)
end
it "does not create time entries from other days" do
do_action.keep_if{|te| te.entry_date != date}.should be_empty
end
it "doesn't duplicate existing time entries" do
do_action
TimeEntry.where(:freckle_id => existing_time_entry.freckle_id).count.should == 1
end
it "returns the new time entries" do
do_action.map(&:freckle_id).sort.should == [1103459, 1103491, 1103762, 1103779, 1103788, 1103801]
end
it "persists all of the time entries" do
a = do_action
b = TimeEntry.all
difference = ( (a || b) - ( a & b) ) #anything that isn't in both sets
difference.should be_empty
end
it "creates time entries that don't exist" do
TimeEntry.where(freckle_id: 1103762).to_a.should be_empty
do_action
TimeEntry.where(freckle_id: 1103762).to_a.should_not be_empty
end
it "updates time entries that have changed" do
expected_attributes = { freckle_id: 1103788, freckle_project_id: 59459, minutes: 10, description_text: "sending anne an email about the upcoming work", url: "", tags: "planning", entry_date: Date.parse("2011-07-04") }
do_action
changed_time_entry.reload
expected_attributes.each_pair do |attr, expected_value|
changed_time_entry[attr].should == expected_value
end
end
it "doesn't change time entries that haven't changed" do
expected = existing_time_entry.attributes
do_action
actual = TimeEntry.find(existing_time_entry.id)
actual.attributes.should == expected
end
it "creates any projects that don't exist" do
Project.where(freckle_id: 52665).to_a.should be_empty
do_action
Project.where(freckle_id: 52665).to_a.should_not be_empty
end
it "doesn't change projects that already exist" do
expect{ do_action }.to_not change{ existing_project.reload }
end
it "doesn't duplicate projects that already exist" do
do_action
Project.where(:freckle_id => existing_project.freckle_id).count.should == 1
end
end
end
end
@elizabrock
Copy link
Author

I use the heroku schedule to run rake cron every hour.

@elizabrock
Copy link
Author

The API documentation is here: http://developer.letsfreckle.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment