schild.rb 15 KB

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