PageRenderTime 84ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/bbc.py

http://github.com/ksdtech/bbc
Python | 654 lines | 611 code | 20 blank | 23 comment | 15 complexity | dfa3e94b0b77c6c5c872b23f0e93495e MD5 | raw file
  1. # bbc.py
  2. # generate students.csv and staff.csv nightly and upload them to Blackboard Connect
  3. import certifi
  4. import csv
  5. import os.path
  6. import pycurl
  7. import re
  8. from StringIO import StringIO
  9. import sys
  10. import traceback
  11. # Application settings (passwords, etd, not in git repository)
  12. from app_config import (
  13. regFormStartDate, verboseFlag, sourceDir, uploadDir,
  14. strUserName, strUserPass, destURL)
  15. # Global settings (TODO: put these in command line arguments)
  16. # input file is only pre-regs
  17. allPreRegs = False
  18. # input file is only graduates
  19. allGraduates = False
  20. # allow pre-reg groups
  21. preRegGroups = False
  22. if allPreRegs:
  23. preRegGroups = True
  24. # Allow online reg status groups
  25. # 0 - do not add these groups
  26. # 1 - check pre-regs (before EOY)
  27. # 2 - check active (after EOY)
  28. regStatusGroups = 0
  29. if allPreRegs or allGraduates:
  30. regStatusGroups = 0
  31. # Algorithm to select PrimaryPhone and AdditionalPhone, as available
  32. staff_phone_prefs = [ 'cell', 'home_phone' ]
  33. student_phone_prefs = [ 'mother_cell', 'father_cell', 'home_phone',
  34. 'mother_work_phone', 'father_work_phone' ]
  35. student_sms_phone_prefs = [ 'mother_cell', 'father_cell' ]
  36. # Settings for with pycurl
  37. cookieFile = 'cookies.txt'
  38. headerFile = 'headers.txt'
  39. libcurlFile = 'curl.c'
  40. traceFile = 'trace.txt'
  41. # bbc-students AutoSend fields
  42. # tab field delimiter, lf line delimiter, no headers
  43. autosend_student_fields = [s.strip() for s in '''
  44. Student_Number
  45. First_Name
  46. Last_Name
  47. Grade_Level
  48. Gender
  49. HomeRoom_Teacher
  50. SchoolID
  51. Home_Phone
  52. Mother_Work_Phone
  53. Mother_Cell
  54. Father_Work_Phone
  55. Father_Cell
  56. Mother_Email
  57. Father_Email
  58. Home2_Phone
  59. Mother2_Work_Phone
  60. Mother2_Cell
  61. Father2_Work_Phone
  62. Father2_Cell
  63. Mother2_Email
  64. Father2_Email
  65. Enroll_Status
  66. Network_Id
  67. Network_Password
  68. Web_Id
  69. Web_Password
  70. Family_Ident
  71. Mother_Staff_Id
  72. Mother_First
  73. Mother
  74. Father_Staff_Id
  75. Father_First
  76. Father
  77. Student_Web_Id
  78. Mother2_Staff_Id
  79. Mother2_First
  80. Mother2_Last
  81. Father2_Staff_Id
  82. Father2_First
  83. Father2_Last
  84. EntryCode
  85. Lang_Adults_Primary
  86. CA_ELAStatus
  87. Reg_Will_Attend
  88. Reg_Grade_Level
  89. ExitCode
  90. Form3_Updated_At
  91. Form4_Updated_At
  92. Form6_Updated_At
  93. Form9_Updated_At
  94. Form10_Updated_At
  95. Form15_Updated_At
  96. Form1_Updated_At
  97. Form16_Updated_At
  98. TK_Current_Year
  99. CA_PrimDisability
  100. CA_SpEd504
  101. '''.split('\n')[1:-1]]
  102. student_nfields = len(autosend_student_fields)
  103. # bbc-staff AutoSend fields
  104. # tab field delimiter, lf line delimiter, no headers
  105. autosend_staff_fields = [s.strip() for s in '''
  106. Status
  107. Staffstatus
  108. Title
  109. Teachernumber
  110. First_Name
  111. Last_Name
  112. Gender
  113. Schoolid
  114. Home_Phone
  115. Cell
  116. Email_Addr
  117. Email_Personal
  118. Network_Id
  119. Network_Password
  120. Group_Membership
  121. '''.split('\n')[1:-1]]
  122. staff_nfields = len(autosend_staff_fields)
  123. # headers for staff file
  124. # 6 groups
  125. staffHeaders = [s.strip() for s in '''
  126. ReferenceCode
  127. FirstName
  128. LastName
  129. HomePhone
  130. MobilePhone
  131. SMSPhone
  132. PrimaryPhone
  133. AdditionalPhone
  134. EmailAddress
  135. Institution
  136. RefreshGroup
  137. RefreshGroup
  138. RefreshGroup
  139. RefreshGroup
  140. RefreshGroup
  141. RefreshGroup
  142. '''.split('\n')[1:-1]]
  143. # headers for students file
  144. # 6 groups
  145. studentHeaders = [s.strip() for s in '''
  146. ReferenceCode
  147. FirstName
  148. LastName
  149. Grade
  150. Language
  151. Gender
  152. HomePhone
  153. WorkPhone
  154. MobilePhone
  155. HomePhoneAlt
  156. WorkPhoneAlt
  157. MobilePhoneAlt
  158. SMSPhone
  159. SMSPhone2
  160. PrimaryPhone
  161. AdditionalPhone
  162. EmailAddress
  163. EmailAddressAlt
  164. Institution
  165. RefreshGroup
  166. RefreshGroup
  167. RefreshGroup
  168. RefreshGroup
  169. RefreshGroup
  170. RefreshGroup
  171. '''.split('\n')[1:-1]]
  172. # headers for students file
  173. # 6 groups
  174. studentNoRefreshHeaders = [s.strip() for s in '''
  175. ReferenceCode
  176. FirstName
  177. LastName
  178. Grade
  179. Language
  180. Gender
  181. HomePhone
  182. WorkPhone
  183. MobilePhone
  184. HomePhoneAlt
  185. WorkPhoneAlt
  186. MobilePhoneAlt
  187. SMSPhone
  188. SMSPhone2
  189. PrimaryPhone
  190. AdditionalPhone
  191. EmailAddress
  192. EmailAddressAlt
  193. Institution
  194. Group
  195. Group
  196. Group
  197. Group
  198. Group
  199. Group
  200. '''.split('\n')[1:-1]]
  201. # Get first and second phone depending on user type
  202. def get_outreach_phones(phones, prefs):
  203. primary_phone = ''
  204. additional_phone = ''
  205. for key in prefs:
  206. test = phones[key]
  207. if test != '':
  208. if primary_phone == '':
  209. primary_phone = test
  210. elif additional_phone == '' and test != primary_phone:
  211. additional_phone = test
  212. break
  213. return (primary_phone, additional_phone)
  214. # Convert one row of staff data.
  215. # WARNING: If you change AutoSend fields, you must change the logic in this function
  216. def writestaffrow(out, row, fname, lno, group):
  217. if len(row) < staff_nfields:
  218. sys.stderr.write('%s line %d - row not parsed?\n' % (fname, lno))
  219. sys.stderr.write('%d staff fields, %d fields in row\n' % (staff_nfields, len(row)))
  220. sys.stderr.write('%s\n' % row)
  221. return
  222. # staff fields
  223. status = int(row[0]) if row[0] else 0
  224. staffstatus = int(row[1]) if row[1] else 0
  225. title = row[2].lower()
  226. teachernumber = row[3] or '000000'
  227. first_name = row[4]
  228. last_name = row[5]
  229. if status == 1 and title != 'v-staff':
  230. groups = [ ]
  231. if group:
  232. groups.append(group)
  233. # Add to groups by staffstatus field:
  234. # staffstatus 1 is certificated, teacher
  235. # staffstatus 5 is certificated, non-teacher
  236. if staffstatus == 1 or staffstatus == 5:
  237. groups.append('Certificated')
  238. # staffstatus 2 is classified
  239. elif staffstatus == 2:
  240. groups.append('Classified')
  241. # staffstatus 6 is contracted staff
  242. # staffstatus 7 is intern
  243. elif staffstatus == 6 or staffstatus == 7:
  244. groups.append('Temporary')
  245. # Not put in groups:
  246. # staffstatus 0 is unassigned
  247. # staffstatus 3 is lunch staff
  248. # staffstatus 4 is substitute
  249. # staffstatus 8 is volunteer
  250. # staffstatus 9 is trustee
  251. # Also put in groups by group membership
  252. group_membership = row[14].lower()
  253. if 'administrators' in group_membership:
  254. groups.append('Administrators')
  255. if 'trustees' in group_membership:
  256. groups.append('Board')
  257. if len(groups) > 0:
  258. gender = row[6].upper()
  259. # any staff not assigned to school goes into District Office code 102
  260. schoolid = row[7]
  261. if schoolid != '103' and schoolid != '104':
  262. schoolid = '102'
  263. phones = {
  264. 'home_phone': row[8],
  265. 'cell': row[9]
  266. }
  267. primary_phone, additional_phone = get_outreach_phones(phones, staff_phone_prefs)
  268. email_address = row[10]
  269. while len(groups) < 6:
  270. groups.append('')
  271. # output a row that matches staffHeaders fields
  272. # use cell for SMSPhone
  273. # use cell, then home phone for Primary and Alternate
  274. out.writerow([teachernumber, first_name, last_name,
  275. phones['home_phone'], phones['cell'], phones['cell'],
  276. primary_phone, additional_phone,
  277. email_address, schoolid] + groups[:6])
  278. if verboseFlag > 2:
  279. sys.stderr.write('row written\n')
  280. elif verboseFlag > 1:
  281. sys.stderr.write('%s %s %s: no staff groups\n' %
  282. (teachernumber, first_name, last_name))
  283. elif verboseFlag > 1:
  284. sys.stderr.write('%s %s %s: not a current staff member\n' %
  285. (teachernumber, first_name, last_name))
  286. # convert row. if autosend fields are changed, you must change the logic
  287. # in this function
  288. def writestudentrow(out, row, fname, lno, group):
  289. if len(row) < student_nfields:
  290. sys.stderr.write('%s line %d - row not parsed?\n' % (fname, lno))
  291. sys.stderr.write('%d student fields, %d fields in row\n' % (student_nfields, len(row)))
  292. sys.stderr.write('%s\n' % row)
  293. return
  294. entrycode = row[40]
  295. if 'NS' == entrycode or 'NR' == entrycode:
  296. return
  297. # student fields
  298. language = 'English'
  299. groups = [ ]
  300. student_number = row[0]
  301. first_name = row[1]
  302. last_name = row[2]
  303. sped_disability = row[55]
  304. is_504 = row[56]
  305. gender = row[4].upper()
  306. teacher = row[5]
  307. schoolid = row[6]
  308. phones = {
  309. 'home_phone': row[7],
  310. 'mother_work_phone': row[8],
  311. 'mother_cell': row[9],
  312. 'father_work_phone': row[10],
  313. 'father_cell': row[11]
  314. }
  315. primary_phone, additional_phone = get_outreach_phones(phones, student_phone_prefs)
  316. sms_phone, sms_phone_2 = get_outreach_phones(phones, student_sms_phone_prefs)
  317. mother_email = row[12]
  318. father_email = row[13]
  319. enroll_status = int(row[21])
  320. # TODO: add support for Spanish
  321. # lang_adults_primary = row[42-1] or '00'
  322. # if lang_adults_primary == '01' then language = 'Spanish' end
  323. ela_status = row[42] or 'EO'
  324. will_attend = row[43]
  325. # complicated grade level settings
  326. grade_level = row[3]
  327. reg_grade_level = row[44]
  328. is_tk = row[54]
  329. if allPreRegs or (enroll_status < 0):
  330. is_tk = (reg_grade_level == 'TK')
  331. if int(grade_level) == 0:
  332. if is_tk:
  333. grade_level = 'TK'
  334. else:
  335. grade_level = 'RK'
  336. if group:
  337. groups.append(group)
  338. if is_tk:
  339. groups.append('Transitional Kindergarten')
  340. if ela_status == 'EL':
  341. groups.append('ELAC')
  342. if sped_disability != '':
  343. groups.append('SPED')
  344. elif is_504:
  345. groups.append('504')
  346. if allGraduates or schoolid == '999999':
  347. schoolid = '104'
  348. grade_level = '9'
  349. groups.append('Graduates')
  350. if schoolid == '103' or schoolid == '104':
  351. is_new = 'ND' == entrycode or 'RD' == entrycode
  352. if (not allPreRegs) and (is_new or enroll_status < 0):
  353. groups.append('New Students')
  354. if preRegGroups and (allPreRegs or (enroll_status < 0)):
  355. if grade_level in ['5','6','7','8']:
  356. schoolid = '104'
  357. groups.append('Pre-Registered 5-8')
  358. else:
  359. schoolid = '103'
  360. if grade_level == 'TK':
  361. groups.append('Pre-Registered TK')
  362. elif grade_level == 'RK':
  363. groups.append('Pre-Registered K')
  364. else:
  365. groups.append('Pre-Registered 1-4')
  366. if regStatusGroups > 0:
  367. attending = True
  368. if regStatusGroups == 1:
  369. will_attend = row[43]
  370. if 'nr-' in will_attend:
  371. # not returning
  372. attending = False
  373. groups.append('Registration Will Be Exiting')
  374. if attending:
  375. # blank (unknown) or returning
  376. pages_completed = 0
  377. pages_required = 0
  378. # check forms 3, 4, 6, 9 and 10 only for now
  379. for k in range(46, 51):
  380. date = row[k] or '0000-00-00'
  381. date = date[:10]
  382. pages_required = pages_required + 1
  383. # WARNING: hard coded date!
  384. if date >= regFormStartDate:
  385. pages_completed = pages_completed + 1
  386. if pages_completed == 0:
  387. groups.append('Registration Not Started')
  388. elif pages_completed < pages_required:
  389. groups.append('Registration Partially Complete')
  390. while len(groups) < 6:
  391. groups.append('')
  392. # output a row that matches studentHeaders fields for primary family
  393. # use mother and father cells for SMSPhones
  394. # use cell, then home phone for Primary and Alternate
  395. # possible groups are Graduates, New Students, ELAC
  396. out.writerow([student_number, first_name, last_name, grade_level, language, gender,
  397. phones['home_phone'], phones['mother_work_phone'], phones['mother_cell'], '',
  398. phones['father_work_phone'], phones['father_cell'],
  399. sms_phone, sms_phone_2,
  400. primary_phone, additional_phone,
  401. mother_email, father_email, schoolid] + groups[:6])
  402. if verboseFlag > 2:
  403. sys.stderr.write('row written\n')
  404. phones2 = {
  405. 'home_phone': row[14],
  406. 'mother_work_phone': row[15],
  407. 'mother_cell': row[16],
  408. 'father_work_phone': row[17],
  409. 'father_cell': row[18]
  410. }
  411. if phones2['home_phone'] or phones2['mother_work_phone'] or phones2['mother_cell'] or phones2['father_work_phone'] or phones2['father_cell']:
  412. # for secondary family, reference code starts with 'NC'
  413. nc_reference = 'NC' + student_number
  414. primary_phone2, additional_phone2 = get_outreach_phones(phones2, student_phone_prefs)
  415. sms_phone2, sms_phone2_2 = get_outreach_phones(phones2, student_sms_phone_prefs)
  416. mother2_email = row[19]
  417. father2_email = row[20]
  418. # output a row that matches studentHeaders fields for secondary family
  419. out.writerow([nc_reference, first_name, last_name, grade_level, language, gender,
  420. phones2['home_phone'], phones2['mother_work_phone'], phones2['mother_cell'], '',
  421. phones2['father_work_phone'], phones2['father_cell'],
  422. sms_phone2, sms_phone2_2,
  423. primary_phone2, additional_phone2,
  424. mother2_email, father2_email, schoolid] + groups[:6])
  425. if verboseFlag > 2:
  426. sys.stderr.write('NC row written\n')
  427. # Process a tab-delimited input file, calling rowfn on each row
  428. # If headers are given, row ia a dict, otherwise row is a list
  429. def readtab(fname, headers, rowfn, out, group):
  430. with open(os.path.join(sourceDir, fname), 'rb') as io:
  431. lno = 0
  432. calling = None
  433. if headers:
  434. lno = 1
  435. cin = csv.DictReader(io, fieldnames=headers,
  436. delimiter='\t', lineterminator='\n', quoting=csv.QUOTE_NONE)
  437. else:
  438. cin = csv.reader(io,
  439. delimiter='\t', lineterminator='\n', quoting=csv.QUOTE_NONE)
  440. for raw_row in cin:
  441. try:
  442. if headers:
  443. row = { }
  444. for key, value in raw_row.iteritems():
  445. row[key] = value.strip()
  446. rowfn(out, row, fname, lno, group)
  447. else:
  448. row = [value.strip() for value in raw_row]
  449. rowfn(out, row, fname, lno, group)
  450. except:
  451. if verboseFlag > 0:
  452. e = sys.exc_info()[0]
  453. sys.stderr.write('%s row %d invalid: %s\n' % (fname, lno, e))
  454. sys.stderr.write('%s\n' % raw_row)
  455. traceback.print_exc()
  456. break
  457. lno += 1
  458. # Convert PowerSchool AutoSend files to csv format required by Blackboard Connect
  459. def create_csv_file(psFile, csvFile, headers, rowfn, group):
  460. with open(os.path.join(uploadDir, csvFile), 'wb') as io:
  461. # write out with CRLF
  462. out = csv.writer(io,
  463. delimiter=',', lineterminator='\r\n', quoting=csv.QUOTE_ALL)
  464. out.writerow(headers)
  465. readtab(psFile, False, rowfn, out, group)
  466. # used with curl command
  467. cookieFile = 'cookies.txt'
  468. headerFile = 'headers.txt'
  469. libcurlFile = 'curl.c'
  470. traceFile = 'trace.txt'
  471. # libcurlLoaded = false
  472. # headers to send for file upload
  473. # copied from WinHttp.WinHttpRequest component defaults
  474. # Connection: Keep-Alive is the key header!
  475. # Expect: removes the Expext: 100-continue header
  476. httpHeaders = [
  477. 'Accept: */*',
  478. 'User-Agent: Mozilla/4.0 (compatible; Win32; WinHttp.WinHttpRequest.5)',
  479. 'Version: HTTP/1.1',
  480. 'Connection: Keep-Alive',
  481. 'Expect:'
  482. ]
  483. # Destroy previous cookies
  484. def clear_cookie_file():
  485. with open(os.path.join(uploadDir, cookieFile), 'wb') as c:
  486. c.write('# Netscape HTTP Cookie File\n')
  487. # contactType: 'Student' or 'Staff'
  488. # preserveData: true to remove records that aren't uploaded
  489. def upload_file(uploadFile, contactType, preserveData):
  490. clear_cookie_file()
  491. resp_headers = StringIO()
  492. resp_body = StringIO()
  493. c = pycurl.Curl()
  494. c.setopt(c.HTTPHEADER, httpHeaders)
  495. c.setopt(c.COOKIEJAR, os.path.join(uploadDir, cookieFile))
  496. c.setopt(c.COOKIEFILE, os.path.join(uploadDir, cookieFile))
  497. c.setopt(c.FOLLOWLOCATION, True)
  498. # Use certifi package to provide SSL CA chain
  499. c.setopt(c.CAINFO , certifi.where());
  500. # c.setopt(autoreferer, True1)
  501. # need a post301 / post302 flag?
  502. postdata = [
  503. ('fNTIUser', strUserName),
  504. ('fNTIPass', strUserPass),
  505. ('fContactType', contactType),
  506. ('fRefreshType', contactType),
  507. ('fPreserveData', '1' if preserveData else '0'),
  508. ('fSubmit', '1'),
  509. ('fFile', (
  510. c.FORM_FILE, os.path.join(uploadDir, uploadFile),
  511. c.FORM_CONTENTTYPE, 'text/plain'
  512. ))
  513. ]
  514. # Sets request method to POST and data to list of tuples
  515. c.setopt(c.HTTPPOST, postdata)
  516. c.setopt(c.HEADERFUNCTION, resp_headers.write)
  517. c.setopt(c.WRITEFUNCTION, resp_body.write)
  518. c.setopt(c.URL, destURL)
  519. if verboseFlag > 0:
  520. sys.stderr.write('Posting to %s with data:\n%s\n' % (destURL, postdata))
  521. result = c.perform()
  522. code = c.getinfo(c.HTTP_CODE)
  523. resp = resp_body.getvalue()
  524. if verboseFlag > 0:
  525. sys.stderr.write('%s\n' % resp)
  526. responseFile = re.sub(r'[.].*$', '-response.html', uploadFile)
  527. with open(os.path.join(uploadDir, responseFile), 'wb') as io:
  528. io.write('%s' % resp)
  529. return (code, resp, result)
  530. # process a staff or student job
  531. # should mimic vbs script actions
  532. def process_file(contactType, uploadFile, outputFile):
  533. if verboseFlag > 0:
  534. sys.stderr.write('Reading input file\n')
  535. slen = 0
  536. try:
  537. with open(os.path.join(uploadDir, uploadFile), 'rb') as io:
  538. c = io.read(100)
  539. if c is not None:
  540. slen = len(c)
  541. except:
  542. pass
  543. strResults = ''
  544. if slen == 0:
  545. strResults = 'Input file has no data.\n'
  546. else:
  547. try:
  548. code, resp, result = upload_file(uploadFile, contactType, True)
  549. if code == 200:
  550. strResults = 'Completed without errors'
  551. else:
  552. strResults = 'Status returned was %d' % code
  553. if resp:
  554. strResults += ('.\nResponse message:\n%s\n' % resp)
  555. else:
  556. strResults += ', with no response message.\n'
  557. except pycurl.error as e:
  558. strResults = 'Post failed with error: %s\n' % e
  559. if verboseFlag > 0:
  560. sys.stderr.write(strResults)
  561. with open(os.path.join(uploadDir, outputFile), 'w') as io:
  562. io.write(strResults)
  563. if verboseFlag > 0:
  564. sys.stderr.write('Job complete\n')
  565. # Begin main script
  566. def main():
  567. if allPreRegs:
  568. # convert pre-reg students
  569. create_csv_file('preregs-1718.txt', 'preregs-1718.csv', studentNoRefreshHeaders, writestudentrow, None)
  570. # process_file('Other', 'preregs-1718.csv', 'preregs-1718_output.txt')
  571. elif allGraduates:
  572. # Convert graduating students
  573. create_csv_file('graduated-2017.txt', 'graduated-2017.csv', studentNoRefreshHeaders, writestudentrow, 'Graduated 2017')
  574. # process_file('Other', 'graduated-2017.csv', 'graduated-2017_output.txt')
  575. else:
  576. # convert powerschool autosend files to BBC csv format
  577. create_csv_file('ps-staff.txt', 'staff.csv', staffHeaders, writestaffrow, None)
  578. create_csv_file('ps-students.txt', 'students.csv', studentHeaders, writestudentrow, None)
  579. # Upload converted files to BBC
  580. process_file('Staff', 'staff.csv', 'staff_output.txt')
  581. process_file('Student', 'students.csv', 'student_output.txt')
  582. if __name__ == '__main__':
  583. main()