schild.rb 16 KB

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