schild.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  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. unless @db
  7. @db = Sequel.connect("#{ENV['S_ADAPTER']}://#{ENV['S_HOST']}/#{ENV['S_DB']}?user=#{ENV['S_USER']}&password=#{ENV['S_PASSWORD']}&zeroDateTimeBehavior=convertToNull")
  8. end
  9. def self.connect(adapter, host, db, user, password)
  10. @db = Sequel.connect("#{adapter}://#{host}/#{db}?user=#{user}&password=#{password}&zeroDateTimeBehavior=convertToNull")
  11. end
  12. def self.db
  13. @db
  14. end
  15. # Stellt die Schüler-Tabelle samt Assoziationen bereit.
  16. class Schueler < Sequel::Model(:schueler)
  17. many_to_one :fachklasse, :class => :Fachklasse, :key => :Fachklasse_ID
  18. one_to_many :abschnitte, :class => :Abschnitt
  19. one_to_one :bk_abschluss, :class => :BKAbschluss
  20. one_to_many :bk_abschluss_leistungen, :class => :BKAbschlussFaecher
  21. one_to_one :abi_abschluss, :class => :AbiAbschluss
  22. one_to_many :abi_abschluss_leistungen, :class => :AbiAbschlussFaecher
  23. one_to_one :fhr_abschluss, :class => :FHRAbschluss
  24. one_to_many :fhr_abschluss_leistungen, :class => :FHRAbschlussFaecher
  25. one_to_many :vermerke, :class => :Vermerke
  26. one_to_one :schuelerfoto, :class => :Schuelerfotos
  27. one_to_many :sprachenfolge, :class => :Sprachenfolge
  28. end
  29. # Dient als Assoziation für Schüler und deren Klassenbezeichnung etc.
  30. class Fachklasse < Sequel::Model(:eigeneschule_fachklassen)
  31. one_to_many :schueler
  32. end
  33. # Assoziation für Lehrer, hauptsächlich für Klassenlehrer
  34. class Klassenlehrer < Sequel::Model(:k_lehrer)
  35. one_to_one :abschnitt, :primary_key=>:Kuerzel, :key=>:KlassenLehrer
  36. end
  37. # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt.
  38. class Abschnitt < Sequel::Model(:schuelerlernabschnittsdaten)
  39. many_to_one :schueler, :class => :Schueler, :key => :Schueler_ID
  40. one_to_many :noten, :class => :Noten
  41. many_to_one :klassenlehrer, :class => :Klassenlehrer, :primary_key=>:Kuerzel, :key=>:KlassenLehrer
  42. end
  43. # Assoziation für Noten
  44. class Noten < Sequel::Model(:schuelerleistungsdaten)
  45. many_to_one :abschnitt, :class => :Abschnitt, :key => :Abschnitt_ID
  46. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  47. end
  48. # Assoziation für Fächer
  49. class Faecher < Sequel::Model(:eigeneschule_faecher)
  50. #siehe abi_...
  51. one_to_one :noten
  52. one_to_many :abi_abschluss_leistungen
  53. one_to_one :sprachenfolge, :class => :Sprachenfolge, :key => :Fach_ID
  54. end
  55. # Assoziation für BK-Abschluss des Schülers
  56. class BKAbschluss < Sequel::Model(:schuelerbkabschluss)
  57. one_to_one :schueler
  58. end
  59. # Assoziation für die Prüfungsfächer des Schülers
  60. class BKAbschlussFaecher < Sequel::Model(:schuelerbkfaecher)
  61. many_to_one :schueler
  62. end
  63. # Assoziation für Abi-Abschluss des Schülers
  64. class AbiAbschluss < Sequel::Model(:schuelerabitur)
  65. one_to_one :schueler
  66. end
  67. # Assoziation für die Abifächer des Schülers
  68. class AbiAbschlussFaecher < Sequel::Model(:schuelerabifaecher)
  69. many_to_one :schueler
  70. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  71. end
  72. # Assoziation für FHR-Abschluss des Schülers
  73. class FHRAbschluss < Sequel::Model(:schuelerfhr)
  74. one_to_one :schueler
  75. end
  76. # Assoziation für die FHR-fächer des Schülers
  77. class FHRAbschlussFaecher < Sequel::Model(:schuelerfhrfaecher)
  78. many_to_one :schueler
  79. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  80. end
  81. # Assoziation für die bisher erreichten Sprachniveaus
  82. class Sprachenfolge < Sequel::Model(:schuelersprachenfolge)
  83. many_to_one :fach, :class => :Faecher, :key => :Fach_ID
  84. end
  85. # Vermerke von Schülern
  86. class Vermerke < Sequel::Model(:schuelervermerke)
  87. many_to_one :Schueler
  88. end
  89. # Schülerfotos als jpg
  90. class Schuelerfotos < Sequel::Model(:schuelerfotos)
  91. one_to_one :schueler
  92. end
  93. # Schul-Tabelle
  94. class Schule < Sequel::Model(:eigeneschule)
  95. end
  96. # Tabelle für Schild-Nutzer
  97. class Nutzer < Sequel::Model(:users)
  98. end
  99. end
  100. module SchildErweitert
  101. # erst Ruby 2.1.0 macht include zu einer public-Methode
  102. if Module.private_method_defined? :include
  103. class Module
  104. public :include
  105. end
  106. end
  107. # String und Symbol werden um snake_case ergänzt, das die Schild-Tabellen umbenennt
  108. # Legacy-Methoden aus alten Schild-Versionen wird teilweise auch unterstützt.
  109. module CoreExtensions
  110. module String
  111. def snake_case
  112. return downcase if match(/\A[A-Z]+\z/)
  113. gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
  114. gsub(/([a-z])([A-Z])/, '\1_\2').
  115. downcase
  116. end
  117. end
  118. module Symbol
  119. def snake_case
  120. to_s.snake_case
  121. end
  122. end
  123. module Schueler
  124. def entlassart
  125. return self.entlass_art if self.respond_to?(:entlass_art)
  126. end
  127. end
  128. module Fachklasse
  129. def dqr_niveau
  130. return self.DQR_Niveau if self.respond_to?(:DQR_Niveau)
  131. "Alte Schild-Version ohne DQR-Niveau"
  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 punkte if ((punkte.to_i == 0) && (punkte.size > 1))
  201. return unless punkte && punkte.to_i.between?(1,15) || punkte == "0"
  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. Fachklasse.include CoreExtensions::Fachklasse
  246. Schueler.include CoreExtensions::Schueler
  247. # Stellt die Schüler-Tabelle samt Assoziationen bereit.
  248. class Schueler
  249. # gibt das z.Zt. aktuelle Halbjahr zurück.
  250. def akt_halbjahr
  251. abschnitte.last
  252. end
  253. # gibt aus +jahr+ das Halbjahr +1+ oder +2+ zurück.
  254. def halbjahr(jahr, abschnitt)
  255. abschnitte_dataset.where(:jahr => jahr, :abschnitt => abschnitt).first
  256. end
  257. # gibt +Herr+ oder +Frau+ als Anrede für Schüler zurück.
  258. def anrede
  259. self.geschlecht == 3 ? "Herr" : "Frau"
  260. end
  261. # gibt die passende Bezeichnung zurück Schüler
  262. def schueler_in
  263. self.geschlecht == 3 ? "Schüler" : "Schülerin"
  264. end
  265. # gibt die passende Bezeichnung zurück Studierende
  266. def studierende_r
  267. self.geschlecht == 3 ? "Studierender" : "Studierende"
  268. end
  269. # gibt die jeweilige Berufsbezeichnung nach Geschlecht zurück.
  270. def berufsbezeichnung_mw
  271. return "Keine Fachklasse zugeordnet" if self.fachklasse.nil?
  272. self.geschlecht == 3 ? self.fachklasse.bezeichnung : self.fachklasse.beschreibung_w
  273. end
  274. # gibt +true+ zurück, wenn Schüler volljährig.
  275. def volljaehrig?
  276. self.volljaehrig == "+"
  277. end
  278. # gibt an, ob der Schüler zu einem Zeitpunkt *datum* volljährig war.
  279. def volljaehrig_bei?(datum)
  280. return false if datum.nil? || self.Geburtsdatum.nil?
  281. geb, datum = self.Geburtsdatum.to_date, datum.to_date
  282. (datum.year - geb.year - ((datum.month > geb.month || (datum.month == geb.month && datum.day >= geb.day)) ? 0 : 1)) >= 18
  283. end
  284. # fragt ab, ob in Schild ein Foto als hinterlegt eingetragen ist.
  285. def foto_vorhanden?
  286. self.foto_vorhanden == "+"
  287. end
  288. # gibt, wenn vorhanden, ein Foto als jpg-String zurück, ansonsten nil.
  289. def foto
  290. self.foto_vorhanden? ? self.schuelerfoto.foto : nil
  291. end
  292. end
  293. # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt.
  294. class Abschnitt
  295. dataset_module do
  296. # filtert den Datensatz nach Jahr
  297. def jahr(i)
  298. where(:Jahr => i)
  299. end
  300. # filtert den Datensatz nach Halbjahr
  301. def halbjahr(i,j)
  302. jahr(i).where(:Abschnitt => j)
  303. end
  304. # filtert und gibt den Datensatz als Abschnitt des aktuellen Halbjahrs zurück
  305. def akt_halbjahr
  306. halbjahr(Time.new.year-1, 1).first
  307. end
  308. end
  309. # Hilfsmethode für die folgenden Methoden
  310. def faecher_nach_id(id)
  311. noten.select{ |n| n.fach.Fachgruppe_ID == id && n.AufZeugnis == '+' }.sort_by{ |n| n.fach.SortierungS2 }
  312. end
  313. # wählt alle berufsübergreifenden Fächer des gewählten Schülers in angegeben Halbjahr.
  314. def berufsuebergreifend
  315. faecher_nach_id 10
  316. end
  317. # wählt alle berufsbezogenen Fächer des gewählten Schülers in angegeben Halbjahr.
  318. def berufsbezogen
  319. faecher_nach_id 20
  320. end
  321. # wählt alle Fächer des Differenzierungsbreichs des gewählten Schülers in angegeben Halbjahr.
  322. def differenzierungsbereich
  323. faecher_nach_id 30
  324. end
  325. # wählt alle Fächergruppen aus.
  326. def faechergruppen
  327. [berufsuebergreifend, berufsbezogen, differenzierungsbereich]
  328. end
  329. # gibt den Namen des Klassenlehrers mit gekürztem Vornamen.
  330. def v_name_klassenlehrer
  331. return "Kein Klassenlehrer angelegt" if klassenlehrer.nil?
  332. v = klassenlehrer.vorname
  333. n = klassenlehrer.nachname
  334. "#{v[0]}. #{n}"
  335. end
  336. # gibt "Klassenlehrer" entsprechend Geschlecht zurück
  337. def klassenlehrer_in
  338. return "Kein Klassenlehrer angelegt" if klassenlehrer.nil?
  339. klassenlehrer.geschlecht == "3" ? "Klassenlehrer" : "Klassenlehrerin"
  340. end
  341. # gibt das aktuelle Schuljahr als String im Format "2014/15" zurück.
  342. def schuljahr
  343. jahr = self.jahr
  344. "#{jahr}/#{jahr-1999}"
  345. end
  346. end
  347. # Assoziation für Noten
  348. class Noten
  349. include NotenHelfer
  350. # note in String umwandeln
  351. def note
  352. note_s self.noten_krz
  353. end
  354. # Bezeichnung des Fachs
  355. def bezeichnung
  356. fach.bezeichnung
  357. end
  358. # Die Fachgruppen ID des Fachs
  359. def fachgruppe_ID
  360. fach.fachgruppe_id
  361. end
  362. end
  363. # Assoziation für BK-Abschlussdaten
  364. class BKAbschluss
  365. # Ist der Schüler zugelassen?
  366. def zulassung?
  367. self.Zulassung == "+"
  368. end
  369. # Ist der Schüler für den Berufsabschluss zugelassen?
  370. def zulassung_ba?
  371. self.ZulassungBA == "+"
  372. end
  373. # Hat der Schüler den Berufsabschluss bestanden?
  374. def bestanden_ba?
  375. self.BestandenBA == "+"
  376. end
  377. end
  378. # Assoziation für die jeweiligen BK-Prüfungsfächer
  379. class BKAbschlussFaecher
  380. include NotenHelfer
  381. # Wurde das Fach schriftlich geprüft?
  382. def fach_schriftlich?
  383. self.FachSchriftlich == "+"
  384. end
  385. # Wurde das Fach mündlich geprüft?
  386. def fach_muendlich?
  387. self.MdlPruefung == "+"
  388. end
  389. def note(notenart=:note_abschluss_ba)
  390. note_s send(notenart)
  391. end
  392. end
  393. # Assoziation für Abi-Abschlussdaten
  394. class AbiAbschluss
  395. # Ist der Schüler zugelassen?
  396. def zulassung?
  397. self.Zugelassen == "+"
  398. end
  399. alias_method :zugelassen?, :zulassung?
  400. # Hat der Schüler die Abi-Prüfung bestanden?
  401. def bestanden_abi?
  402. self.PruefungBestanden == "+"
  403. end
  404. alias_method :pruefung_bestanden?, :bestanden_abi?
  405. def latinum?
  406. self.Latinum == "+"
  407. end
  408. def kl_latinum?
  409. self.KlLatinum == "+"
  410. end
  411. def graecum?
  412. self.Graecum == "+"
  413. end
  414. def hebraicum?
  415. self.Hebraicum == "+"
  416. end
  417. end
  418. # Assoziation für die jeweiligen Abi-Prüfungsfächer
  419. class AbiAbschlussFaecher
  420. include NotenHelfer
  421. def note(notenart)
  422. note_s send(notenart)
  423. end
  424. end
  425. # Assoziation für die jeweiligen FHR-Prüfungsfächer
  426. class FHRAbschlussFaecher
  427. include NotenHelfer
  428. def note(notenart)
  429. note_s send(notenart)
  430. end
  431. end
  432. # Schul-Tabelle mit vereinfachtem Zugriff auf Datenfelder mittel class-Methoden
  433. class Schule
  434. # gibt die Schulnummer zurück
  435. def self.schulnummer
  436. self.first.schul_nr
  437. end
  438. # gibt den Namen des Schulleiters als V. Name zurück
  439. def self.v_name_schulleiter
  440. "#{self.first.schulleiter_vorname[0]}. #{self.first.schulleiter_name}"
  441. end
  442. # gibt die männliche bzw. weibliche Form des Schulleiters zurück
  443. def self.schulleiter_in
  444. self.first.schulleiter_geschlecht == 3 ? "Schulleiter" : "Schulleiterin"
  445. end
  446. # gibt den Ort der Schule zurück
  447. def self.ort
  448. self.first.ort
  449. end
  450. end
  451. # Tabelle der Schuld-Benutzer zum Abgleichen der Daten
  452. class Nutzer
  453. # der Nutzername
  454. def name
  455. self.us_name
  456. end
  457. # der Login-Name des Nutzers
  458. def login
  459. self.us_login_name
  460. end
  461. # das Passwort des Nutzers
  462. def passwort
  463. self.us_password
  464. end
  465. alias :password :passwort
  466. # prüft, ob das angegebene Passwort mit dem gespeicherten Passwort übereinstimmt
  467. def passwort?(passwort='')
  468. crypt(passwort) == self.passwort
  469. end
  470. alias :password? :passwort?
  471. # ver- bzw. entschlüsselt einen String mit dem Schild-Passwortalgorithmus
  472. def crypt(passwort)
  473. passwort.codepoints.map{|c| ((c/16)*32+15-c).chr}.join('')
  474. end
  475. end
  476. end