schild.rb 16 KB

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