schild.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. require 'schild/version'
  2. require 'sequel'
  3. # Das Schild Modul, das alle Klassen für die Datenbankanbindung bereitstellt
  4. module Schild
  5. # ist die Datenbank-Verbindung. Alle Daten können über diese Konstante abgerufen werden
  6. @db = Sequel.connect("#{ENV['S_ADAPTER']}://#{ENV['S_HOST']}/#{ENV['S_DB']}?user=#{ENV['S_USER']}&password=#{ENV['S_PASSWORD']}&zeroDateTimeBehavior=convertToNull")
  7. def self.connect
  8. @db = Sequel.connect("#{ENV['S_ADAPTER']}://#{ENV['S_HOST']}/#{ENV['S_DB']}?user=#{ENV['S_USER']}&password=#{ENV['S_PASSWORD']}&zeroDateTimeBehavior=convertToNull")
  9. end
  10. def self.db
  11. @db
  12. end
  13. # Stellt die Schüler-Tabelle samt Assoziationen bereit.
  14. class Schueler < Sequel::Model(:schueler)
  15. many_to_one :fachklasse, :class => :Fachklasse, :key => :Fachklasse_ID
  16. one_to_many :abschnitte, :class => :Abschnitt
  17. one_to_one :bk_abschluss, :class => :BKAbschluss
  18. one_to_many :bk_abschluss_leistungen, :class => :BKAbschlussFaecher
  19. one_to_one :abi_abschluss, :class => :AbiAbschluss
  20. one_to_many :abi_abschluss_leistungen, :class => :AbiAbschlussFaecher
  21. one_to_one :fhr_abschluss, :class => :FHRAbschluss
  22. one_to_many :fhr_abschluss_leistungen, :class => :FHRAbschlussFaecher
  23. one_to_many :vermerke, :class => :Vermerke
  24. one_to_one :schuelerfoto, :class => :Schuelerfotos
  25. one_to_many :sprachenfolge, :class => :Sprachenfolge
  26. end
  27. # Dient als Assoziation für Schüler und deren Klassenbezeichnung etc.
  28. class Fachklasse < Sequel::Model(:eigeneschule_fachklassen)
  29. one_to_many :schueler
  30. end
  31. # Assoziation für Lehrer, hauptsächlich für Klassenlehrer
  32. class Klassenlehrer < Sequel::Model(:k_lehrer)
  33. one_to_one :abschnitt, :primary_key=>:Kuerzel, :key=>:KlassenLehrer
  34. end
  35. # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt.
  36. class Abschnitt < Sequel::Model(:schuelerlernabschnittsdaten)
  37. many_to_one :schueler, :class => :Schueler, :key => :Schueler_ID
  38. one_to_many :noten, :class => :Noten
  39. many_to_one :klassenlehrer, :class => :Klassenlehrer, :primary_key=>:Kuerzel, :key=>:KlassenLehrer
  40. end
  41. # Assoziation für Noten
  42. class Noten < Sequel::Model(:schuelerleistungsdaten)
  43. many_to_one :abschnitt, :class => :Abschnitt, :key => :Abschnitt_ID
  44. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  45. end
  46. # Assoziation für Fächer
  47. class Faecher < Sequel::Model(:eigeneschule_faecher)
  48. #siehe abi_...
  49. one_to_one :noten
  50. one_to_many :abi_abschluss_leistungen
  51. one_to_one :sprachenfolge, :class => :Sprachenfolge, :key => :Fach_ID
  52. one_to_many :gliederungen, :class => :Fach_Gliederung, :key => :Fach_ID
  53. end
  54. # Assoziation für BK-Abschluss des Schülers
  55. class BKAbschluss < Sequel::Model(:schuelerbkabschluss)
  56. one_to_one :schueler
  57. end
  58. # Assoziation für die Prüfungsfächer des Schülers
  59. class BKAbschlussFaecher < Sequel::Model(:schuelerbkfaecher)
  60. many_to_one :schueler
  61. end
  62. # Assoziation für Abi-Abschluss des Schülers
  63. class AbiAbschluss < Sequel::Model(:schuelerabitur)
  64. one_to_one :schueler
  65. end
  66. # Assoziation für die Abifächer des Schülers
  67. class AbiAbschlussFaecher < Sequel::Model(:schuelerabifaecher)
  68. many_to_one :schueler
  69. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  70. end
  71. # Assoziation für FHR-Abschluss des Schülers
  72. class FHRAbschluss < Sequel::Model(:schuelerfhr)
  73. one_to_one :schueler
  74. end
  75. # Assoziation für die FHR-fächer des Schülers
  76. class FHRAbschlussFaecher < Sequel::Model(:schuelerfhrfaecher)
  77. many_to_one :schueler
  78. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  79. end
  80. # Assoziation für die bisher erreichten Sprachniveaus
  81. class Sprachenfolge < Sequel::Model(:schuelersprachenfolge)
  82. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  83. end
  84. # Assoziation für die bisher erreichten Sprachniveaus
  85. class Fach_Gliederung < Sequel::Model(:fach_gliederungen)
  86. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  87. end
  88. # Vermerke von Schülern
  89. class Vermerke < Sequel::Model(:schuelervermerke)
  90. many_to_one :Schueler
  91. end
  92. # Schülerfotos als jpg
  93. class Schuelerfotos < Sequel::Model(:schuelerfotos)
  94. one_to_one :schueler
  95. end
  96. # Schul-Tabelle
  97. class Schule < Sequel::Model(:eigeneschule)
  98. end
  99. # Tabelle für Schild-Nutzer
  100. class Nutzer < Sequel::Model(:users)
  101. end
  102. end
  103. module SchildErweitert
  104. # erst Ruby 2.1.0 macht include zu einer public-Methode
  105. if Module.private_method_defined? :include
  106. class Module
  107. public :include
  108. end
  109. end
  110. # String und Symbol werden um snake_case ergänzt, das die Schild-Tabellen umbenennt
  111. # Legacy-Methoden aus alten Schild-Versionen wird teilweise auch unterstützt.
  112. module CoreExtensions
  113. module String
  114. def snake_case
  115. return downcase if match(/\A[A-Z]+\z/)
  116. gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
  117. gsub(/([a-z])([A-Z])/, '\1_\2').
  118. downcase
  119. end
  120. end
  121. module Symbol
  122. def snake_case
  123. to_s.snake_case
  124. end
  125. end
  126. end
  127. # Schild hat teilweise nil in DB-Feldern. SchildTypeSaver gibt entweder einen
  128. # Leer-String zurück ("") oder bei strftime das 1899 Datum zurück.
  129. module SchildTypeSaver
  130. Symbol.include SchildErweitert::CoreExtensions::Symbol
  131. String.include CoreExtensions::String
  132. # bei include wird für jede Spalte in der Schild-Tabelle eine Ersatzmethode
  133. # erstellt, die bei nil ein Null-Objekt erstellt.
  134. def self.included(klass)
  135. klass.columns.each do |column|
  136. name = column.snake_case
  137. MethodLogger::Methods.add(klass, name)
  138. # allow_nil ist als Argument optional und lässt bei +true+ alle Ergebnisse durch
  139. define_method(("_"+name.to_s).to_sym) {public_send(column)}
  140. define_method(name) do |allow_nil=false|
  141. ret = public_send(column)
  142. if allow_nil || ret
  143. ret = ret.strip if ret.class == String
  144. ret
  145. else
  146. create_null_object(klass, column)
  147. end
  148. end
  149. end
  150. end
  151. def create_null_object(klass, column)
  152. k = Schild.db.schema_type_class(klass.db_schema[column][:type])
  153. if k.class == Array
  154. # Sequel stellt :datetime als [Time, DateTime] dar, deswegen die Abfrage nach Array
  155. # Schild verwendet Time Objekte, wir machen das auch
  156. Time.new(1899)
  157. elsif k == Integer
  158. 0
  159. elsif k == Float
  160. 0.0
  161. else
  162. # alle anderen types werden als Klasse zurückgegeben
  163. k.new
  164. end
  165. end
  166. end
  167. # Halten wir Protokoll zu den erstellten Methoden
  168. # Ist brauchbar, wenn man z.B. noch extremer als der SchildTypeSaver arbeiten möchte
  169. module MethodLogger
  170. class Methods
  171. @@accessor_methods = {}
  172. def self.add(klass, meth)
  173. @@accessor_methods[klass] ||= []
  174. @@accessor_methods[klass] << meth
  175. end
  176. def self.list(klass)
  177. @@accessor_methods[klass]
  178. end
  179. end
  180. end
  181. # Mixin für Notenbezeichnungen
  182. module NotenHelfer
  183. # Noten können als Punkte abgerufen werden:
  184. # note[5] => "4-"
  185. # oder auch andersherum: note.index("4-") => 5
  186. @note = %w[6 5- 5 5+ 4- 4 4+ 3- 3 3+ 2- 2 2+ 1- 1 1+]
  187. def self.punkte_aus_note(note)
  188. return if note.nil?
  189. @note.index(note)
  190. end
  191. def self.note_aus_punkten(punkte)
  192. return unless punkte && punkte.to_i.between?(1,15) || punkte == "0"
  193. return punkte if ((punkte.to_i == 0) && (punkte.size > 1))
  194. return if (punkte.class == String) && punkte.empty?
  195. @note[punkte.to_i]
  196. end
  197. # Notenbezeichnung als String
  198. def note_s(ziffer)
  199. case ziffer
  200. when "1", "1+", "1-"
  201. "sehr gut"
  202. when "2", "2+", "2-"
  203. "gut"
  204. when "3", "3+", "3-"
  205. "befriedigend"
  206. when "4", "4+", "4-"
  207. "ausreichend"
  208. when "5", "5+", "5-"
  209. "mangelhaft"
  210. when "6"
  211. "ungenügend"
  212. when 'NB'
  213. "––––––"
  214. when "E1"
  215. "mit besonderem Erfolg teilgenommen"
  216. when "E2"
  217. "mit Erfolg teilgenommen"
  218. when 'E3'
  219. "teilgenommen"
  220. end
  221. end
  222. end
  223. # Klassen sind Konstanten. Deswegen alle auslesen, die Klassen behalten und
  224. # dynamisch neue Klassen mit gleichem Namen erstellen.
  225. # Automatisch SchildTypeSaver einbinden.
  226. #
  227. # Sollen zusätzliche Methoden eingebunden werden, muss - wie unten Schueler
  228. # und andere Klassen - die neu erstelle Klasse gepatcht werden.
  229. # Die alten Methoden bleiben erhalten, d.h. auch die TypeSaver-Methoden.
  230. Schild.constants.map {|name| Schild.const_get(name)}.select {|o| o.is_a?(Class)}.each do |klass|
  231. name = Schild.const_get(klass.to_s).name.split("::").last
  232. klass = Class.new(klass) do
  233. include SchildTypeSaver
  234. end
  235. name = const_set(name, klass)
  236. end
  237. # Stellt die Schüler-Tabelle samt Assoziationen bereit.
  238. class Schueler
  239. # gibt das z.Zt. aktuelle Halbjahr zurück.
  240. def akt_halbjahr
  241. abschnitte.last
  242. end
  243. # gibt aus +jahr+ das Halbjahr +1+ oder +2+ zurück.
  244. def halbjahr(jahr, abschnitt)
  245. abschnitte_dataset.where(:jahr => jahr, :abschnitt => abschnitt).first
  246. end
  247. # gibt +Herr+ oder +Frau+ als Anrede für Schüler zurück.
  248. def anrede
  249. self.geschlecht == 3 ? "Herr" : "Frau"
  250. end
  251. # gibt die passende Bezeichnung zurück Schüler
  252. def schueler_in
  253. self.geschlecht == 3 ? "Schüler" : "Schülerin"
  254. end
  255. # gibt die passende Bezeichnung zurück Studierende
  256. def studierende_r
  257. self.geschlecht == 3 ? "Studierender" : "Studierende"
  258. end
  259. # gibt die jeweilige Berufsbezeichnung nach Geschlecht zurück.
  260. def berufsbezeichnung_mw
  261. return "Keine Fachklasse zugeordnet" if self.fachklasse.nil?
  262. self.geschlecht == 3 ? self.fachklasse.bezeichnung : self.fachklasse.beschreibung_w
  263. end
  264. # gibt +true+ zurück, wenn Schüler volljährig.
  265. def volljaehrig?
  266. self.volljaehrig == "+"
  267. end
  268. # gibt an, ob der Schüler zu einem Zeitpunkt *datum* volljährig war.
  269. def volljaehrig_bei?(datum)
  270. return false if datum.nil? || self.Geburtsdatum.nil?
  271. geb, datum = self.Geburtsdatum.to_date, datum.to_date
  272. (datum.year - geb.year - ((datum.month > geb.month || (datum.month == geb.month && datum.day >= geb.day)) ? 0 : 1)) >= 18
  273. end
  274. # fragt ab, ob in Schild ein Foto als hinterlegt eingetragen ist.
  275. def foto_vorhanden?
  276. !!(self.schuelerfoto && self.schuelerfoto.foto)
  277. end
  278. # gibt, wenn vorhanden, ein Foto als jpg-String zurück, ansonsten nil.
  279. def foto
  280. self.schuelerfoto.foto if self.foto_vorhanden?
  281. end
  282. end
  283. # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt.
  284. class Abschnitt
  285. dataset_module do
  286. # filtert den Datensatz nach Jahr
  287. def jahr(i)
  288. where(:Jahr => i)
  289. end
  290. # filtert den Datensatz nach Halbjahr
  291. def halbjahr(i,j)
  292. jahr(i).where(:Abschnitt => j)
  293. end
  294. # filtert und gibt den Datensatz als Abschnitt des aktuellen Halbjahrs zurück
  295. def akt_halbjahr
  296. halbjahr(Time.new.year-1, 1).first
  297. end
  298. end
  299. # Hilfsmethode für die folgenden Methoden
  300. def faecher_nach_id(id)
  301. noten.select{ |n| n.fach.Fachgruppe_ID == id && n.AufZeugnis == '+' }.sort_by{ |n| n.fach.SortierungS2 }
  302. end
  303. # wählt alle berufsübergreifenden Fächer des gewählten Schülers in angegeben Halbjahr.
  304. def berufsuebergreifend
  305. faecher_nach_id 10
  306. end
  307. # wählt alle berufsbezogenen Fächer des gewählten Schülers in angegeben Halbjahr.
  308. def berufsbezogen
  309. faecher_nach_id 20
  310. end
  311. # wählt alle Fächer des Differenzierungsbreichs des gewählten Schülers in angegeben Halbjahr.
  312. def differenzierungsbereich
  313. faecher_nach_id 30
  314. end
  315. # wählt alle Fächergruppen aus.
  316. def faechergruppen
  317. [berufsuebergreifend, berufsbezogen, differenzierungsbereich]
  318. end
  319. # gibt den Namen des Klassenlehrers mit gekürztem Vornamen.
  320. def v_name_klassenlehrer
  321. return "Kein Klassenlehrer angelegt" if klassenlehrer.nil?
  322. v = klassenlehrer.vorname
  323. n = klassenlehrer.nachname
  324. "#{v[0]}. #{n}"
  325. end
  326. # gibt "Klassenlehrer" entsprechend Geschlecht zurück
  327. def klassenlehrer_in
  328. return "Kein Klassenlehrer angelegt" if klassenlehrer.nil?
  329. klassenlehrer.geschlecht == "3" ? "Klassenlehrer" : "Klassenlehrerin"
  330. end
  331. # gibt das aktuelle Schuljahr als String im Format "2014/15" zurück.
  332. def schuljahr
  333. jahr = self.jahr
  334. "#{jahr}/#{jahr-1999}"
  335. end
  336. end
  337. # Assoziation für Noten
  338. class Noten
  339. include NotenHelfer
  340. # note in String umwandeln
  341. def note
  342. note_s self.noten_krz
  343. end
  344. # Bezeichnung des Fachs
  345. def bezeichnung
  346. fach.bezeichnung
  347. end
  348. # Die Fachgruppen-ID des Fachs
  349. def fachgruppe_ID
  350. fach.fachgruppe_id
  351. end
  352. end
  353. # Assoziation für BK-Abschlussdaten
  354. class BKAbschluss
  355. # Ist der Schüler zugelassen?
  356. def zulassung?
  357. self.Zulassung == "+"
  358. end
  359. # Ist der Schüler für den Berufsabschluss zugelassen?
  360. def zulassung_ba?
  361. self.ZulassungBA == "+"
  362. end
  363. # Hat der Schüler den Berufsabschluss bestanden?
  364. def bestanden_ba?
  365. self.BestandenBA == "+"
  366. end
  367. end
  368. # Assoziation für die jeweiligen BK-Prüfungsfächer
  369. class BKAbschlussFaecher
  370. include NotenHelfer
  371. # Wurde das Fach schriftlich geprüft?
  372. def fach_schriftlich?
  373. self.FachSchriftlich == "+"
  374. end
  375. # Wurde das Fach mündlich geprüft?
  376. def fach_muendlich?
  377. self.MdlPruefung == "+"
  378. end
  379. def note(notenart=:note_abschluss_ba)
  380. note_s send(notenart)
  381. end
  382. end
  383. # Assoziation für Abi-Abschlussdaten
  384. class AbiAbschluss
  385. # Ist der Schüler zugelassen?
  386. def zulassung?
  387. self.Zugelassen == "+"
  388. end
  389. alias_method :zugelassen?, :zulassung?
  390. # Hat der Schüler die Abi-Prüfung bestanden?
  391. def bestanden_abi?
  392. self.PruefungBestanden == "+"
  393. end
  394. alias_method :pruefung_bestanden?, :bestanden_abi?
  395. def latinum?
  396. self.Latinum == "+"
  397. end
  398. def kl_latinum?
  399. self.KlLatinum == "+"
  400. end
  401. def graecum?
  402. self.Graecum == "+"
  403. end
  404. def hebraicum?
  405. self.Hebraicum == "+"
  406. end
  407. end
  408. # Assoziation für die jeweiligen Abi-Prüfungsfächer
  409. class AbiAbschlussFaecher
  410. include NotenHelfer
  411. def note(notenart)
  412. note_s send(notenart)
  413. end
  414. end
  415. # Assoziation für die jeweiligen FHR-Prüfungsfächer
  416. class FHRAbschlussFaecher
  417. include NotenHelfer
  418. def note(notenart)
  419. note_s send(notenart)
  420. end
  421. end
  422. # Schul-Tabelle mit vereinfachtem Zugriff auf Datenfelder mittel class-Methoden
  423. class Schule
  424. # gibt die Schulnummer zurück
  425. def self.schulnummer
  426. self.first.schul_nr
  427. end
  428. # gibt den Namen des Schulleiters als V. Name zurück
  429. def self.v_name_schulleiter
  430. "#{self.first.schulleiter_vorname[0]}. #{self.first.schulleiter_name}"
  431. end
  432. # gibt die männliche bzw. weibliche Form des Schulleiters zurück
  433. def self.schulleiter_in
  434. self.first.schulleiter_geschlecht == 3 ? "Schulleiter" : "Schulleiterin"
  435. end
  436. # gibt den Ort der Schule zurück
  437. def self.ort
  438. self.first.ort
  439. end
  440. end
  441. # Tabelle der Schuld-Benutzer zum Abgleichen der Daten
  442. class Nutzer
  443. alias :name :us_name
  444. alias :login :us_login_name
  445. alias :passwort :us_password
  446. alias :password :passwort
  447. # prüft, ob das angegebene Passwort mit dem gespeicherten Passwort übereinstimmt
  448. def passwort?(passwort='')
  449. crypt(passwort) == self.passwort
  450. end
  451. alias :password? :passwort?
  452. # ver- bzw. entschlüsselt einen String mit dem Schild-Passwortalgorithmus
  453. def crypt(passwort)
  454. passwort.codepoints.map{|c| ((c/16)*32+15-c).chr}.join('')
  455. end
  456. end
  457. end