-
-
Save eric1234/1082868 to your computer and use it in GitHub Desktop.
# An API wrapper for the MaxHire API. Based on the ActiveResource API. | |
# Designed to keep the horrors of SOAP away from decent code :) | |
# | |
# USAGE | |
# | |
# Creating a new object | |
# | |
# a = Maxhire::Person.new | |
# a.first = 'Eric' | |
# a.last = 'Anderson' | |
# .... | |
# a.save | |
# | |
# Creating object with mass assignment | |
# | |
# a = Maxhire::Person.new | |
# a.attributes = params[:person] | |
# a.save | |
# | |
# Or as a one-liner | |
# | |
# Maxhire::Application.new(params[:application]).save | |
# | |
# Pull down an existing record and update an attribute. | |
# | |
# a = Maxhire::Person.get params[:id] | |
# a.email = '[email protected]' | |
# a.save | |
# | |
# All objects are supported (Person, Document, Application, Interview, | |
# Placement, Company, Job and Activity). When the library is first | |
# loaded it will pull down all the schemas to define the accessors. This | |
# makes the library nicer to work with (as you get a NoMethodError if | |
# you try to set a invalid attribute) but it does mean there is a slight | |
# performance hit at load time. | |
# | |
# Do to this library querying the API as part of the loading process | |
# you need to specify the connection info via the following environment | |
# variables: | |
# | |
# * MAXHIRE_DATABASE_NAME | |
# * MAXHIRE_SECURITY_KEY | |
# | |
# DEPENDENCIES | |
# | |
# Savon:: Used to wrangle SOAP | |
# Nokogiri:: Used to wrangle XML | |
# Friends from stdlib | |
require 'singleton' | |
require 'base64' | |
# 3rd party friends | |
require 'savon' | |
require 'nokogiri' | |
# Savon and HTTPI are noisy. Shut them up | |
Savon.configure {|c| c.log = false} | |
HTTPI.log = false | |
# Monkey-patch Ruby | |
class Module | |
# Backport from Ruby 1.9 if not already defined | |
def singleton_class | |
class << self | |
self | |
end | |
end unless respond_to? :singleton_class | |
# Returns the class name without any namespace. So | |
# Maxhire::Person.base_name will return the string Person. | |
def base_name | |
name.split('::').last | |
end | |
end | |
module Maxhire | |
# All objects record a "enterby" and "modifiedby" field which store | |
# who made the record and last changed it. These are required fields. | |
# To provide a reasonable default we set these to the below string. | |
# Calling code can do a "replace" on the constant to replace it with | |
# something more relevant if desired. | |
# | |
# NOTE: If changed you are limited to 15 characters | |
EDITING_USER = 'N/A' | |
# Module to simplify specifying namespaces. Just include in module | |
# (or extend) and then you can just say ns(:xs, :xmlns) and | |
# a hash will be returned that looks like: | |
# | |
# { | |
# :xs => 'http://www.w3.org/2001/XMLSchema', | |
# :xmlns => 'http://www.maxhire.net/' | |
# } | |
# | |
# This module just keep the code clean and neat. | |
module NamespaceLookup | |
# The various XML namespaces used | |
NAMESPACES = { | |
:xs => 'http://www.w3.org/2001/XMLSchema', | |
:diffgr => 'urn:schemas-microsoft-com:xml-diffgram-v1', | |
:xmlns => 'http://www.maxhire.net/' | |
} | |
# Implementation of the "ns" method | |
def namespaces *namespaces | |
Array(namespaces).flatten.inject({}) do |memo, namespace| | |
memo[namespace.to_s] = NAMESPACES[namespace] | |
memo | |
end | |
end | |
alias_method :ns, :namespaces | |
end | |
# Abstract class for CRUD of Maxhire objects. | |
class Base | |
include NamespaceLookup | |
# Unique identifier of object assigned by MaxHire. Will be nil | |
# on an un-saved new record. | |
attr_reader :id | |
# Create a new object not currently backed by the database. When | |
# saved it will be persisted via MaxHire API. | |
def initialize attributes={} | |
# Setup XML object this data object writes to | |
@xml = self.class.new_record_xml | |
# Setup defaults on attributes and on XML | |
@attributes = defaults | |
@xml.write defaults | |
# Nothing is dirty yet | |
@dirty = {} | |
# Assign initial attributes | |
self.attributes = attributes | |
end | |
# Will return the given attribute. | |
# | |
# If the attribute is not a valid attribute for this object then | |
# an ArgumentError will be thrown. | |
# | |
# Calling obj.active is the exact same as calling obj[:active]. | |
# Just depends on if you want to use dot notation or accessor | |
# notation. | |
def [] attribute | |
validate_attribute! attribute | |
@attributes[attribute.to_sym] | |
end | |
# Will assign the given value to the given attribute. | |
# | |
# If the attribute is not a valid attribute for this object then | |
# an ArgumentError will be thrown. | |
# | |
# Calling obj.active = true is the same a calling | |
# obj[:active] = true. Just depends on if you want to use dot | |
# notation or accessor notation. | |
def []= attribute, new_value | |
validate_attribute! attribute | |
@dirty[attribute.to_sym] = @attributes[attribute.to_sym] = new_value | |
end | |
# Will return all attributes assigned to this object | |
def attributes | |
@attributes.dup | |
end | |
# Will merge the new attributes into the existing attributes | |
def attributes= attributes | |
(attributes || {}).each {|f, v| self[f] = v} | |
end | |
# Keep the output more reasonable. | |
def inspect | |
"#{self.class.base_name} <#{@attributes.inspect}>" | |
end | |
alias_method :to_s, :inspect | |
# Will save this object back to the database. Correctly handles | |
# a new record vs. updating an existing record. Sometimes the | |
# Add/Update methods have extra arguments in addition to the main | |
# dataset. If you need to use those you can pass this method | |
# a block which yields a Builder object to add more XML info. | |
def save | |
# Prevent un-necessary API interaction. | |
return if @dirty.empty? | |
@xml.write @dirty | |
@dirty = {} | |
api_method = new? ? :add : :update | |
response = APIConnection.instance.request self.class.base_name, api_method do |xml| | |
xml.tag! "ds#{self.class.base_name}" do | |
xml << @xml.schema.to_s | |
xml << @xml.data.to_s | |
end | |
xml.tag! self.class.id_param, @id unless new? | |
yield xml if block_given? | |
end | |
response = process_update response, api_method | |
# Reload from Get as DataSet from X_GetEmptyForAdd cannot be used | |
# to update. Also useful if server adjusts data. | |
load_id response.at_xpath('//Id').content if new? | |
end | |
# Is this record a new record that will be inserted when saved or | |
# an existing record that will be updated? | |
def new? | |
id.nil? | |
end | |
# Will remove the record from Maxhire. Will freeze instance to | |
# prevent further interaction. | |
def destroy | |
response = APIConnection.instance.request self.class.base_name, :delete do |xml| | |
xml.tag! self.class.id_param, id | |
end | |
process_update response, :delete | |
freeze | |
end | |
# Will load the current object with the data on the given id. | |
# Nothing from the previous state is saved. | |
# | |
# Used internally by Maxhire::Base.get and Maxhire::Base.save and | |
# not intended for public usage. | |
def load_id id # :nodoc: | |
# Make API call to get data. | |
response = APIConnection.instance.request self.class.base_name, :get do |xml| | |
xml.tag! self.class.id_param, id | |
end | |
@xml = DataSet.new response.to_xml | |
self.class.overwrite_field_types @xml.columns | |
# Make sure the data was found | |
raise Error, 'Invalid id' unless @xml.xml.at_xpath '//Table' | |
# Load into internal attributes | |
@attributes = @xml.attributes | |
@id = id | |
# Assign defaults if no existing value | |
defaults.each do |fld, value| | |
self[fld] = value if self[fld].blank? | |
end | |
self | |
end | |
private | |
# Maxhire has some fields that are "required" even if you have | |
# no valid value to provide. This is usually for foreign keys and | |
# often a dummy value such as 0, 1, or 'N/A' can be provided. | |
# | |
# To get around this issue each subclass can define a constant | |
# DEFAULTS which is a hash of attributes and their default value. | |
# | |
# All numeric columns are assumed to be 0 to cut down on the number | |
# of defaults that must be specified. Also the "enterby" and | |
# "modifiedby" fields will be set to the constant EDITING_USER. | |
# | |
# This method returns that constant combined with the magic columns | |
# (numbers and editing user) | |
def defaults | |
defs = if self.class.const_defined? 'DEFAULTS' | |
self.class.const_get('DEFAULTS').dup | |
else | |
{} | |
end | |
# Set numeric fields to 0 | |
self.class.columns.each do |attribute, specs| | |
defs[attribute] ||= 0 if | |
['int', 'decimal'].include? specs[:field_type] | |
end | |
# Set user | |
defs[:enterby] ||= EDITING_USER | |
defs[:modifiedby] ||= EDITING_USER | |
defs | |
end | |
# The status of an updated is encoded in an XML document which | |
# itself is encoded in the response XML document. This method will | |
# extract a reasonable answer out of that mess and raise an error if | |
# there was a problem. | |
# | |
# The api_method used is needed to be able to extract the response. | |
# The "inner" XML document is returned. | |
def process_update response, api_method | |
response_tag = "#{self.class.base_name}_#{api_method.to_s.capitalize}Result" | |
response = Nokogiri::XML response.to_xml | |
response = response.at_xpath("//xmlns:#{response_tag}", ns(:xmlns)).content | |
raise Error, "#{api_method} error" if response.empty? | |
response = Nokogiri::XML response | |
raise Error.from_error_message(response.at_xpath('//ErrorDescription').content) if | |
response.at_xpath('//Success').content == 'False' | |
response | |
end | |
# Will ensure the given attribute is a valid attribute. If not | |
# an ArgumentError will be thrown. | |
def validate_attribute! attribute | |
raise ArgumentError, "Attributes #{attribute} is not valid" unless | |
self.class.columns.keys.include? attribute.to_sym | |
end | |
class << self | |
# Will instantiate the object with the given id. | |
def get(id) | |
new.load_id id | |
end | |
# Will return the schema for this class. | |
# | |
# Pulled from the info in X_GetEmptyForAdd which is slightly | |
# different (and less detailed) then the schema that X_Get | |
# provides. But since we need to know the schema before we pull | |
# any actual data we are forced to use this version of the schema. | |
def columns | |
overwrite_field_types new_record_xml.columns | |
end | |
# In general most of the API calls use "intObjectNameId" for the | |
# paramter used to load, delete and update records. But some of | |
# the objects do not follow this pattern. Therefore we are | |
# providing a good default and but abstracting it as a method | |
# so the subclass can override. | |
def id_param | |
"int#{base_name}Id" | |
end | |
# The schema coming from Maxhire is not correct sometimes. For | |
# example work_emp_from is a dateTime field but the schema reports | |
# it as a string. Subclasses can define the constant | |
# FIELD_TYPE_OVERRIDES to specify the true types of each field. | |
# This ensures the proper encoding and decoding of values. | |
# | |
# Will take a column hash and adjusted it according to the | |
# overrides. This is used internally when getting the schema for | |
# new records as well as the schema for existing records. | |
def overwrite_field_types(columns) # :nodoc | |
overrides = if const_defined? 'FIELD_TYPE_OVERRIDES' | |
const_get('FIELD_TYPE_OVERRIDES').dup | |
else | |
{} | |
end | |
overrides.each do |column, type| | |
columns[column][:field_type] = type if columns.has_key? column | |
end | |
columns | |
end | |
# Using the X_GetEmptyForAdd API call determine columns and | |
# dynamically create accessors on singleton class. | |
def inherited subclass # :nodoc: | |
# Call API to get schema | |
xml = APIConnection.instance.request subclass.base_name, :get_empty_for_add | |
xml = DataSet.new xml.to_xml | |
xml.clear | |
# We will be installing some singleton methods on the meta_class | |
# so our subclass will have object specific methods. | |
meta_class = subclass.singleton_class | |
# Store template DataSet XML structure to create new records | |
meta_class.send(:define_method, :new_record_xml) {xml.dup} | |
subclass.columns.keys.each do |column_name| | |
# If method already defined do not override | |
next if subclass.instance_methods.include? column_name.to_s | |
# Install getter accessor | |
subclass.send :define_method, column_name do | |
self[column_name] | |
end | |
# Install setter accessor | |
subclass.send :define_method, "#{column_name}=" do |new_value| | |
self[column_name] = new_value | |
end | |
end | |
end | |
end | |
end | |
# When creating, updating and reading a record the data is not just | |
# a simple XML document. Instead it is a serialized dataset that | |
# includes: | |
# | |
# * the schema | |
# * the actual data for the record | |
# * a "before" snapshot of the data | |
# (probably used to resolve conflicts) | |
# | |
# We don't really provide full support for this XML document but this | |
# class encapsulates enough of it for our purposes. | |
class DataSet | |
include NamespaceLookup | |
# Parsed from the schema | |
attr_reader :columns, :xml | |
# The XML document as it comes from GetEmptyForAdd or Get. Note | |
# that the xml needs to include the schema and diffgram but it | |
# does not need to be exclusive. It can have other stuff and we | |
# will just extract what we need. | |
def initialize xml | |
@xml = Nokogiri::XML xml | |
@columns = {} | |
extract_columns | |
# The before snapshot is not needed and would require more | |
# code to maintain so just remove it. | |
before = @xml.at_xpath('//diffgr:diffgram/diffgr:before', ns(:diffgr)) | |
before.remove if before | |
end | |
# Returns just the XML schema | |
def schema | |
@xml.at_xpath '//xs:schema', ns(:xs) | |
end | |
# The data part of the XML | |
def data | |
@xml.dup.at_xpath '//diffgr:diffgram', ns(:diffgr) | |
end | |
# Will return the attributes in the data. | |
def attributes | |
attrs = {} | |
@xml.at_xpath('//Table').children.each do |attribute| | |
name = attribute.name.snakecase.to_sym | |
value = @xml.at_xpath("//#{attribute.name}").content | |
type = columns[name][:field_type] | |
value = decode value, type | |
attrs[name] = value unless value.to_s.empty? || | |
type == 'dateTime' && value.is_a?(DateTime) && value.year == 1900 | |
end | |
attrs | |
end | |
# Will write the given attributes to the XML document. If the | |
# attribute node already exists simply update. If it does not | |
# exist the node will be created. | |
def write(attributes) | |
attributes.each do |attribute, value| | |
column = columns[attribute] | |
tag = if column | |
column[:soap_field] | |
else | |
attribute | |
end | |
attr_node = @xml.at_xpath "//#{tag}", ns(:diffgr) | |
attr_node = Nokogiri::XML::Node.new tag, @xml.document unless attr_node | |
attr_node.content = if column | |
encode value, column[:field_type] | |
else | |
value | |
end | |
attr_node.parent = @xml.at_xpath "//Table", ns(:diffgr) | |
end | |
end | |
# The dataset for a "New" record includes values for many fields. | |
# Some of these values are invalid if we submit them back to the | |
# API. This method can be used to clear all that old cruft. | |
def clear | |
@xml.at_xpath('//diffgr:diffgram/NewDataSet/Table', ns(:diffgr)).content = nil | |
end | |
private | |
# Will encode the given value according to the specified type | |
def encode value, type | |
case type | |
when 'string' then value.to_s | |
when 'decimal', 'float' then value.to_f | |
when 'int', 'short' then value.to_i | |
when 'boolean' | |
value = false if value.to_s == '0' || value == 'false' | |
value ? 1 : 0 | |
when 'dateTime' | |
value = DateTime.parse value rescue value | |
if value.respond_to? :strftime | |
if %w(hour min sec).all? {|f| value.send(f.to_sym) == 0} | |
value.strftime '%Y-%m-%d' | |
else | |
value.strftime('%Y-%m-%dT%H:%M:%S%z').insert(-3, ':') | |
end | |
else | |
value.to_s | |
end | |
end | |
end | |
# Will decode a value according to the specified type | |
def decode value, type | |
case type | |
when 'string' then value.to_s | |
when 'decimal', 'float' then value.to_f | |
when 'int', 'short' then value.to_i | |
when 'boolean' then | |
value = false if value.to_s == '0' || value == 'false' | |
!!value | |
when 'dateTime' then DateTime.parse value rescue value | |
end | |
end | |
# Will read the XML data and schema and store the column specs | |
# in a useful data structure. | |
def extract_columns | |
data = @xml.at_xpath '//Table' | |
return unless data | |
data.children.each do |attribute| | |
name = attribute.name.snakecase.to_sym | |
type = @xml.at_xpath( | |
"//xs:element[@name='#{attribute.name}']", ns(:xs) | |
)['type'].split(':').last | |
# Save column info for type casing and saving back to the | |
# correct soap field later. | |
@columns[name] = { | |
:field_type => type, | |
:soap_field => attribute.name | |
} | |
end | |
end | |
end | |
# Provides simplification of Savon::Client by pre-filling parameters | |
# and extra help when request is called. | |
# | |
# This is an abstract class concretely implemented as APIConnection | |
# and AdHocConnection. | |
class Connection < Savon::Client | |
include Singleton | |
DATABASE_NAME = ENV['MAXHIRE_DATABASE_NAME'] | |
SECURITY_KEY = ENV['MAXHIRE_SECURITY_KEY' ] | |
# Like Savon::Client.new only WSDL automatically supplied from | |
# subclass constant. | |
def initialize | |
super {wsdl.document = self.class.const_get('WSDL')} | |
end | |
# Like Savon::Client#request only auth automatically supplied and | |
# any error converted to raised Maxhire::Error. | |
# | |
# If a block is given then a Builder::XmlMarkup object will be | |
# passed to the block to allow addition tags to get appended to | |
# the body of the request. Also the soap object is passed as a | |
# second argument for more direct access. | |
def request method_name, &blk | |
begin | |
response = super method_name do | |
soap.namespaces["xmlns"] = 'http://www.maxhire.net/' | |
soap.header = { | |
'AuthHeader' => { | |
'DatabaseName' => DATABASE_NAME, | |
'SecurityKey' => SECURITY_KEY, | |
} | |
} | |
if block_given? | |
xml = Builder::XmlMarkup.new | |
if blk.arity == 2 | |
yield xml, soap | |
else | |
yield xml | |
end | |
soap.body = xml.target! unless xml.target!.empty? | |
end | |
end | |
response | |
rescue Savon::SOAP::Fault | |
raise Error.from_error_message($!) | |
end | |
end | |
end | |
# Most of the API calls are handled through this connection. | |
class APIConnection < Connection | |
WSDL = 'https://www.maxhire.net/MaxHireAPI/Services.asmx?wsdl' | |
# Automatically constructs API method from object and method | |
def request object_name, method_name, &blk | |
method_name = "#{object_name.to_s.snakecase}_#{method_name.to_s.snakecase}".to_sym | |
super method_name, &blk | |
end | |
end | |
# A few misc API calls are on this WSDL. The primary one being the | |
# ability to call any stored procedure. | |
class AdhocConnection < Connection | |
include NamespaceLookup | |
WSDL = 'https://www.maxhire.net/MaxHireAPI/UserServices.asmx?wsdl' | |
# Will execute the stored procedure given | |
def stored_proc(name, params={}) | |
# Get XML for params | |
param_xml = request :get_sql_params_data_set do |xml| | |
xml.lngNumOfRows params.keys.size | |
end | |
param_xml = Nokogiri::XML param_xml.to_xml | |
param_names = param_xml.xpath "//ParamName" | |
param_values = param_xml.xpath "//ParamValue" | |
params.each_with_index do |(key, value), idx| | |
param_names[idx].content = key | |
param_values[idx].content = value | |
end | |
param_xml | |
schema = param_xml.at_xpath '//xs:schema', ns(:xs) | |
data = param_xml.at_xpath '//diffgr:diffgram', ns(:diffgr) | |
# Actually execute API call | |
response = request :execute_custom_stored_procedure_get_results do |xml| | |
xml.dsParams do | |
xml << schema.to_s | |
xml << data.to_s | |
end | |
xml.strProcName name | |
end | |
# Return reasonable data structure from response mess | |
results = [] | |
Nokogiri::XML(response.to_xml).xpath('//Table').each do |row| | |
data = {} | |
row.children().each do |column| | |
data[column.name.snakecase.to_sym] = column.content | |
end | |
results << data | |
end | |
results | |
end | |
end | |
# Encapsulates an error coming from MaxHire. Adds an error code in | |
# addition to the message StandardError supplies. | |
class Error < StandardError | |
attr_accessor :code | |
# Will create a new error instance with the given code and message | |
def initialize msg, code=nil | |
self.code = code | |
super msg | |
end | |
# Will parse the error message and extract the maxhire code and error | |
def self.from_error_message fault | |
code, msg = *fault.to_s.scan(/MaxHire Error (\d+): (.+)$/).first | |
new msg, code | |
end | |
end | |
# A candidate | |
class Person < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:active => 1, | |
:candid_divisions_id => 1, | |
:candid_status_id => 18, | |
:candid_recruiter => 'N/A', | |
} | |
FIELD_TYPE_OVERRIDES = { | |
:work_emp_from => 'dateTime', | |
:work_emp_to => 'dateTime', | |
} | |
# Overwrite parent to support check for duplicate flag. By default | |
# the check is enabled. The flag does nothing if we are updating | |
# an existing record. | |
# | |
# Note that this flag will cause an error to be thrown if the | |
# user does already exist. | |
def save(check_for_duplicates=true) | |
super() do |xml| | |
xml.blnCheckForDuplicates 1 if new? && check_for_duplicates | |
end | |
end | |
end | |
# A document attached to a specific Person. Note that "cid" is the | |
# field that should store the foreign key of the person we are | |
# attached to. | |
# | |
# file_ext is a required file but no reasonable default can be | |
# provided. If the file_data responds to :original_filename (common | |
# with uploaded files) or :path then the filename will be automatically | |
# extracted and the extension will be automatically extracted from | |
# that if a extension has not already been specified. But if the | |
# filename/extension cannot be determined then it must be supplied | |
# by the calling code or a validation error will be generated. | |
class Document < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:doctypes_id => 1, | |
:docs_divisions_id => 1, | |
} | |
# Will pull down the actual file. | |
def file | |
response = APIConnection.instance.request :document, :get_file do |xml| | |
xml.tag! "intDocumentId", id | |
end | |
response = Nokogiri::XML(response.to_xml) | |
response = response.at_xpath '//xmlns:Document_GetFileResult', ns(:xmlns) | |
Base64.decode64 response.content if response | |
end | |
# Overwritten to support additional info: | |
# | |
# file_data:: | |
# The actual data that represents the file. This can be either | |
# raw data or a IO object that supports the read method. Leave | |
# blank if doing an update and only changing the meta data. | |
# resume:: | |
# Set to true if this document should be the person's default | |
# resume. | |
# check_for_duplicates:: | |
# Will raise an error if the system detects this file already | |
# exists. Does nothing if we are updating an existing record. | |
def save(file_data='', default_resume=false, check_for_duplicates=true) | |
raise Error, 'File not provided' if new? && !file_data | |
file_name = file_data.original_filename if file_data.respond_to? :original_filename | |
file_name = file_data.path if !file_name && file_data.respond_to?(:path) | |
self.file_ext ||= File.extname(file_name)[1..-1] if file_name | |
file_data = file_data.read if file_data.respond_to? :read | |
file_data = Base64.encode64(file_data).gsub! /\n/, "" | |
super() do |xml| | |
xml.bytFile file_data | |
xml.blnSetDefaultResume 1 if default_resume | |
xml.blnCheckForDuplicates 1 if new? && check_for_duplicates | |
end | |
end | |
# Override since it does not follow pattern of other objects | |
def self.id_param | |
'docs_id' | |
end | |
end | |
# An application to a job by a candidate. | |
# | |
# The following two fields are required and there are not reasonable | |
# defaults so they must be provided before save is called: | |
# | |
# reference:: The foreign key for the job being applied to | |
# id:: The foreign key to the company being applied to | |
class Application < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:profile_status_id => 8, | |
:divisions_id => 1, | |
:priority => 4, | |
} | |
end | |
# An interview with a candidate for a job. | |
class Interview < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:ref_divisions_id => 1, | |
} | |
end | |
# A fulfilled job with a candidate. | |
class Placement < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:placedivisions_id => 1, | |
} | |
# Override to provide a default value (today) for dateenter | |
def save | |
self.dateenter ||= Date.today | |
super | |
end | |
end | |
# An organization that a job belongs to | |
class Company < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:contacts_divisions_id => 1, | |
:company_status_id => 1, | |
:comp_recruiter => 'N/A', | |
} | |
# Override since it does not follow pattern of other objects | |
def self.id_param | |
'intId' | |
end | |
end | |
# A job that a candidate might apply for | |
class Job < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:jobs_divisions_id => 1, | |
:id => 1, | |
:jobs_counselor => 'N/A', | |
} | |
# Override since it does not follow pattern of other objects | |
def self.id_param | |
'intReference' | |
end | |
end | |
# Some sort of activity log on a candidate. The candidate foreign | |
# key is stored in the field 'cid' and is required to save a record. | |
class Activity < Base | |
# Fields that require a value in order to create a valid record | |
# even though there really is no valid value. | |
DEFAULTS = { | |
:activity_divisions_id => 1, | |
:activitycounsel => 'N/A', | |
} | |
end | |
end |
This code is now running in production on http://www.gotoagile.com. So it is pretty decent but still should be consider beta quality as this is the only place the code is used and our usage is still limited (People a good bit, Application and Document a little).
Hi,
Thanks for this great wrapper.
In Document class it should be:
xml.tag! "intDocumentId", docs_id
Thanks
@kleine2 - Thanks for the suggestion. The "id" is usually intOBJECTNAMEId (replace OBJECTNAME with the name of the object). But there are exceptions like Company (where it is just intId). So I made a hook that lets an object specify the name of the id field (again see the Company object for an example). So is your comment saying that I should change Document to have add the following:
# Override since it does not follow pattern of other objects
def self.id_param
'docs_id'
end
That way using "id" will be valid and we don't need to change the line of code you suggested. Also any other place where we pull the id will work as well. Let me know if that sounds right and I will make the change. I don't use this API on a regular basis so it would be nice to get some confirmation from someone actively developing with it before I make the change.
That's good
Do you also need to use the same hook when you check for a new object in new?
Since the parameter is not called id (in Application it's called job_prof_id and the id is something else - the company id)
what code we change for that ?
@kleine2 - Took a look at what your comment so I can update the code and now I'm not sure my previous comment really addressed what you were saying before. The original line you said to add. Where were you meaning that to be added? How is "docs_id" in your original code initialized? It it an attribute of the document coming from Maxhire?
In the file method in the Document class I made this change:
def file
response = APIConnection.instance.request :document, :get_file do |xml|
xml.tag! "intDocumentId", docs_id
end
....
There are a whole bunch of other issues I am dealing with in a more get it done type of way (versus more generic) especially with the Application object.
Are you still invested into this work? If you are, maybe we could discuss some details more privately.
@kleine2 - After looking again I think my original comment is correct. I just needed to override id_param
so it would know which field stores the id
for Document
. I never ran into this myself because I only create Document, I never deal with existing documents.
Regarding your question about new?
I don't believe you need to change anything. Line 249 will assign the id with the correct value, even if there is an "attribute" called "id" which is not the unique identifier (that id becomes not accessible using the generated getter/setter but you can access it via [] and []=).
I am not currently invested in the work as the project it was created for is over for now. But if that project opens back up I may become invested in it again. Or if another project comes along with Maxhire I could become invested in it again. So not a personal passion but something I will work on as part of a paying project.
If you have some changes my suggestion is to just fork my Gist into your own. That way others can compare and contrast. I don't mind handing over the maintenance of this library to someone else but in case I do become invested in it again I would prefer it not be updated in a "get it done type of way". If I do become invested again I will certainly review your fork to incorporate the patches (in a way that is as clean as possible). Also if there is interest from others to turn this into a real library (i.e. a real git project) and maintain it in a clean way that is fine with me as well.
Although some basic testing has been done on all the objects,
Maxhire::Person
is the only object with real use. So issues may be encountered in the other objects.I am a little unsure about the values I am passing to the various DEFAULTS constant in each object. Basically Maxhire has fields that are required despite there often being no decent default value. These constants provide a default that works so they don't have to be specified by the calling code. But I have not got verification that these defaults are the preferred defaults from anybody. They are just what worked for me.