schild.rb 16 KB

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