require 'schild/version' require 'sequel' ## # === Schild Basis-Modul # Das Schild Modul, das alle Klassen für die Datenbankanbindung bereitstellt. # Die zur Verfügung gestellten Klassen entsprechen dabei den Schild-Tabellen, # und den damit verbundenen Assoaziationen. # # Neben dem Zugriff aus Daten innerhalb einer Tabelle kann über die Schild-Klassen # auch auf weitere verknüpfte Tabellen zugegriffen werden. Dazu ein Beispiel: # # Schueler[0].name # => "Olsen" # # Schueler[0].abschnitte[0].jahr # => 2017 # # Im ersten Beispiel wurde auf den ersten Schüler in der Datenbank ([0]) zugegriffen, # und der `name` ausgelesen. # # Im zweiten Beispiel wird auf den gleichen Schüler zugegriffen und dessen Abschnitt-Tabelle # ebenfalls abgefragt. D.h. es wird bereits auf die Abschnitte-Tabelle zugegriffen # und automatisch nur Abschnitte zurückgegeben, die zum Schüler gehören. Aus # dieser Menge wird ebenfalls der erste Abschnitt gewählt ([0]). # # Auf diesem Weg können beliebig verschachtelte Anfragen gestartet werden. # # Es wird empfohlen statt Schild das Modul SchildErweitert einzubinden. Es # beinhaltet neben den Assoaziationen auch weitere Methoden. # Weitere Erläuterungen dazu weiter im Modul. module Schild Sequel::Model.plugin :tactical_eager_loading # @db ist die Datenbank-Verbindung. Alle Daten können über diese Variable abgerufen werden @db = Sequel.connect("#{ENV['S_ADAPTER']}://#{ENV['S_HOST']}/#{ENV['S_DB']}?user=#{ENV['S_USER']}&password=#{ENV['S_PASSWORD']}&zeroDateTimeBehavior=convertToNull") begin retries ||= 0 @db.test_connection rescue puts "Verbindung zum Server konnte nicht hergestellt werden" puts "#{retries += 1}. Verbindungsversuch in 5s (max 50 Versuche)" puts "Sie können mit Strg-c abbrechen." sleep 5 retry if retries < 50 end @db.extension(:freeze_datasets) @db.extension(:connection_validator) @db.extension(:null_dataset) # gibt die Datenbank zurück def self.db @db end # Stellt die Schüler-Tabelle mit Assoziationen bereit. # Die aufgeführten Instanzmethoden sind ausschließlich Assoaziationen. # # Tabellenname: schueler class Schueler < Sequel::Model(:schueler) # @!method fachklasse # @return [Fachklasse] many_to_one :fachklasse, :class => :Fachklasse # @!method abschnitte # @return [Array] one_to_many :abschnitte, :class => :Abschnitt # @!method bk_abschluss # @return [BKAbschluss] one_to_one :bk_abschluss, :class => :BKAbschluss # @!method bk_abschluss_leistungen # @return [Array] one_to_many :bk_abschluss_leistungen, :class => :BKAbschlussFach # @!method abi_abschluss # @return [AbiAbschluss] one_to_one :abi_abschluss, :class => :AbiAbschluss # @!method abi_abschluss_leistungen # @return [Array] one_to_many :abi_abschluss_leistungen, :class => :AbiAbschlussFach # @!method fhr_abschluss # @return [FHRAbschluss] one_to_one :fhr_abschluss, :class => :FHRAbschluss # @!method fhr_abschluss_leistungen # @return [Array] one_to_many :fhr_abschluss_leistungen, :class => :FHRAbschlussFach # @!method vermerke # @return [Array] one_to_many :vermerke, :class => :Vermerk # @!method schuelerfoto # @return [Schuelerfoto] one_to_one :schuelerfoto, :class => :Schuelerfoto # @!method sprachenfolgen # @return [Array] one_to_many :sprachenfolgen, :class => :Sprachenfolge end # Informationen zu Klassenbezeichnung und weiteren Daten über die jeweiligen # Bildungsgänge. Verfügbar als Assoziationen für Schüler. # # Tabellenname: eigeneschule_fachklassen class Fachklasse < Sequel::Model(:eigeneschule_fachklassen) end # Versetzungstabelle für Fachklassen. D.h. Klassenbezeichnungen für vorhergehende # und nachfolgende Klassen. # # Tabellenname: versetzung class Versetzung < Sequel::Model(:versetzung) # @!method fachklasse # @return [Fachklasse] many_to_one :fachklasse, :class => :Fachklasse end # Assoziation für Lehrer, hauptsächlich für Klassenlehrer # # Tabellenname: k_lehrer class Klassenlehrer < Sequel::Model(:k_lehrer) end # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt. # # Tabellenname: schuelerlernabschnittsdaten class Abschnitt < Sequel::Model(:schuelerlernabschnittsdaten) # @!method noten # @return [Array] one_to_many :noten, :class => :Note # @!method fachklasse # @return [Fachklasse] many_to_one :fachklasse, :class => :Fachklasse # @!method klassenlehrer # @return [Klassenlehrer] many_to_one :klassenlehrer, :primary_key=>:Kuerzel, :key=>:KlassenLehrer, :class=>:Klassenlehrer end # Assoziation für Noten # # Tabellenname: schuelerleistungsdaten class Note < Sequel::Model(:schuelerleistungsdaten) # @!method fach # @return [Fach] many_to_one :fach, :class => :Fach end # Assoziation für Fächer # # Tabellenname: eigeneschule_faecher class Fach < Sequel::Model(:eigeneschule_faecher) # @!method sprachenfolge # @return [Sprachenfolge] one_to_one :sprachenfolge, :class => :Sprachenfolge, :key => :Fach_ID # @!method gliederungen # @return [Array] one_to_many :gliederungen, :class => :Fach_Gliederung, :key => :Fach_ID end # Assoziation für BK-Abschluss des Schülers # # Tabellenname: schuelerbkabschluss class BKAbschluss < Sequel::Model(:schuelerbkabschluss) end # Assoziation für die Prüfungsfächer des Schülers # # Tabellenname: schuelerbkfaecher class BKAbschlussFach < Sequel::Model(:schuelerbkfaecher) # @!method fach # @return [Fach] many_to_one :fach, :class => :Fach end # Assoziation für Abi-Abschluss des Schülers # # Tabellenname: schuelerabitur class AbiAbschluss < Sequel::Model(:schuelerabitur) end # Assoziation für die Abifächer des Schülers # # Tabellenname: schuelerabifaecher class AbiAbschlussFach < Sequel::Model(:schuelerabifaecher) # @!method fach # @return [Fach] many_to_one :fach, :class => :Fach end # Assoziation für FHR-Abschluss des Schülers # # Tabellenname: schuelerfhr class FHRAbschluss < Sequel::Model(:schuelerfhr) end # Assoziation für die FHR-fächer des Schülers # # Tabellenname: schuelerfhrfaecher class FHRAbschlussFach < Sequel::Model(:schuelerfhrfaecher) # @!method fach # @return [Fach] many_to_one :fach, :class => :Fach end # Assoziation für die bisher erreichten Sprachniveaus # # Tabellenname: schuelersprachenfolge class Sprachenfolge < Sequel::Model(:schuelersprachenfolge) end # Besondere Facheinstellungen nach Fachklasse. Betrifft v.a. Sortierung, # Festlegungen über Prüfungsfächer etc. # # Tabellenname: fach_gliederungen class Fach_Gliederung < Sequel::Model(:fach_gliederungen) # @!method fachklasse # @return [Fachklasse] many_to_one :fachklasse, :class => :Fachklasse end # Vermerke von Schülern # # Tabellenname: schuelervermerke class Vermerk < Sequel::Model(:schuelervermerke) end # Schülerfotos als jpg # # Tabellenname: schuelerfotos class Schuelerfoto < Sequel::Model(:schuelerfotos) end # Schul-Tabelle # # Tabellenname: eigeneschule class Schule < Sequel::Model(:eigeneschule) end # Tabelle für Schild-Nutzer # # Tabellenname: users class Nutzer < Sequel::Model(:users) end end # ==SchildErweitert # Dieses Modul bindet das Schild-Modul ein und erweitert es mit zusätzlichen Methoden # zum einfachen Zugriff auf Schild-Methoden, die teilweise verschachtelt sind. # Ebenso bietet SchildErweitert alle Methoden mit Null-Objekten, d.h. es # gibt kein +nil+, nur Standardwerte. module SchildErweitert if Module.private_method_defined? :include # erst Ruby 2.1.0 macht include zu einer public-Methode class Module public :include end end # String und Symbol werden um snake_case ergänzt, das die Schild-Tabellen umbenennt # Legacy-Methoden aus alten Schild-Versionen wird teilweise auch unterstützt. module CoreExtensions # Patch für String module String # wandelt Strings in +Snake Case+ um. # Beispiel: # CamelCase.snake_case # => camel_case def snake_case return downcase if match(/\A[A-Z]+\z/) gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). gsub(/([a-z])([A-Z])/, '\1_\2'). downcase end end # Patch für Symbol module Symbol # snake_case für Symbol, verwandel Symbol in String def snake_case to_s.snake_case end end end # Schild hat teilweise nil in DB-Feldern. SchildTypeSaver gibt entweder einen # Leer-String zurück ("") oder bei strftime das 1899 Datum zurück. module SchildTypeSaver Symbol.include SchildErweitert::CoreExtensions::Symbol String.include CoreExtensions::String # es wird für jede Spalte in der Schild-Tabelle eine Ersatzmethode # erstellt, die bei nil ein Null-Objekt erstellt. # Dazu wird die neu angelegte Methode per MethodLogger gesichert. def self.included(klass) klass.columns.each do |column| name = column.snake_case MethodLogger::Methods.add(klass, name) # allow_nil ist als Argument optional und lässt bei +true+ alle Ergebnisse durch define_method(("_"+name.to_s).to_sym) {public_send(column)} define_method(name) do |allow_nil=false| ret = public_send(column) if allow_nil || ret ret = ret.strip if ret.class == String ret else create_null_object(klass, column) end end end end # Es wird ein Null-Objekt erstellt, das einen Standardwert zurückgibt. def create_null_object(klass, column) k = Sequel::Database::SCHEMA_TYPE_CLASSES[klass.db_schema[column][:type]] case # Sequel stellt :datetime als [Time, DateTime] dar, deswegen die Abfrage nach Array # Schild verwendet Time Objekte, wir machen das auch when k.class == Array then Time.new(1899) when k == Integer then 0 when k == Float then 0.0 # alle anderen types werden als Klasse zurückgegeben else k.new end end end # Halten wir Protokoll zu den erstellten Methoden # Ist brauchbar, wenn man z.B. noch extremer als der SchildTypeSaver arbeiten möchte module MethodLogger # Methoden-Klasse die alles Daten sammelt class Methods @@accessor_methods = {} # Eine Methode dem MethodLogger hinzufügen # @param [Class, method] def self.add(klass, meth) @@accessor_methods[klass] ||= [] @@accessor_methods[klass] << meth end # Liste von Methoden für eine Klasse auslesen def self.list(klass=nil) klass ? @@accessor_methods[klass] : @@accessor_methods end end end # Mixin für Notenbezeichnungen module NotenHelfer # Noten können als Punkte abgerufen werden: # note[5] => "4-" # oder auch andersherum: note.index("4-") => 5 @note = %w[6 5- 5 5+ 4- 4 4+ 3- 3 3+ 2- 2 2+ 1- 1 1+] # @param note [Integer] def self.punkte_aus_note(note) return if note.nil? @note.index(note) end # @param punkte [Integer] def self.note_aus_punkten(punkte) return unless punkte && punkte.to_i.between?(1,15) || punkte == "0" return punkte if ((punkte.to_i == 0) && (punkte.size > 1)) return if (punkte.class == String) && punkte.empty? @note[punkte.to_i] end # Notenbezeichnung als String # @param ziffer [String] def note_s(ziffer) case ziffer when "1", "1+", "1-" then "sehr gut" when "2", "2+", "2-" then "gut" when "3", "3+", "3-" then "befriedigend" when "4", "4+", "4-" then "ausreichend" when "5", "5+", "5-" then "mangelhaft" when "6" then "ungenügend" when 'NB' then "––––––" when "E1" then "mit besonderem Erfolg teilgenommen" when "E2" then "mit Erfolg teilgenommen" when 'E3' then "teilgenommen" end end end # Klassen sind Konstanten. Deswegen alle auslesen, die Klassen behalten und # dynamisch neue Klassen mit gleichem Namen erstellen. # Automatisch SchildTypeSaver einbinden. # # Sollen zusätzliche Methoden eingebunden werden, muss - wie unten Schueler # und andere Klassen - die neu erstelle Klasse gepatcht werden. # Die alten Methoden bleiben erhalten, d.h. auch die TypeSaver-Methoden. Schild.constants.map {|name| Schild.const_get(name)}.select {|o| o.is_a?(Class)}.each do |klass| name = Schild.const_get(klass.to_s).name.split("::").last klass = Class.new(klass){include SchildTypeSaver} name = const_set(name, klass) end # Stellt die Schüler-Tabelle samt Assoziationen bereit. class Schueler # gibt das z.Zt. aktuelle Halbjahr zurück. def akt_halbjahr abschnitte.last end # gibt aus +jahr+ das Halbjahr +1+ oder +2+ zurück. def halbjahr(jahr, abschnitt) abschnitte_dataset.where(:jahr => jahr, :abschnitt => abschnitt).first end # gibt +Herr+ oder +Frau+ als Anrede für Schüler zurück. def anrede self.geschlecht == 3 ? "Herr" : "Frau" end # gibt die passende Bezeichnung zurück Schüler def schueler_in self.geschlecht == 3 ? "Schüler" : "Schülerin" end # gibt die passende Bezeichnung zurück Studierende def studierende_r self.geschlecht == 3 ? "Studierender" : "Studierende" end # gibt die jeweilige Berufsbezeichnung nach Geschlecht zurück. def berufsbezeichnung_mw return "Keine Fachklasse zugeordnet" if self.fachklasse.nil? self.geschlecht == 3 ? self.fachklasse.bezeichnung : self.fachklasse.beschreibung_w end # gibt +true+ zurück, wenn Schüler volljährig. def volljaehrig? self.volljaehrig == "+" end # gibt an, ob der Schüler zu einem Zeitpunkt *datum* volljährig war. def volljaehrig_bei?(datum) return false if datum.nil? || self.Geburtsdatum.nil? geb, datum = self.Geburtsdatum.to_date, datum.to_date (datum.year - geb.year - ((datum.month > geb.month || (datum.month == geb.month && datum.day >= geb.day)) ? 0 : 1)) >= 18 end # fragt ab, ob in Schild ein Foto als hinterlegt eingetragen ist. def foto_vorhanden? !!(self.schuelerfoto && self.schuelerfoto.foto) end # gibt, wenn vorhanden, ein Foto als jpg-String zurück, ansonsten nil. def foto self.schuelerfoto.foto if self.foto_vorhanden? end end # Ist die Assoziation, die Halbjahre, sog. Abschnitte zurückgibt. class Abschnitt dataset_module do # filtert den Datensatz nach Jahr def jahr(i) where(:Jahr => i) end # filtert den Datensatz nach Halbjahr def halbjahr(i,j) jahr(i).where(:Abschnitt => j) end # filtert und gibt den Datensatz als Abschnitt des aktuellen Halbjahrs zurück def akt_halbjahr halbjahr(Time.new.year-1, 1).first end end # Hilfsmethode für die folgenden Methoden def faecher_nach_id(id) noten.select{ |n| n.fach.Fachgruppe_ID == id && n.AufZeugnis == '+' }.sort_by{ |n| n.fach.SortierungS2 } end # wählt alle berufsübergreifenden Fächer des gewählten Schülers in angegeben Halbjahr. def berufsuebergreifend faecher_nach_id 10 end # wählt alle berufsbezogenen Fächer des gewählten Schülers in angegeben Halbjahr. def berufsbezogen faecher_nach_id 20 end # wählt alle Fächer des Differenzierungsbreichs des gewählten Schülers in angegeben Halbjahr. def differenzierungsbereich faecher_nach_id 30 end # wählt alle Fächergruppen aus. def faechergruppen [berufsuebergreifend, berufsbezogen, differenzierungsbereich] end # gibt den Namen des Klassenlehrers mit gekürztem Vornamen. def v_name_klassenlehrer return "Kein Klassenlehrer angelegt" if klassenlehrer.nil? v = klassenlehrer.vorname n = klassenlehrer.nachname "#{v[0]}. #{n}" end # gibt "Klassenlehrer" entsprechend Geschlecht zurück def klassenlehrer_in return "Kein Klassenlehrer angelegt" if klassenlehrer.nil? klassenlehrer.geschlecht == "3" ? "Klassenlehrer" : "Klassenlehrerin" end # gibt das aktuelle Schuljahr als String im Format "2014/15" zurück. def schuljahr jahr = self.jahr "#{jahr}/#{jahr-1999}" end end # Assoziation für Noten class Note include NotenHelfer # note in String umwandeln def note note_s self.noten_krz end # Bezeichnung des Fachs def bezeichnung fach.bezeichnung end # Die Fachgruppen-ID des Fachs def fachgruppe_ID fach.fachgruppe_id end end # Assoziation für BK-Abschlussdaten class BKAbschluss # Ist der Schüler zugelassen? def zulassung? self.Zulassung == "+" end # Ist der Schüler für den Berufsabschluss zugelassen? def zulassung_ba? self.ZulassungBA == "+" end # Hat der Schüler den Berufsabschluss bestanden? def bestanden_ba? self.BestandenBA == "+" end end # Assoziation für die jeweiligen BK-Prüfungsfächer class BKAbschlussFach include NotenHelfer # Wurde das Fach schriftlich geprüft? def fach_schriftlich? self.FachSchriftlich == "+" end # Wurde das Fach mündlich geprüft? def fach_muendlich? self.MdlPruefung == "+" end # holt die jeweilige Note aus der Tabelle. # Standard ist note_abschluss_ba, als Argument kann auch eine andere # verwendet werden (siehe Tabelle) def note(notenart=:note_abschluss_ba) note_s send(notenart) end end # Assoziation für Abi-Abschlussdaten class AbiAbschluss # Ist der Schüler zugelassen? def zulassung? self.Zugelassen == "+" end alias_method :zugelassen?, :zulassung? # Hat der Schüler die Abi-Prüfung bestanden? def bestanden_abi? self.PruefungBestanden == "+" end alias_method :pruefung_bestanden?, :bestanden_abi? # Latinum gemacht? def latinum? self.Latinum == "+" end # Kleines Latinum erreicht? def kl_latinum? self.KlLatinum == "+" end # Gräcum erreicht? def graecum? self.Graecum == "+" end # Hebraicum erreicht? def hebraicum? self.Hebraicum == "+" end end # Assoziation für die jeweiligen Abi-Prüfungsfächer class AbiAbschlussFach include NotenHelfer # Note aus Tebelle abfragen, Notenart angeben (siehe Tabelle) def note(notenart) note_s send(notenart) end end # Assoziation für die jeweiligen FHR-Prüfungsfächer class FHRAbschlussFach include NotenHelfer # Note aus Tebelle abfragen, Notenart angeben (siehe Tabelle) def note(notenart) note_s send(notenart) end end # Schul-Tabelle mit vereinfachtem Zugriff auf Datenfelder mittel class-Methoden class Schule # gibt die Schulnummer zurück def self.schulnummer self.first.schul_nr end # gibt den Namen des Schulleiters als V. Name zurück def self.v_name_schulleiter "#{self.first.schulleiter_vorname[0]}. #{self.first.schulleiter_name}" end # gibt die männliche bzw. weibliche Form des Schulleiters zurück def self.schulleiter_in self.first.schulleiter_geschlecht == 3 ? "Schulleiter" : "Schulleiterin" end # gibt den Ort der Schule zurück def self.ort self.first.ort end end # Tabelle der Schuld-Benutzer zum Abgleichen der Daten class Nutzer alias :name :us_name alias :login :us_login_name alias :passwort :us_password alias :password :passwort # prüft, ob das angegebene Passwort mit dem gespeicherten Passwort übereinstimmt def passwort?(passwort='') crypt(passwort) == self.passwort end alias :password? :passwort? # ver- bzw. entschlüsselt einen String mit dem Schild-Passwortalgorithmus def crypt(passwort) passwort.codepoints.map{|c| ((c/16)*32+15-c).chr}.join('') end end end