schild.rb 16 KB

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