schild.rb 16 KB

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