schild.rb 16 KB

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