public
Description: OneBody is web-based software that connects community members, especially churches, on the web.
Homepage: http://beonebody.com
Clone URL: git://github.com/seven1m/onebody.git
onebody / lib / connectors / coms.rb
100644 287 lines (270 sloc) 10.982 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# Coms Connector
# run with: script/sync coms path/to/comsdata
 
# This connector is used by Cedar Ridge Christian Church to connect with
# an old system written in FoxPro called COMS. If writing a connector for
# your own external database, please note it might not be nearly this
# complicated. Some voodoo is done here because of the nature of working
# with the COMS database and tools we have to do it.
 
# Refer to base.rb in this directory for a template connector
 
require File.dirname(__FILE__) + '/base'
require 'rubygems'
require 'dbf'
 
class ComsConnector < ExternalDataConnector
  BEG_CLASS_WEEK_NUM = 25
  
  def initialize(db_path)
    # set up database tables
    @db = {}
    {
    # name filename in memory
      :people => ['ssmember.DBF', false],
      :postal => ['sspostal.DBF', true ],
      :phone => ['ssphone.DBF', true ],
      :categories => ['sscatcod.DBF', false],
      :classes => ['sswclass.DBF', false],
      :board => ['ssboard.DBF', false],
      :service => ['sservice.DBF', false],
      :families => ['ssfamily.DBF', false]
    }.each do |name, details|
      file, in_memory = details
      @db[name] = DBF::Table.new(File.join(db_path, file), :in_memory => in_memory)
    end
 
    precache_class_data
 
    nil
  end
  
  def people_ids
    unless @people_ids
      @people_ids = []
      @db[:people].each_record do |record|
        if not (record.deceased or record.info_5 =~ /deny/i or record.familyname =~ /church$/i)
          @people_ids << record.memberid
        end
      end
    end
    @people_ids
  end
  
  def family_ids
    unless @family_ids
      @family_ids = []
      @db[:families].each_record do |record|
        if record.familyname !~ /church$/i
          @family_ids << record.familyid
        end
      end
    end
    @family_ids
  end
  
  def each_person(updated_since)
    updated_since -= 1.day # because the COMS update time is a date only (time is midnight)
    people_ids # get all ids for completion reporting
    index = 0
    @db[:people].each_record do |record|
      new_data = false
      if not (record.deceased or record.info_5 =~ /deny/i or record.familyname =~ /church$/i)
        print "\r%.02f%% complete" % (index/@people_ids.length.to_f*100.0/2.0)
        member_phone_record = @db[:phone].find(:first, 'MEMBERID' => record.memberid)
        new_data = true if member_phone_record and member_phone_record.updates > updated_since
        family_phone_record = @db[:phone].find(:first, 'FAMILYID' => record.familyid)
        new_data = true if family_phone_record and family_phone_record.updates > updated_since
        classes = []
        @classes[record.memberid].to_a.each do |class_cat, updates|
          new_data = true if updates > updated_since
          classes << class_cat
        end
        can_sign_in = %w(M A P Y O C V).include?(record.mailgroup) or record.info_5 =~ /allow/i
        new_data = true if record.updates > updated_since
        if new_data
          yield({
            :legacy_id => record.memberid,
            :legacy_family_id => record.familyid,
            :sequence => record.fam_seq,
            :gender => record.sex,
            :first_name => record.nickname || record.first,
            :last_name => record.last =~ /,\s/ ? record.last.split(', ').first : record.last,
            :suffix => record.last =~ /,\s/ ? record.last.split(', ').last : nil,
            :mobile_phone => get_phone('CELLULAR', 'CELL_EXT', 'CELL_UNL', [member_phone_record, family_phone_record]),
            :work_phone => get_phone('WORKPHONE', 'WORK_EXT', 'WORK_UNL', [record]),
            :fax => get_phone('FAX', 'FAX_EXT', 'FAX_UNL', [member_phone_record, family_phone_record]),
            :birthday => to_datetime(record.birthday),
            :email => (e = record.email.to_s.strip.downcase).any? ? e : nil,
            :classes => classes.to_a.join(','),
            :mail_group => record.mailgroup == '(None)' ? nil : record.mailgroup,
            :anniversary => to_datetime(record.weddate),
            :member => (record.date1 and not %w(N F).include?(record.mailgroup)),
            :staff => record.email =~ /@cedarridgecc\.com$/,
            :elder => classes =~ /[\b,]BEL[\b,]/,
            :deacon => false,
            :can_sign_in => can_sign_in,
            :visible_to_everyone => can_sign_in,
            :visible_on_printed_directory => %w(M A).include?(record.mailgroup),
            :full_access => (%w(M A C).include?(record.mailgroup) or record.info_5 =~ /allow/i),
            :can_pick_up => record.info_10,
            :cannot_pick_up => record.info_11,
            :medical_notes => record.info_12,
            :barcode_id => record.memberid
          })
        end
        index += 1
      end
    end
    nil
  end
  
  def each_family(updated_since)
    updated_since -= 1.day # because the COMS update time is a date only (time is midnight)
    family_ids
    index = 0
    @db[:families].each_record do |record|
      new_data = false
      if record.familyname !~ /church$/i
        print "\r%.02f%% complete" % (index/@family_ids.length.to_f*100.0/2.0+50.0)
        family_phone_record = @db[:phone].find(:first, 'FAMILYID' => record.familyid)
        new_data = true if family_phone_record and family_phone_record.updates > updated_since
        family_postal_record = @db[:postal].find(:first, 'FAMILYID' => record.familyid)
        new_data = true if family_postal_record and family_postal_record.updates > updated_since
        new_data = true if record.updates > updated_since
        if new_data
          yield({
            :legacy_id => record.familyid,
            :name => record.familyname,
            :last_name => record.last =~ /,\s/ ? record.last.split(', ').first : record.last,
            #:suffix => record.last =~ /,\s/ ? record.last.split(', ').last : nil,
            :address1 => family_postal_record ? family_postal_record.address1 : nil,
            :address2 => family_postal_record ? family_postal_record.address2 : nil,
            :city => family_postal_record ? family_postal_record.city : nil,
            :state => family_postal_record ? family_postal_record.state : nil,
            :zip => family_postal_record ? family_postal_record.zip.to_s[0..9] : nil,
            :home_phone => get_phone('HOMEPHONE', nil, 'UNLISTED', [family_phone_record]),
            :email => (e = record.internet.to_s.strip.downcase).any? ? e : nil
          })
        end
        index += 1
      end
    end
    print "\r100.00% complete\n"
    nil
  end
  
  private
    # hocus pocus to build a complete phone number from a series of records + columns
    # each record is tried in order to get the desired outcome
    # if the number is unlisted, the next record is tried
    def get_phone(phone_attr, ext_attr, unlisted_attr, records)
      phone = nil
      while records.any?
        record = records.shift
        if (
          record and
          (not unlisted_attr or not record.attributes[unlisted_attr]) and
          p = record.attributes[phone_attr] and
          p.gsub(/\s/, '').length >= 7
        )
          phone = p
          phone += ' ' + record.attributes[ext_attr].to_s if record.attributes[ext_attr].to_s.any?
          break
        end
      end
      return phone
    end
    
    def precache_class_data
      logger.info 'loading class attendance/membership data'
      logger.info ' categories'
      @class_cats = []
      @board_cats = []
      @service_cats = []
      @db[:categories].each_record do |record|
        case record.modulename
        when 'CA-CLAS-CATE'
          @class_cats << record.category
        when 'CA-BOAR-CATE'
          @board_cats << record.category
        when 'CA-SERV-CATE'
          @service_cats << record.category
        end
      end
      
      @classes = {}
      logger.info ' classes'
      years = [Date.today.year.to_s, (Date.today.year-1).to_s]
      @db[:classes].each_record do |record|
        @classes[record.memberid] ||= []
        if @class_cats.include?(record.category) and years.include?(record.year.to_s)
          @classes[record.memberid] << ['C'+record.category, record.updates]
        end
      end
      logger.info ' board'
      @db[:board].each_record do |record|
        @classes[record.memberid] ||= []
        if @board_cats.include?(record.category) and record.category !~ /^[0-9]/
          @classes[record.memberid] << ['B'+record.category, record.updates]
        end
      end
      logger.info ' service'
      @db[:service].each_record do |record|
        @classes[record.memberid] ||= []
        if @service_cats.include?(record.category)
          @classes[record.memberid] << ['S'+record.category, record.updates]
        end
      end
    end
 
    def to_datetime(time)
      if time
        DateTime.new(time.year, time.month, time.day) rescue nil
      end
    end
end
 
module DBF
  class Record
    private
    # fix bug in DBF code (or workaround bug in Coms dbf files; I don't know :-)
    def initialize_values(columns)
      columns.each do |column|
        case column.type
        when 'I' # added by Tim - I don't understand this much, but it seems to work
          @attributes[column.name] = @data.read(column.length).unpack("I").first
        when 'N' # number
          @attributes[column.name] = column.decimal.zero? ? unpack_string(column).to_i : unpack_string(column).to_f
        when 'D' # date
          raw = unpack_string(column).strip
          unless raw.empty?
            begin
              parts = raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}
              @attributes[column.name] = Time.gm(*parts)
            rescue
              parts = raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}
              @attributes[column.name] = Date.new(*parts)
            end
          end
        when 'M' # memo
          starting_block = unpack_string(column).to_i
          @attributes[column.name] = read_memo(starting_block)
        when 'L' # logical
          @attributes[column.name] = unpack_string(column) =~ /^(y|t)$/i ? true : false
        else
          @attributes[column.name] = unpack_string(column).strip
        end
      end
    end
    # don't know why, but accessors stopped working for me.
    def define_accessors
      @table.columns.each do |column|
        underscored_column_name = underscore(column.name)
        if @table.options[:accessors]
          self.class.send :define_method, underscored_column_name do
            @attributes[column.name]
          end
          @@accessors_defined = true
        end
      end
    end
  end
  class Table
    # more efficient iterator (so we don't load everything)
    def each_record
      if options[:in_memory] and @records
        @records.each { |r| yield(r) }
      else
        0.upto(@record_count - 1) do |n|
          seek_to_record(n)
          yield(DBF::Record.new(self)) unless deleted_record?
        end
      end
    end
  end
end