PageRenderTime 23ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/bbe.lua

http://github.com/ksdtech/bbc
Lua | 461 lines | 378 code | 40 blank | 43 comment | 52 complexity | c1682d3728caa8183b11ae52bf05b4a1 MD5 | raw file
  1. -- bbe.lua
  2. -- generate 4 csv files nightly and for upload to Edline (Blackboard Engage)
  3. -- uses Lua-cURL module
  4. -- see https://github.com/Lua-cURL/Lua-cURLv3
  5. -- install with luarocks install Lua-cURL
  6. require 'table_print'
  7. require 'app_config'
  8. debugFlag = 1
  9. verboseFlag = 1
  10. -- bbc-students AutoSend fields
  11. -- tab field delimiter, lf line delimiter, no headers
  12. autosend_student_fields = [[
  13. Student_Number
  14. First_Name
  15. Last_Name
  16. Grade_Level
  17. Gender
  18. HomeRoom_Teacher
  19. SchoolID
  20. Home_Phone
  21. Mother_Work_Phone
  22. Mother_Cell
  23. Father_Work_Phone
  24. Father_Cell
  25. Mother_Email
  26. Father_Email
  27. Home2_Phone
  28. Mother2_Work_Phone
  29. Mother2_Cell
  30. Father2_Work_Phone
  31. Father2_Cell
  32. Mother2_Email
  33. Father2_Email
  34. Enroll_Status
  35. Network_Id
  36. Network_Password
  37. Web_Id
  38. Web_Password
  39. Family_Ident
  40. Mother_Staff_Id
  41. Mother_First
  42. Mother
  43. Father_Staff_Id
  44. Father_First
  45. Father
  46. Student_Web_Id
  47. Mother2_Staff_Id
  48. Mother2_First
  49. Mother2_Last
  50. Father2_Staff_Id
  51. Father2_First
  52. Father2_Last
  53. EntryCode
  54. Lang_Adults_Primary
  55. CA_ELAStatus
  56. Reg_Will_Attend
  57. Reg_Grade_Level
  58. ExitCode
  59. Form3_Updated_At
  60. Form4_Updated_At
  61. Form6_Updated_At
  62. Form9_Updated_At
  63. Form10_Updated_At
  64. Form15_Updated_At
  65. Form1_Updated_At
  66. Form16_Updated_At
  67. ]]
  68. -- bbc-staff AutoSend fields
  69. -- tab field delimiter, lf line delimiter, no headers
  70. autosend_staff_fields = [[
  71. Status
  72. Staffstatus
  73. Title
  74. Teachernumber
  75. First_Name
  76. Last_Name
  77. Gender
  78. Schoolid
  79. Home_Phone
  80. Cell
  81. Email_Addr
  82. Email_Personal
  83. Network_Id
  84. Network_Password
  85. Group_Membership
  86. ]]
  87. -- course fields
  88. autosend_section_fields = [[
  89. id
  90. schoolid
  91. course_number
  92. [02]course_name
  93. [02]code
  94. section_number
  95. [13]abbreviation
  96. expression
  97. [05]teachernumber
  98. [05]last_name
  99. ]]
  100. -- cc fields
  101. autosend_roster_fields = [[
  102. sectionid
  103. schoolid
  104. course_number
  105. [02]course_name
  106. [02]code
  107. section_number
  108. [13]abbreviation
  109. expression
  110. [05]teachernumber
  111. [05]last_name
  112. [01]student_number
  113. ]]
  114. -- number of elements in table (table.getn is for "lists" only)
  115. function tlength(tbl)
  116. local count = 0
  117. for _ in pairs(tbl) do count = count + 1 end
  118. return count
  119. end
  120. -- strip whitespace from beginning and end of string
  121. function string:trim6()
  122. return self:match'^()%s*$' and '' or self:match'^%s*(.*%S)'
  123. end
  124. -- headers for staff file
  125. function string:is_empty()
  126. return not string.find(self, "%S")
  127. end
  128. function string:split(delimiter)
  129. local t = { }
  130. local from = 1
  131. local delim_from, delim_to = string.find(self, delimiter, from)
  132. while delim_from do
  133. table.insert(t, string.sub(self, from, delim_from - 1))
  134. from = delim_to + 1
  135. delim_from, delim_to = string.find(self, delimiter, from)
  136. end
  137. table.insert(t, string.sub(self, from))
  138. return t
  139. end
  140. function wordsplit(s)
  141. local t = { }
  142. for word in s:gmatch('%w+') do
  143. table.insert(t, word)
  144. end
  145. return t
  146. end
  147. function string:splitcsv()
  148. s = self .. ',' -- ending comma
  149. local t = {} -- table to collect fields
  150. local fieldstart = 1
  151. repeat
  152. -- next field is quoted? (start with `"'?)
  153. if string.find(s, '^"', fieldstart) then
  154. local a, c
  155. local i = fieldstart
  156. repeat
  157. -- find closing quote
  158. a, i, c = string.find(s, '"("?)', i + 1)
  159. until c ~= '"' -- quote not followed by quote?
  160. if not i then error('unmatched "') end
  161. local f = string.sub(s, fieldstart + 1, i - 1)
  162. table.insert(t, (string.gsub(f, '""', '"')))
  163. fieldstart = string.find(s, ',', i) + 1
  164. else -- unquoted; find next comma
  165. local nexti = string.find(s, ',', fieldstart)
  166. table.insert(t, string.sub(s, fieldstart, nexti - 1))
  167. fieldstart = nexti + 1
  168. end
  169. until fieldstart > string.len(s)
  170. return t
  171. end
  172. function course_abbr(name)
  173. local words = wordsplit(name)
  174. local abbr = string.sub(words[1], 1, 4)
  175. local suffix = ''
  176. local nwords = table.getn(words)
  177. if nwords > 1 then
  178. local lastw = words[nwords]
  179. if lastw == 'K' or tonumber(lastw) ~= nil and tonumber(lastw) > 0 then
  180. suffix = lastw
  181. end
  182. end
  183. return abbr .. suffix
  184. end
  185. function readcsv(fname, headers, rowfn)
  186. io.input(fname)
  187. local lno = headers and 0 or 1
  188. local columns = { }
  189. while true do
  190. local line = io.read()
  191. if line == nil then break end
  192. line = string.gsub(line, "\r$", "") -- handle CRLF
  193. if lno == 0 then
  194. columns = string.splitcsv(line)
  195. else
  196. local row = { }
  197. if headers then
  198. local values = string.splitcsv(line)
  199. for k, v in pairs(columns) do
  200. row[v] = values[k]
  201. end
  202. else
  203. row = string.splitcsv(line)
  204. end
  205. for k, v in pairs(row) do
  206. if string.is_empty(v) then row[k] = nil end
  207. end
  208. rowfn(row)
  209. end
  210. lno = lno + 1
  211. end
  212. end
  213. function readtab(fname, headers, rowfn, sumfn)
  214. io.input(sourceDir..fname)
  215. local lno = headers and 0 or 1
  216. local columns = { }
  217. while true do
  218. local line = io.read()
  219. if verboseFlag > 2 then io.stderr:write("reading " .. fname .. " line " .. lno .. "\n") end
  220. if line == nil then break end
  221. line = string.gsub(line, "\r$", "") -- handle CRLF
  222. if lno == 0 then
  223. columns = string.split(line, "\t")
  224. else
  225. local row = { }
  226. if headers then
  227. local values = string.split(line, "\t")
  228. for k, v in pairs(columns) do
  229. row[v] = values[k]
  230. end
  231. else
  232. row = string.split(line, "\t")
  233. end
  234. for k, v in pairs(row) do
  235. if string.is_empty(v) then row[k] = nil end
  236. end
  237. local status, err = pcall(rowfn, row, fname, lno)
  238. if verboseFlag > 0 and not status then
  239. io.stderr:write("row invalid: " .. err .. "\n")
  240. end
  241. end
  242. lno = lno + 1
  243. end
  244. if sumfn then
  245. local status, err = pcall(sumfn, fname, lno)
  246. end
  247. end
  248. -- edline specific stuff
  249. included_students = nil
  250. if debugFlag then
  251. included_students = { ['111984']=true, ['111985']=true }
  252. end
  253. excluded_courses = {
  254. ['AAAA']=true, -- attendance
  255. ['9966']=true, -- study period
  256. ['9977']=true, -- student aide
  257. ['9991']=true,
  258. ['9993']=true,
  259. ['9996']=true,
  260. ['9997']=true, -- grade level grouping
  261. }
  262. class_table = { }
  263. course_terms = { }
  264. assigned_teachers = { }
  265. enrolled_students = { }
  266. enrolled_classes = { }
  267. -- if we are limiting students for testing purposes
  268. function check_student(sn)
  269. return sn~= "" and (not included_students or included_students[sn])
  270. end
  271. function check_teacher(tn)
  272. return tn~= ""
  273. end
  274. function check_course(cn)
  275. return cn ~= "" and not excluded_courses[cn]
  276. end
  277. function check_school(sid)
  278. return sid == "103" or sid == "104"
  279. end
  280. function check_class(cid)
  281. return cid and enrolled_classes[cid]
  282. end
  283. function full_year(term)
  284. return not term:match('^[STQH][1-6]$')
  285. end
  286. function make_class_id(class_abbr, teacher_last, term, course_name, course_number)
  287. if not class_abbr then
  288. io.stderr:write("no abbreviation for course " .. course_name .. " (" .. course_number .. ")\n")
  289. return nil
  290. end
  291. local class_id = class_abbr .. "-" .. string.upper(string.sub(teacher_last, 1, 3))
  292. if not full_year(term) then
  293. class_id = class_id .. "-" .. term
  294. end
  295. return class_id
  296. end
  297. -- convert row. if autosend fields are changed, you must change the logic
  298. -- in this function
  299. function writestudentrow(row, fname, lno)
  300. -- student fields
  301. local student_number = row[1] or ""
  302. local first_name = row[2]
  303. local last_name = row[3]
  304. local grade_level = 0 + row[4]
  305. local schoolid = row[7]
  306. local enroll_status = tonumber(row[22] or 0)
  307. if enroll_status == 0 and check_student(student_number) and check_school(schoolid) then
  308. enrolled_students[student_number] = true
  309. local s_grade_level = string.format("%02d", grade_level);
  310. -- output a row
  311. -- "ID","LastName","FirstName","GradeLevel","SchoolID"
  312. io.write(string.format("%q,%q,%q,%q,%q\r\n", "S" .. student_number, last_name, first_name, s_grade_level, schoolid))
  313. if verboseFlag > 2 then io.stderr:write("row written\n") end
  314. end
  315. end
  316. -- convert row. if autosend fields are changed, you must change the logic
  317. -- in this function
  318. function writeschedulerow(row, fname, lno)
  319. -- cc fields
  320. local schoolid = row[2]
  321. local course_number = row[3]
  322. local student_number = row[11] or ""
  323. if enrolled_students[student_number] and check_course(course_number) and check_school(schoolid) then
  324. local course_name = (row[4] or ""):trim6()
  325. local class_abbr = row[5]
  326. local section_number = row[6]
  327. local term = row[7]
  328. local teachernumber = row[9]
  329. local teacher_last = row[10]
  330. local class_id = make_class_id(class_abbr, teacher_last, term, course_name, course_number)
  331. if class_id then
  332. enrolled_classes[class_id] = true
  333. -- output a row
  334. -- "ClassID","StudentID","SchoolID"
  335. io.write(string.format("%q,%q,%q\r\n", class_id, "S" .. student_number, schoolid))
  336. if verboseFlag > 2 then io.stderr:write("row written\n") end
  337. end
  338. end
  339. end
  340. -- convert row. if autosend fields are changed, you must change the logic
  341. -- in this function
  342. function writeclassrow(row, fname, lno)
  343. -- section fields
  344. local schoolid = row[2]
  345. local course_number = row[3]
  346. local course_name = (row[4] or ""):trim6()
  347. local teachernumber = row[9] or ""
  348. local teacher_last = row[10]
  349. if check_teacher(teachernumber) and check_course(course_number) and check_school(schoolid) then
  350. local class_abbr = row[5]
  351. local section_number = row[6]
  352. local term = row[7]
  353. local class_id = make_class_id(class_abbr, teacher_last, term, course_name, course_number)
  354. if check_class(class_id) then
  355. local course_term = class_abbr
  356. local class_teacher_name = course_name .. "-" .. teacher_last
  357. local class_bare_name = course_name
  358. if not full_year(term) then
  359. course_term = course_term .. "-" .. term
  360. class_teacher_name = class_teacher_name .. "-" .. term
  361. class_bare_name = class_bare_name .. "-" .. term
  362. end
  363. if not class_table[class_id] then
  364. assigned_teachers[teachernumber] = true
  365. class_table[class_id] = { class_bare_name, class_teacher_name, "T" .. teachernumber, schoolid }
  366. end
  367. if not course_terms[course_term] then
  368. course_terms[course_term] = { }
  369. end
  370. course_terms[course_term][class_id] = true
  371. end
  372. else
  373. if verboseFlag > 0 then io.stderr:write("rejecting class " .. course_name .. " (" .. course_number .. ") " .. teacher_last .. "\n") end
  374. end
  375. end
  376. function writeclasses(fname, lno)
  377. for course_term, class_id_table in pairs(course_terms) do
  378. local nclass_ids = tlength(class_id_table)
  379. for class_id, _ in pairs(class_id_table) do
  380. local data = class_table[class_id]
  381. local class_name = nclass_ids > 1 and data[2] or data[1]
  382. -- output a row
  383. -- ClassID","Class Name","TeacherID","SchoolID"
  384. io.write(string.format("%q,%q,%q,%q\r\n", class_id, class_name, data[3], data[4]))
  385. end
  386. end
  387. end
  388. -- convert row. if autosend fields are changed, you must change the logic
  389. -- in this function
  390. function writestaffrow(row, fname, lno)
  391. -- staff fields
  392. local status = 0 + (row[1] or 0)
  393. local staffstatus = 0 + (row[2] or 0)
  394. local teachernumber = row[4] or ""
  395. local first_name = row[5] or ""
  396. local last_name = row[6] or ""
  397. local schoolid = row[8] or ""
  398. if assigned_teachers[teachernumber] then
  399. -- output a row
  400. -- "ID","LastName","FirstName","GradeLevel","SchoolID”
  401. io.write(string.format("%q,%q,%q,%q,%q\r\n", "T" .. teachernumber, last_name, first_name, "", schoolid))
  402. if verboseFlag > 2 then io.stderr:write("row written\n") end
  403. end
  404. end
  405. -- convert powerschool autosend files to csv format required by Edline
  406. function create_csv_file(psFile, csvFile, mode, rowfn, sumfn)
  407. local o = assert(io.open(edlineDir..csvFile, mode))
  408. io.output(o)
  409. readtab(psFile, false, rowfn, sumfn)
  410. o:close()
  411. end
  412. -- begin main script
  413. -- must run students first (to get enroll_status)!
  414. create_csv_file("ps-students.txt", "student.csv", 'wb', writestudentrow, nil)
  415. -- must run rosters before sections!
  416. create_csv_file("ps-rosters-bacich.txt", "schedule.csv", 'wb', writeschedulerow, nil)
  417. create_csv_file("ps-rosters-kent.txt", "schedule.csv", 'ab', writeschedulerow, nil)
  418. -- must run sections before staff!
  419. create_csv_file("ps-sections-bacich.txt", "class.csv", 'wb', writeclassrow, nil)
  420. create_csv_file("ps-sections-kent.txt", "class.csv", 'ab', writeclassrow, writeclasses)
  421. create_csv_file("ps-staff.txt", "teacher.csv", 'wb', writestaffrow, nil)