schild.rb 16 KB

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