Class: Main

Inherits:
Sinatra::Base
  • Object
show all
Includes:
Neo4jBolt
Defined in:
src/ruby/pry.rb,
src/ruby/main.rb,
src/ruby/credentials.rb,
src/ruby/include/gev.rb,
src/ruby/include/otp.rb,
src/ruby/include/sms.rb,
src/ruby/include/aula.rb,
src/ruby/include/file.rb,
src/ruby/include/hack.rb,
src/ruby/include/ical.rb,
src/ruby/include/poll.rb,
src/ruby/include/user.rb,
src/ruby/include/vote.rb,
src/ruby/include/admin.rb,
src/ruby/include/color.rb,
src/ruby/include/event.rb,
src/ruby/include/image.rb,
src/ruby/include/jitsi.rb,
src/ruby/include/login.rb,
src/ruby/include/salzh.rb,
src/ruby/include/stats.rb,
src/ruby/include/tests.rb,
src/ruby/include/theme.rb,
src/ruby/include/cypher.rb,
src/ruby/include/groups.rb,
src/ruby/include/lesson.rb,
src/ruby/include/matrix.rb,
src/ruby/include/tresor.rb,
src/ruby/include/comment.rb,
src/ruby/include/message.rb,
src/ruby/include/monitor.rb,
src/ruby/include/website.rb,
src/ruby/include/ext_user.rb,
src/ruby/include/homework.rb,
src/ruby/include/techpost.rb,
src/ruby/include/bib_login.rb,
src/ruby/include/directory.rb,
src/ruby/include/zeugnisse.rb,
src/ruby/include/tablet_set.rb,
src/ruby/include/test_events.rb,
src/ruby/include/public_event.rb,
src/ruby/include/lehrbuchverein.rb

Constant Summary collapse

MAX_HACK_LEVEL =
10
NAMES =
%w(babbage boole catmull cerf chomsky codd dijkstra
engelbart feinler hamilton hamming hejlsberg hopper kay knuth lamport
lovelace minsky ritchie stroustrup thompson torvalds turing wirth zuse)
FRUIT =
%w(apfel birne tomate kiwi melone ananas kartoffel zitrone orange salat)
SPACE_EVENTS =
{
    '1957-10-04' => ['4. Oktober 1957', 'Am __DATE__ startete <b>Sputnik 1</b>, der erste künstliche Satellit, in die Umlaufbahn der Erde.'],
    '1957-11-03' => ['3. November 1957', 'Am __DATE__ startete <b>Sputnik 2</b> in die Umlaufbahn der Erde. An Bord befand sich <b>Laika</b>, das erste Tier im Weltraum.'],
    '1959-10-07' => ['7. Oktober 1959', 'Am __DATE__ sendete <b>Luna 3</b> die ersten Bilder von der Rückseite des Mondes.'],
    '1961-04-12' => ['12. April 1961', 'Am __DATE__ flog <b>Juri Gagarin</b> als erster Mensch in den Weltraum.'],
    '1962-12-14' => ['14. Dezember 1962', 'Am __DATE__ sendete <b>Mariner 2</b> erstmals Daten von der Venus zur Erde.'],
    '1963-06-16' => ['16. Juni 1963', 'Am __DATE__ flog <b>Walentina Tereschkowa</b> als erste Frau ins Weltall.'],
    '1965-03-18' => ['18. März 1965', 'Am __DATE__ unternahm <b>Alexei Leonow</b> den ersten Weltraumspaziergang.'],
}
PRIMES =
%w(2	3	5	7	11	13	17	19	23	29
31	37	41	43	47	53	59	61	67	71
73	79	83	89	97	101	103	107	109	113
127	131	137	139	149	151	157	163	167	173
179	181	191	193	197	199	211	223	227	229
233	239	241	251	257	263	269	271	277	281
283	293	307	311	313	317	331	337	347	349
353	359	367	373	379	383	389	397	401	409
419	421	431	433	439	443	449	457	461	463
467	479	487	491	499	503	509	521	523	541
547	557	563	569	571	577	587	593	599	601
607	613	617	619	631	641	643	647	653	659
661	673	677	683	691	701	709	719	727	733
739	743	751	757	761	769	773	787	797	809
811	821	823	827	829	839	853	857	859	863
877	881	883	887	907	911	919	929	937	941
947	953	967	971	977	983	991	997).map { |x| x.to_i }
MAX_CYPHER_LEVEL =
10
CYPHER_LANGUAGES =
%w(Ada Algol awk Bash Basic Cobol dBase Delphi Erlang Fortran
Go Haskell Java Lisp Logo Lua MASM Modula Oberon Pascal Perl PHP
Prolog Ruby Rust Scala Scumm Squeak Swift TeX ZPL)
MONITOR_MESSAGE_PATH =
'/vplan/message.json'
@@GEN_IMAGE_WIDTHS =
[2048, 1200, 1024, 768, 512, 384, 256].sort
@@BOOTSTRAP_BREAKPOINTS =
{
    :lg => 1200,
    :md => 992,
    :sm => 768,
    :xs => 480
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.check_zeugnisformular(key) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'src/ruby/include/zeugnisse.rb', line 229

def self.check_zeugnisformular(key)
    unless @@zeugnisse[:formulare][key]
        return ['kein Formular vorhanden!']
    end
    required_tags = []
    wf_count = 0
    FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR][key].each do |tag|
        if tag[0] == '$'
            wf_count += 1
            required_tags << "#WF#{wf_count}"
            required_tags << "#WF#{wf_count}_Name"
            # required_tags << "##{tag[1, tag.size - 1]}"
        else
            required_tags << "##{tag}"
        end
        if DETAIL_NOTEN[key].include?(tag)
            required_tags << "##{tag}_AT"
            required_tags << "##{tag}_SL"
        end
    end
    required_tags << '#Zeugnisdatum'
    required_tags << '#Name'
    required_tags << '#Geburtsdatum'
    required_tags << '#Klasse'
    required_tags << '#VT'
    required_tags << '#VT_UE'
    required_tags << '#VS'
    required_tags << '#VS_UE'
    required_tags << '#VSP'
    required_tags << '#Angebote'
    required_tags << '#Bemerkungen'
    required_tags << '#WeitereBemerkungen' if key.include?('sesb')
    optional_tags = []

    optional_tags << '#Vorname'

    missing_tags = Set.new(required_tags) - Set.new(@@zeugnisse[:formulare][key][:tags])
    superfluous_tags = Set.new(@@zeugnisse[:formulare][key][:tags]) - Set.new(required_tags)
    superfluous_tags -= Set.new(optional_tags)
    errors = []
    unless missing_tags.empty?
        errors << "fehlende Markierungen: #{missing_tags.join(', ')}"
    end
    unless superfluous_tags.empty?
        errors << "unbekannte Markierungen: #{superfluous_tags.join(', ')}"
    end
    return nil if errors.empty?
    return errors
end

.collect_dataObject



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
# File 'src/ruby/main.rb', line 477

def self.collect_data
    @@user_info = {}
    @@login_shortcuts = {}
    @@email_for_matrix_login = {}
    @@shorthands = {}
    @@shorthand_order = []
    @@schueler_for_klasse = {}
    @@faecher = {}
    @@ferien_feiertage = []
    @@tablets = {}
    @@tablet_sets = {}
    @@lehrer_order = []
    @@klassen_order = []
    @@current_email_addresses = []
    @@antikenfahrt_recipients = {}
    @@antikenfahrt_mailing_lists = {}
    @@birthday_entries = {}
    @@server_etag = RandomTag.generate(24)

    @@index_for_klasse = {}
    @@predefined_external_users = {}
    @@bib_summoned_books = {}
    @@bib_unconfirmed_books = {}
    @@bib_summoned_books_last_ts = 0

    if File.exist?('/data/login-shortcuts.txt')
        File.open('/data/login-shortcuts.txt') do |f|
            f.each_line do |line|
                line.strip!
                next if line.empty? || line[0] == '#'
                parts = line.split(' ')
                next if parts.size != 2
                @@login_shortcuts[parts[0]] = parts[1]
            end
        end
    end

    parser = Parser.new()
    parser.parse_faecher do |fach, bezeichnung|
        @@faecher[fach] = bezeichnung
    end
    @@off_days = Set.new()
    parser.parse_ferien_feiertage do |t0, t1, title|
        @@ferien_feiertage << {:from => t0, :to => t1, :title => title}
        day = Date.parse(t0)
        last = Date.parse(t1)
        while day <= last
            @@off_days << day.to_s
            day += 1
        end
    end
    @@tage_infos = []
    parser.parse_tage_infos do |t0, t1, title|
        @@tage_infos << {:from => t0, :to => t1, :title => title}
    end
    begin
        @@config = YAML::load_file('/data/config.yaml')
    rescue
        @@config = {
            :first_day => '2020-06-25',
            :first_school_day => '2020-08-10',
            :last_day => '2021-08-06'
        }
        debug "Can't read /data/config.yaml, using a few default values:"
        debug @@config.to_yaml
    end
    parser.parse_lehrer do |record|
        next unless record[:can_log_in]
        @@user_info[record[:email]] = {
            :teacher => true,
            :shorthand => record[:shorthand],
            :first_name => record[:first_name],
            :last_name => record[:last_name],
            :titel => record[:titel],
            :display_name => record[:display_name],
            :display_name_official => record[:display_name_official],
            :display_last_name => record[:display_last_name],
            :display_last_name_dativ => record[:display_last_name_dativ],
            :email => record[:email],
            :can_log_in => record[:can_log_in],
            :nc_login => record[:nc_login],
            :matrix_login => record[:matrix_login],
            :initial_nc_password => record[:initial_nc_password]
        }
         = record[:matrix_login]
        raise "oops: duplicate matrix / nc login: #{}" if @@email_for_matrix_login.include?()
        @@email_for_matrix_login[] = record[:email]
        @@shorthands[record[:shorthand]] = record[:email]
        @@lehrer_order << record[:email]
    end
    @@klassenleiter = {}
    parser.parse_klassenleiter do |record|
        @@klassenleiter[record[:klasse]] = record[:klassenleiter]
        record[:klassenleiter].each do |shorthand|
            if @@shorthands[shorthand]
                email = @@shorthands[shorthand]
                @@user_info[email][:klassenleitung] ||= []
                @@user_info[email][:klassenleitung] << record[:klasse]
            end
        end
    end
    @@shorthand_order = @@shorthands.keys.sort do |a, b|
        a.downcase <=> b.downcase
    end

    @@lehrer_order.sort!() do |a, b|
        la = @@user_info[a][:shorthand].downcase
        lb = @@user_info[b][:shorthand].downcase
        la = 'zzz' + la if la[0] == '_'
        lb = 'zzz' + lb if lb[0] == '_'
        la <=> lb
    end
    @@klassen_order = KLASSEN_ORDER
    @@klassen_index = {}
    KLASSEN_ORDER.each_with_index do |klasse, i|
        @@klassen_index[klasse] = i
    end
    @@klassen_order.each.with_index { |k, i| @@index_for_klasse[k] = i }
    @@klassen_id = {}
    @@klassen_order.each do |klasse|
        @@klassen_id[klasse] = Digest::SHA2.hexdigest(KLASSEN_ID_SALT + klasse).to_i(16).to_s(36)[0, 16]
    end

    self.fix_stundenzeiten()

    disable_jitsi_for_email = Set.new()
    if File.exist?('/data/schueler/disable-jitsi.txt')
        File.open('/data/schueler/disable-jitsi.txt') do |f|
            f.each_line do |line|
                line.strip!
                next if line.empty?
                disable_jitsi_for_email << line
            end
        end
    end

    parser.parse_schueler do |record|
         = "@#{record[:email].split('@').first.sub(/\.\d+$/, '')}:#{MATRIX_DOMAIN_SHORT}"
        unless KLASSEN_ORDER.include?(record[:klasse])
            raise "Klasse #{record[:klasse]} is included in KLASSEN_ORDER"
        end
        @@user_info[record[:email]] = {
            :teacher => false,
            :first_name => record[:first_name],
            :official_first_name => record[:official_first_name],
            :display_first_name => record[:display_first_name],
            :display_last_name => record[:display_last_name],
            :display_name_official => record[:display_name_official],
            :last_name => record[:last_name],
            :display_name => record[:display_name],
            :email => record[:email],
            :id => record[:id],
            :klasse => record[:klasse],
            :klassenstufe => record[:klasse].to_i,
            :geschlecht => record[:geschlecht],
            :nc_login => record[:email].split('@').first.sub(/\.\d+$/, ''),
            :matrix_login => ,
            :initial_nc_password => record[:initial_nc_password],
            :biber_password => Main.gen_password_for_email(record[:email] + 'biber')[0, 4].downcase,
            :jitsi_disabled => disable_jitsi_for_email.include?(record[:email]),
            :geburtstag => record[:geburtstag]
        }
        raise "oops: duplicate matrix / nc login: #{}" if @@email_for_matrix_login.include?()
        @@email_for_matrix_login[] = record[:email]
        @@schueler_for_klasse[record[:klasse]] ||= []
        @@schueler_for_klasse[record[:klasse]] << record[:email]
        birthday = record[:geburtstag]
        if birthday
            birthday_md = birthday[5, 5]
            @@birthday_entries[birthday_md] ||= []
            @@birthday_entries[birthday_md] << record[:email]
        end
    end
    all_prefixes = {}
    @@user_info.keys.each do |email|
        @@user_info[email][:id] = Digest::SHA2.hexdigest(USER_ID_SALT + email).to_i(16).to_s(36)[0, 16]
        (1..email.size).each do |length|
            prefix = email[0, length]
            all_prefixes[prefix] ||= Set.new()
            all_prefixes[prefix] << email
        end
    end
    @@login_shortcuts.keys.each do |email|
        (1..email.size).each do |length|
            prefix = email[0, length]
            all_prefixes[prefix] ||= Set.new()
            all_prefixes[prefix] << email
        end
    end
    @@user_info.keys.each do |email|
        length = 1
        while all_prefixes[email[0, length]].size > 1
            length += 1
        end
        @@user_info[email][:shortest_prefix] = email[0, length]
    end

    @@tablets_for_school_streaming = Set.new()
    @@tablets_which_are_lehrer_tablets = Set.new()
    parser.parse_tablets do |record|
        if @@tablets.include?(record[:id])
            raise "Ooops: already got this tablet called #{record[:id]}"
        end
        @@tablets[record[:id]] = record
        bg_color = TABLET_COLORS[record[:color]] || TABLET_DEFAULT_COLOR
        rgb = @@renderer.hex_to_rgb(bg_color).map { |x| x / 255.0 }
        gray = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114
        @@tablets[record[:id]][:bg_color] = bg_color
        @@tablets[record[:id]][:fg_color] = gray < 0.5 ? '#ffffff' : '#000000'
        if record[:status].index('Klassenstreaming') == 0
            @@tablets[record[:id]][:klassen_stream] = record[:status].sub('Klassenstreaming', '').strip
        end
        if record[:school_streaming]
            @@tablets_for_school_streaming << record[:id]
        end
        if record[:lehrer_modus]
            @@tablets_which_are_lehrer_tablets << record[:id]
        end
    end

    @@tablet_sets = parser.parse_tablet_sets || {}

    ADMIN_USERS.each do |email|
        next unless @@user_info[email]
        @@user_info[email][:admin] = true
    end
    ZEUGNIS_ADMIN_USERS.each do |email|
        next unless @@user_info[email]
        @@user_info[email][:zeugnis_admin] = true
    end
    (CAN_SEE_ALL_TIMETABLES_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_see_all_timetables] = true
    end
    (CAN_MANAGE_SALZH_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_salzh] = true
    end
    (CAN_UPLOAD_VPLAN_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_upload_vplan] = true
    end
    (CAN_UPLOAD_FILES_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_upload_files] = true
    end
    (CAN_MANAGE_NEWS_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_news] = true
    end
    (CAN_MANAGE_AGS_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_ags] = true
    end
    (CAN_MANAGE_MONITORS_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_monitors] = true
    end
    (CAN_MANAGE_TABLETS_USERS + TECHNIKTEAM + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_tablets] = true
    end
    (CAN_REPORT_TECH_PROBLEMS_USERS + TECHNIKTEAM + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_report_tech_problems] = true
    end
    (TECHNIKTEAM + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:technikteam] = true
    end
    (CAN_MANAGE_ANTIKENFAHRT_USERS + ADMIN_USERS).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:can_manage_antikenfahrt] = true
    end
    (SV_USERS + TECHNIKTEAM).each do |email|
        next unless @@user_info[email]
        @@user_info[email][:sv] = true
    end

    # add Eltern
    @@predefined_external_users = {:groups => [], :recipients => {}}
    @@klassen_order.each do |klasse|
        next unless @@schueler_for_klasse.include?(klasse)
        @@predefined_external_users[:groups] << "/eltern/#{klasse}"
        @@predefined_external_users[:recipients]["/eltern/#{klasse}"] = {
            :label => "Eltern der Klasse #{self.tr_klasse(klasse)}",
            :entries => @@schueler_for_klasse[klasse].map { |x| 'eltern.' + @@user_info[x][:email] }
        }
        @@schueler_for_klasse[klasse].each do |x|
            eltern_email = 'eltern.' + @@user_info[x][:email]
            @@predefined_external_users[:recipients][eltern_email] = {
                :label => "Eltern von #{@@user_info[x][:display_name]}"
            }
        end
    end

    lesson_key_tr = {}
    lesson_key_tr = self.fix_lesson_key_tr(lesson_key_tr)
    # if DASHBOARD_SERVICE == 'ruby'
    #     debug lesson_key_tr.to_yaml
    # end

    @@lessons, @@vertretungen, @@vplan_timestamp, @@day_messages, @@lesson_key_back_tr, @@original_lesson_key_for_lesson_key = parser.parse_timetable(@@config, lesson_key_tr)
    @@original_fach_for_lesson_key = {}
    @@original_lesson_key_for_lesson_key.each_pair do |k, vl|
        vl.each do |v|
            @@original_fach_for_lesson_key[v] = k
        end
    end
    @@current_lesson_key_order = []
    @@current_lesson_key_info = {}
    if DASHBOARD_SERVICE == 'ruby'
        @@lessons[:lesson_keys].keys.sort do |a, b|
            afach = (a.split('_').first || '').downcase
            bfach = (b.split('_').first || '').downcase
            astufe = a.split('_')[1].to_i
            bstufe = b.split('_')[1].to_i
            afach == bfach ? ((astufe == bstufe) ? (a <=> b) : (astufe <=> bstufe)) : (afach <=> bfach)
        end.each do |lesson_key|
            lesson = @@lessons[:lesson_keys][lesson_key]
            stunden = Set.new()
            ((@@lessons[:timetables][@@lessons[:timetables].keys.sort.last][lesson_key] || {})[:stunden] || {}).each_pair do |dow, h|
                h.each_pair do |stunde, info|
                    stunden << sprintf('%d/%02d', info[:tag], info[:stunde])
                end
            end
            unless stunden.empty?
                @@current_lesson_key_order << lesson_key
                @@current_lesson_key_info[lesson_key] = {}
                @@current_lesson_key_info[lesson_key][:stunden] = stunden.to_a.sort
            end
        end
    end

    # patch lesson_keys in @@lessons and @@vertretungen
    @@lessons, @@vertretungen = parser.parse_timetable(@@config, lesson_key_tr)
    # patch @@faecher
    @@lessons[:lesson_keys].each_pair do |lesson_key, info|
        # STDERR.puts "[#{lesson_key}]"
        unless @@faecher[lesson_key]
            x = lesson_key.split('_').first.split('-').first
            @@faecher[info[:fach]] = @@faecher[x] if @@faecher[x]
        end
    end
    today = Time.now.strftime('%Y-%m-%d')
    today = @@config[:first_school_day] if today < @@config[:first_school_day]
    today = @@config[:last_day] if today > @@config[:last_day]
    today = @@lessons[:start_date_for_date][today]
    timetable_today = @@lessons[:timetables][today]
    pretty_folder_names_for_teacher = {}
    @@lessons[:lesson_keys].keys.each do |lesson_key|
        @@lessons[:lesson_keys][lesson_key][:id] = Digest::SHA2.hexdigest(LESSON_ID_SALT + lesson_key).to_i(16).to_s(36)[0, 16]
        lesson_info = @@lessons[:lesson_keys][lesson_key]
        fach = lesson_info[:fach]
        fach = @@faecher[fach] || fach
        pretty_folder_name = "#{fach.gsub('/', '-')} (#{lesson_info[:klassen].sort.map { |x| tr_klasse(x) }.join(', ')})"
        lesson_info[:lehrer].each do |shorthand|
            pretty_folder_names_for_teacher[shorthand] ||= {}
            pretty_folder_names_for_teacher[shorthand][pretty_folder_name] ||= Set.new()
            pretty_folder_names_for_teacher[shorthand][pretty_folder_name] << lesson_key
        end
        @@lessons[:lesson_keys][lesson_key][:pretty_folder_name] = pretty_folder_name
    end
    # if we have 2x Chemie GK (11) for one teacher, differentiate with A and B
    @@lessons[:lesson_keys].keys.each do |lesson_key|
        lesson_info = @@lessons[:lesson_keys][lesson_key]
        pretty_folder_name = lesson_info[:pretty_folder_name]
        more_than_one = false
        lesson_info[:lehrer].each do |shorthand|
            if ((pretty_folder_names_for_teacher[shorthand] || {})[pretty_folder_name] || Set.new()).size > 1
                (pretty_folder_names_for_teacher[shorthand] || {})[pretty_folder_name].sort.each.with_index do |lesson_key, _|
                    @@lessons[:lesson_keys][lesson_key][:pretty_folder_name] = pretty_folder_name.dup.insert(pretty_folder_name.rindex('('), ('A'.ord + _).chr + ' ')
                end
                pretty_folder_names_for_teacher[shorthand].delete(pretty_folder_name)
            end
        end
    end
    @@lessons_for_klasse = {}
    @@lessons[:lesson_keys].each_pair do |lesson_key, lesson|
        lesson[:klassen].each do |klasse|
            @@lessons_for_klasse[klasse] ||= []
            @@lessons_for_klasse[klasse] << lesson_key
        end
    end
    @@lessons_for_shorthand = {}
    @@lessons[:lesson_keys].each_pair do |lesson_key, lesson|
        lesson[:lehrer].each do |lehrer|
            @@lessons_for_shorthand[lehrer] ||= []
            @@lessons_for_shorthand[lehrer] << lesson_key
        end
    end

    @@klassen_for_shorthand = {}
    @@teachers_for_klasse = {}

    self.fix_lessons_for_shorthand()

    @@lessons_for_shorthand.keys.each do |shorthand|
        @@lessons_for_shorthand[shorthand].sort! do |_a, _b|
            a = @@lessons[:lesson_keys][_a] || {}
            b = @@lessons[:lesson_keys][_b] || {}
            (a[:fach] == b[:fach]) ?
            (((a[:klassen] || []).map { |x| @@klassen_order.index(x) || -1}.min || 0) <=> ((b[:klassen] || []).map { |x| @@klassen_order.index(x) || -1 }.min || 0)) :
            (a[:fach] <=> b[:fach])
        end
    end
    @@lessons[:lesson_keys].each_pair do |lesson_key, lesson|
        next if lesson_key[0, 8] == 'Testung_'
        lesson[:klassen].each do |klasse|
            if @@klassen_order.include?(klasse)
                lesson[:lehrer].each do |lehrer|
                    @@klassen_for_shorthand[lehrer] ||= Set.new()
                    @@klassen_for_shorthand[lehrer] << klasse
                end
            end
        end
    end
    @@klassen_for_shorthand.keys.each do |shorthand|
        @@klassen_for_shorthand[shorthand] = @@klassen_for_shorthand[shorthand].to_a.sort do |a, b|
            @@klassen_order.index(a) <=> @@klassen_order.index(b)
        end
    end
    unless @@lessons[:start_dates].empty?
        @@lessons[:timetables][@@lessons[:start_dates].last].each_pair do |lesson_key, lesson_info|
            lesson = @@lessons[:lesson_keys][lesson_key]
            next if lesson[:fach] == 'Testung'
            lesson[:klassen].each do |klasse|
                @@teachers_for_klasse[klasse] ||= {}
                lesson[:lehrer].each do |lehrer|
                    @@teachers_for_klasse[klasse][lehrer] ||= {}
                    @@teachers_for_klasse[klasse][lehrer][lesson[:fach]] ||= 0
                    lesson_info[:stunden].each_pair do |dow, stunden|
                        stunden.each_pair do |i, stunde|
                            @@teachers_for_klasse[klasse][lehrer][lesson[:fach]] += stunde[:count]
                        end
                    end
                end
            end
        end
    end

    last_start_date = nil
    @@lessons[:start_dates].each do |start_date|
        if last_start_date
            added_lesson_keys = Set.new(@@lessons[:timetables][start_date].keys) - Set.new(@@lessons[:timetables][last_start_date].keys)
            removed_lesson_keys = Set.new(@@lessons[:timetables][last_start_date].keys) - Set.new(@@lessons[:timetables][start_date].keys)
            (added_lesson_keys + removed_lesson_keys).to_a.sort.each do |lesson_key|
            end
        end
        last_start_date = start_date
    end

    kurse_for_schueler, schueler_for_kurs = parser.parse_kurswahl(@@user_info.reject { |x, y| y[:teacher] }, @@lessons, lesson_key_tr, @@original_lesson_key_for_lesson_key, @@shorthands)
    @@kurse_for_schueler = kurse_for_schueler
    wahlpflicht_sus_for_lesson_key = parser.parse_wahlpflichtkurswahl(@@user_info.reject { |x, y| y[:teacher] }, @@lessons, lesson_key_tr, @@schueler_for_klasse)
    sesb_sus = parser.parse_sesb(@@user_info.reject { |x, y| y[:teacher] }, @@schueler_for_klasse)
    sesb_sus.each do |email|
        @@user_info[email][:sesb] = true
    end

    @@materialamt_for_lesson = {}
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User)-[r:HAS_AMT {amt: 'material'}]->(l:Lesson)
        RETURN u.email, l.key;
    END_OF_QUERY
    rows.each do |row|
        @@materialamt_for_lesson[row['l.key']] ||= Set.new()
        @@materialamt_for_lesson[row['l.key']] << row['u.email']
    end

    @@lessons_for_user = {}
    @@schueler_for_lesson = {}
    @@schueler_offset_in_lesson = {}
    @@user_info.keys.sort do |a, b|
        (@@user_info[a][:last_name].downcase == @@user_info[b][:last_name].downcase) ?
        (@@user_info[a][:first_name].downcase <=> @@user_info[b][:first_name].downcase) :
        (@@user_info[a][:last_name].downcase <=> @@user_info[b][:last_name].downcase)
    end.each do |email|
        user = @@user_info[email]
        lessons = (user[:teacher] ? @@lessons_for_shorthand[user[:shorthand]] : @@lessons_for_klasse[user[:klasse]]).dup
        unless user[:teacher]
            if ['11', '12'].include?(user[:klasse])
                lessons = (kurse_for_schueler[email] || Set.new()).to_a
            end
        end
        lessons ||= []
        unless user[:teacher]
            lessons.reject! do |lesson_key|
                lesson = @@lessons[:lesson_keys][lesson_key]
                (lesson[:fach] == 'SpoM' && user[:geschlecht] != 'w') ||
                (lesson[:fach] == 'SpoJ' && user[:geschlecht] != 'm')
            end
        end
        lessons += wahlpflicht_sus_for_lesson_key.keys
        lessons.uniq!
        unless user[:teacher]
            lessons.each do |lesson_key|
                if wahlpflicht_sus_for_lesson_key.include?(lesson_key)
                    next unless wahlpflicht_sus_for_lesson_key[lesson_key].include?(email)
                end
                @@schueler_for_lesson[lesson_key] ||= []
                @@schueler_for_lesson[lesson_key] << email
                @@lessons_for_user[email] ||= Set.new()
                @@lessons_for_user[email] << lesson_key
            end
        end
    end
    @@schueler_for_lesson.each_pair do |lesson_key, emails|
        @@schueler_offset_in_lesson[lesson_key] ||= {}
        sorted_emails = emails.sort do |a, b|
            @@user_info[a][:display_name] <=> @@user_info[b][:display_name]
        end
        sorted_emails.each.with_index do |email, i|
            @@schueler_offset_in_lesson[lesson_key][email] = i
        end
    end

    @@schueler_for_teacher = {}
    @@lessons_for_shorthand.each_pair do |shorthand, lesson_keys|
        @@schueler_for_teacher[shorthand] ||= Set.new()
        lesson_keys.each do |lesson_key|
            (@@schueler_for_lesson[lesson_key] || []).each do |email|
                @@schueler_for_teacher[shorthand] << email
            end
        end
    end

    @@pausenaufsichten = parser.parse_pausenaufsichten(@@config)

    @@mailing_lists = {}
    self.update_antikenfahrt_groups()
    self.update_mailing_lists()
    @@current_email_addresses = parser.parse_current_email_addresses()

    @@holiday_dates = Set.new()
    @@ferien_feiertage.each do |entry|
        temp0 = Date.parse(entry[:from])
        temp1 = Date.parse(entry[:to])
        while temp0 <= temp1
            @@holiday_dates << temp0.strftime('%Y-%m-%d')
            temp0 += 1
        end
    end

    @@room_ids = {}
    ROOM_ORDER.each do |room|
        @@room_ids[room] = Digest::SHA2.hexdigest(KLASSEN_ID_SALT + room).to_i(16).to_s(36)[0, 16]
    end
    @@rooms_for_shorthand = {}
    room_order_set = Set.new(ROOM_ORDER)
    undeclared_rooms = Set.new()
    unless timetable_today.nil?
        timetable_today.each_pair do |lesson_key, info|
            info[:stunden].each_pair do |wday, day_info|
                day_info.each_pair do |stunde, lesson_info|
                    lesson_info[:lehrer].each do |shorthand|
                        (lesson_info[:raum] || '').split('/').each do |room|
                            unless (room || '').strip.empty?
                                if room_order_set.include?(room)
                                    @@rooms_for_shorthand[shorthand] ||= Set.new()
                                    @@rooms_for_shorthand[shorthand] << room
                                else
                                    undeclared_rooms << room
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    unless undeclared_rooms.empty?
        debug("Undeclared rooms: #{undeclared_rooms.to_a.sort.join(' ')}")
    end
    @@lesson_keys_with_sus_feedback = {}
    if File.exist?('/data/kurswahl/sus_feedback.yaml')
        @@lesson_keys_with_sus_feedback = YAML::load_file('/data/kurswahl/sus_feedback.yaml')
    end

    if ENV['DASHBOARD_SERVICE'] == 'ruby'
        self.parse_zeugnisformulare()
    end

    if ENV['DASHBOARD_SERVICE'] == 'ruby'
        FileUtils.rm_rf('/internal/debug/')
        FileUtils.mkpath('/internal/debug/')
        Main.class_variables.each do |x|
            File.open(File.join('/internal/debug', "#{x.to_s}.yaml"), 'w') do |f|
                f.write Main.class_variable_get(x).to_yaml
            end
        end
        File::open('/internal/debug/emails.txt', 'w') do |f|
            @@user_info.keys.sort.each do |email|
                f.puts "#{email}"
            end
        end
    end
end

.compile_cssObject



1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
# File 'src/ruby/main.rb', line 1254

def self.compile_css()
    files = [
        '/include/flowbite/flowbite.min.css',
        '/include/bootstrap/bootstrap.min.css',
        '/include/summernote/summernote-bs4.min.css',
        '/include/fork-awesome/fork-awesome.min.css',
        '/include/fullcalendar/main.min.css',
        '/include/bootstrap4-toggle/bootstrap4-toggle.min.css',
        '/include/dropzone/dropzone.min.css',
        '/include/chart.js/Chart.min.css',
        '/styles.css',
        '/cling.css',
        '/include/print.min.css',
        '/include/odometer-theme-default.css',
    ]

    self.compile_files(:css, 'text/css', files)
    FileUtils::rm_rf('/gen/css/')
    FileUtils::mkpath('/gen/css/')
    File.open("/gen/css/compiled-#{@@compiled_files[:css][:sha1]}.css", 'w') do |f|
        f.print(@@compiled_files[:css][:content])
    end
end

.compile_files(key, mimetype, paths) ⇒ Object



1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
# File 'src/ruby/main.rb', line 1166

def self.compile_files(key, mimetype, paths)
    @@compiled_files[key] ||= {:timestamp => nil, :content => nil}

    latest_file_timestamp = paths.map do |path|
        File.mtime(File.join('/static', path))
    end.max

    if @@compiled_files[key][:timestamp].nil? || @@compiled_files[key][:timestamp] < latest_file_timestamp
        @@compiled_files[key][:content] = StringIO.open do |io|
            paths.each do |path|
                io.puts File.read(File.join('/static', path))
            end
            io.string
        end
        @@compiled_files[key][:sha1] = Digest::SHA1.hexdigest(@@compiled_files[key][:content])[0, 16]
        @@compiled_files[key][:timestamp] = latest_file_timestamp
    end
end

.compile_jsObject



1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
# File 'src/ruby/main.rb', line 1214

def self.compile_js()
    files = [
        '/include/jquery/jquery-3.6.1.min.js',
        '/include/jquery-ui/jquery-ui.min.js',
        '/include/popper.js/popper.min.js',
        '/include/bootstrap/bootstrap.min.js',
        '/include/fullcalendar/main.min.js',
        '/include/fullcalendar/de.js',
        '/include/pako/pako_inflate.min.js',
        '/include/bootstrap4-toggle/bootstrap4-toggle.min.js',
        '/include/summernote/summernote-bs4.min.js',
        '/include/summernote/summernote-de-DE.min.js',
        '/include/clipboard/clipboard.min.js',
        '/include/moment/moment-with-locales.min.js',
        '/include/dropzone/dropzone.min.js',
        '/include/chart.js/Chart.min.js',
        '/include/jszip/dist/jszip.min.js',
        '/include/flowbite/flowbite.js',
        '/code.js',
        '/include/zxing.min.js',
        '/barcode-widget.js',
        '/sound.js',
        '/include/howler.core.min.js',
        '/sortable-table.js',
        '/include/print.min.js',
        '/include/odometer.min.js',
        '/include/zoom.min.js',
        '/include/turn.min.js',
        '/include/scissor.min.js',
        '/include/hash.js',
    ]

    self.compile_files(:js, 'application/javascript', files)
    FileUtils::rm_rf('/gen/js/')
    FileUtils::mkpath('/gen/js/')
    File.open("/gen/js/compiled-#{@@compiled_files[:js][:sha1]}.js", 'w') do |f|
        f.print(@@compiled_files[:js][:content])
    end
end

.determine_lehrmittelverein_state_for_allObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'src/ruby/include/lehrbuchverein.rb', line 2

def self.determine_lehrmittelverein_state_for_all()
    @@lehrmittelverein_state_cache = {}
    @@user_info.each_pair do |email, info|
        @@lehrmittelverein_state_cache[email] = info[:teacher]
    end
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| { :email => x['u.email'] } }
        MATCH (u:User {lmv_no_pay: true})
        RETURN u.email;
    END_OF_QUERY
    temp.each do |row|
        email = row[:email]
        @@lehrmittelverein_state_cache[email] = true
    end
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY, {:jahr => LEHRBUCHVEREIN_JAHR}).map { |x| { :email => x['u.email'] } }
        MATCH (u:User)-[:PAID_FOR]->(j:Lehrbuchvereinsjahr {jahr: $jahr})
        RETURN u.email;
    END_OF_QUERY
    temp.each do |row|
        email = row[:email]
        @@lehrmittelverein_state_cache[email] = true
    end
end

.determine_zeugnislistenObject



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'src/ruby/include/zeugnisse.rb', line 69

def self.determine_zeugnislisten()
    STDERR.puts "ATTENTION determine_zeugnislisten() IS DOING NOTHING RIGHT NOW"
    return
    @@all_zeugnis_faecher = Set.new()
    FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR].each_pair do |key, faecher|
        faecher.each do |fach|
            @@all_zeugnis_faecher << fach.sub('$', '')
        end
    end

    @@zeugnisliste_for_klasse = {}
    @@zeugnisliste_for_lehrer = {}

    kurse_for_klasse = Hash[ZEUGNIS_KLASSEN_ORDER.map do |klasse|
        [klasse, (@@lessons_for_klasse[klasse] || []).map { |x| @@lessons[:lesson_keys][x].merge({:lesson_key => x})}]
    end]

    delegates = {}
    delegates_for_klasse = {}
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :path => "#{ZEUGNIS_SCHULJAHR}/#{ZEUGNIS_HALBJAHR}/")
        MATCH (n:ZeugnisDelegate)-[:WHO]->(u:User)
        WHERE n.path STARTS WITH $path
        RETURN n.path AS path, u.email AS email;
    END_OF_QUERY
    rows.each do |row|
        delegates[row['path']] ||= Set.new()
        delegates[row['path']] << row['email']
        klasse = row['path'].split('/')[2]
        delegates_for_klasse[klasse] ||= {}
        delegates_for_klasse[klasse][row['path']] ||= Set.new()
        delegates_for_klasse[klasse][row['path']] << row['email']
    end

    if ZEUGNIS_USE_MOCK_NAMES
        srand(42)
    end
    ZEUGNIS_KLASSEN_ORDER.each do |klasse|
        lesson_keys_for_fach = {}
        shorthands_for_fach = {}
        # get teachers from stundenplan
        (kurse_for_klasse[klasse]).each do |kurs|
            next unless @@all_zeugnis_faecher.include?(kurs[:fach])
            lesson_keys_for_fach[kurs[:fach]] ||= []
            lesson_keys_for_fach[kurs[:fach]] << kurs[:lesson_key]
            fach = ZEUGNIS_CONSOLIDATE_FACH[kurs[:fach]] || kurs[:fach]
            shorthands = kurs[:lehrer]
            # check if we have delegate overrides
            path = "#{ZEUGNIS_SCHULJAHR}/#{ZEUGNIS_HALBJAHR}/#{klasse}/#{fach}"
            if delegates[path]
                shorthands = delegates[path].to_a.map { |email| @@user_info[email][:shorthand] }
            end
            shorthands.each do |shorthand|
                shorthands_for_fach[fach] ||= {}
                shorthands_for_fach[fach][shorthand] = true
                @@zeugnisliste_for_lehrer[shorthand] ||= {}
                @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}"] = true
                if FAECHER_SPRACHEN.include?(fach)
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}_AT"] = true
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}_SL"] = true
                end
                ['ZV', 'LLB', 'SSK', 'KF', 'SV'].each do |item|
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/#{fach}"] = true
                end
            end
        end
        # get teachers from delegate entries
        (delegates_for_klasse[klasse] || {}).each_pair do |path, emails|
            fach = path.split('/')[3]
            emails.to_a.sort.each do |email|
                shorthand = @@user_info[email][:shorthand]
                shorthands_for_fach[fach] ||= {}
                shorthands_for_fach[fach][shorthand] = true
                @@zeugnisliste_for_lehrer[shorthand] ||= {}
                @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}"] = true
                if FAECHER_SPRACHEN.include?(fach)
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}_AT"] = true
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{fach}_SL"] = true
                end
                ['ZV', 'LLB', 'SSK', 'KF', 'SV'].each do |item|
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/#{fach}"] = true
                end
            end

        end
        
        @@zeugnisliste_for_klasse[klasse] = {}
        @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach] = {}
        @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach_is_delegate] = {}
        (@@klassenleiter[klasse] || []).each do |shorthand|
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'] ||= []
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'] << shorthand
            ['VT', 'VT_UE', 'VS', 'VS_UE', 'VSP'].each do |item|
                @@zeugnisliste_for_lehrer[shorthand] ||= {}
                @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}"] = true
            end
            ['ZV', 'LLB', 'SSK', 'KF', 'SV'].each do |item|
                @@zeugnisliste_for_lehrer[shorthand] ||= {}
                @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/_KL"] = true
            end
        end
        path = "#{ZEUGNIS_SCHULJAHR}/#{ZEUGNIS_HALBJAHR}/#{klasse}/_KL"
        if delegates[path]
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'] = []
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach_is_delegate]['_KL'] = true
            delegates[path].each do |email|
                shorthand = @@user_info[email][:shorthand]
                @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'] << shorthand
                ['VT', 'VT_UE', 'VS', 'VS_UE', 'VSP'].each do |item|
                    @@zeugnisliste_for_lehrer[shorthand] ||= {}
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}"] = true
                end
                ['ZV', 'LLB', 'SSK', 'KF', 'SV'].each do |item|
                    @@zeugnisliste_for_lehrer[shorthand] ||= {}
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/_KL"] = true
                end
            end
        end
        @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'] ||= []
        @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach]['_KL'].uniq!
        faecher = self.zeugnis_faecher_for_emails(@@schueler_for_klasse[klasse])
        @@zeugnisliste_for_klasse[klasse][:faecher] = faecher.map do |x|
            x[0] == '$' ? x[1, x.size - 1] : x
        end
        @@zeugnisliste_for_klasse[klasse][:faecher].uniq!
        @@zeugnisliste_for_klasse[klasse][:wahlfach] = Hash[faecher.map do |x|
            [x.sub('$', ''), x[0] == '$' ? true : false]
        end]
        @@zeugnisliste_for_klasse[klasse][:faecher].each do |fach|
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach][fach] = (shorthands_for_fach[fach] || {}).keys
            path = "#{ZEUGNIS_SCHULJAHR}/#{ZEUGNIS_HALBJAHR}/#{klasse}/#{fach}"
            @@zeugnisliste_for_klasse[klasse][:lehrer_for_fach_is_delegate][fach] = delegates.include?(path)
        end
        @mock = {}
        if ZEUGNIS_USE_MOCK_NAMES
            @mock[:nachnamen] = JSON.parse(File.read('mock/nachnamen.json'))
            @mock[:vornamen] = {
                'm' => JSON.parse(File.read('mock/vornamen-m.json')),
                'w' => JSON.parse(File.read('mock/vornamen-w.json'))
            }
        end

        @@zeugnisliste_for_klasse[klasse][:index_for_schueler] ||= {}
        @@schueler_for_klasse[klasse].each.with_index do |email, index|
            @@zeugnisliste_for_klasse[klasse][:index_for_schueler][email] = index
        end

        @@zeugnisliste_for_klasse[klasse][:schueler] = @@schueler_for_klasse[klasse].map do |email|
            geschlecht = ZEUGNIS_USE_MOCK_NAMES ? ['m', 'w'].sample : @@user_info[email][:geschlecht]
            {
                :email => email,
                :zeugnis_key => self.zeugnis_key_for_email(email),
                :official_first_name => ZEUGNIS_USE_MOCK_NAMES ? @mock[:vornamen][geschlecht].sample : @@user_info[email][:official_first_name],
                :last_name => ZEUGNIS_USE_MOCK_NAMES ? @mock[:nachnamen].sample : @@user_info[email][:last_name],
                :geburtstag => ZEUGNIS_USE_MOCK_NAMES ? sprintf("%04d-%02d-%02d", @@user_info[email][:geburtstag][0, 4].to_i, (1..12).to_a.sample, (1..28).to_a.sample) : @@user_info[email][:geburtstag],
                :geschlecht => geschlecht,
            }
        end
    end
end

.fix_lesson_key_tr(lesson_key_tr) ⇒ Object



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'src/ruby/credentials.rb', line 395

def self.fix_lesson_key_tr(lesson_key_tr)
    # # Kursawe: Agr 9c => Agr 9c, 9e
    # lesson_key_tr['Agr~741a'] = 'Agr~184a'
    # # Hollmann-Stelzer: Agr 10c => Agr 10c, 10e
    # lesson_key_tr['Agr~207a'] = 'Agr~403a'
    # # Munk: Deutsch GK 11
    # lesson_key_tr['de-G01~497a'] = 'de-G01~474a'
    # # Crome: Deutsch GK 11
    # lesson_key_tr['de3~497a'] = 'de1~477a'
    # # Liebe: Deutsch GK 11
    # lesson_key_tr['de6~497a'] = 'de1~769a'
    # # Kilian: Deutsch GK 12
    # lesson_key_tr['de5~755a'] = 'de1~513a'
    # # AP: Deutsch GK 12
    # lesson_key_tr['de3~755a'] = 'de1~474b'
    # # AP / Ho: Deutsch GK 11
    # lesson_key_tr['de4~497a'] = 'de1~477a'
    # lesson_key_tr['ds-G119~791a'] = 'ds-G119~505a'
    # lesson_key_tr['mo-G46~795a'] = 'mo-G46~786a'
    # # [???] de4~497a Deutsch GK 11 Frau Arias Porras, Frau Hodgkiss 1 Stunde
    # # [???] en1~499a Englisch GK 11 Herr Weging, 6 Stunden
    # lesson_key_tr['de03'] = 'de-G02'
    
    lesson_key_tr
end

.fix_lessons_for_shorthandObject



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'src/ruby/credentials.rb', line 421

def self.fix_lessons_for_shorthand
    # @@lessons_for_shorthand['Led'] ||= []
    # @@lessons_for_shorthand['Led'] << 'Ma~99a'
    # @@lessons_for_shorthand['Sch'] ||= []
    # @@lessons_for_shorthand['Sch'] << 'Ge~353a'
    # @@lessons_for_shorthand['Sch'] << 'GE-L11~474a'
    # @@lessons_for_shorthand['Sch'] << 'evR~331a'
    # @@lessons_for_shorthand['Sch'] << 'evR~709a'
    # @@lessons_for_shorthand['Sch'] << 'evR~399a'
    # @@lessons_for_shorthand['Eb'] ||= []
    # @@lessons_for_shorthand['Eb'] << 'katR~830a'
    # @@lessons_for_shorthand['Wr'] ||= []
    # @@lessons_for_shorthand['Wr'] << 'ch-G35~788a'
    # @@lessons_for_shorthand['Wr'] << 'Ch~115a'
    # @@lessons_for_shorthand['Wr'] << 'Ch~160a'
    # @@lessons_for_shorthand['Wr'] << 'BI-L115~515a'
    # @@lessons_for_shorthand['Wr'] << 'Bio~190a'
    # @@lessons_for_shorthand['Eb'] ||= []
    # @@lessons_for_shorthand['Eb'] << 'Pol~118a'
    # @@lessons_for_shorthand['Rk'] ||= []
    # @@lessons_for_shorthand['Rk'] << 'D~609a'
    # @@lessons_for_shorthand['Rk'] << 'EN-L105-107~513a'
    # @@lessons_for_shorthand['Rk'] << 'En~21a'
    # @@lessons_for_shorthand['Rk'] << 'En~258a'
    # @@lessons_for_shorthand['Rk'] << 'EN-L05-07~529a'
    # @@lessons_for_shorthand['Rk'] << 'En~354a'
    # @@lessons_for_shorthand['Rk'] << 'En~611a'
    # @@lessons_for_shorthand['Rk'] << 'En~196a'
    # @@lessons_for_shorthand['Rk'] << 'en-G109-111~557a'
    # @@lessons_for_shorthand['Rk'] << 'En~48a'
    # @@lessons_for_shorthand['Rk'] << 'En~12a'
    
    # ['5b', '7a', '8a', '9b', '11'].each do |klasse|
    #     @@teachers_for_klasse[klasse] ||= {}
    #     @@teachers_for_klasse[klasse]['Sch'] ||= {}
    #     @@klassen_for_shorthand['Sch'] ||= Set.new()
    #     @@klassen_for_shorthand['Sch'] << klasse
    # end
    # ['7a', '7b', '7c'].each do |klasse|
    #     @@teachers_for_klasse[klasse] ||= {}
    #     @@teachers_for_klasse[klasse]['Eb'] ||= {}
    #     @@klassen_for_shorthand['Eb'] ||= Set.new()
    #     @@klassen_for_shorthand['Eb'] << klasse
    # end
    # ['8a', '9a', '9c', '11', '12'].each do |klasse|
    #     @@teachers_for_klasse[klasse] ||= {}
    #     @@teachers_for_klasse[klasse]['Wr'] ||= {}
    #     @@klassen_for_shorthand['Wr'] ||= Set.new()
    #     @@klassen_for_shorthand['Wr'] << klasse
    # end
    # ['11', '12', '5b', '5c', '6c', '6d', '8a', '9b', '9e'].each do |klasse|
    #     @@teachers_for_klasse[klasse] ||= {}
    #     @@teachers_for_klasse[klasse]['Rk'] ||= {}
    #     @@klassen_for_shorthand['Rk'] ||= Set.new()
    #     @@klassen_for_shorthand['Rk'] << klasse
    # end
    @@klassen_for_shorthand['_Sek'] = KLASSEN_ORDER
end

.fix_parsed_klasse(klasse) ⇒ Object



480
481
482
483
# File 'src/ruby/credentials.rb', line 480

def self.fix_parsed_klasse(klasse)
    d = {'12' => '11', '13' => '12'}
    d[klasse] || klasse
end

.fix_public_event_configObject



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'src/ruby/include/public_event.rb', line 324

def self.fix_public_event_config
    debug "Fixing public event config!"
    @@public_event_config.map! do |entry|
        if entry[:auto_rows]
            rows = []
            entry[:auto_rows].each do |auto_entry|
                row = {}
                day = Date.parse(auto_entry[:day])
                row[:description] = "#{%w(So Mo Di Mi Do Fr Sa)[day.wday]}., #{day.strftime("%d.%m.%Y")}"
                row[:entries] = []
                auto_entry[:spans].each do |span|
                    t = Time.parse("#{auto_entry[:day]}T#{span[0]}")
                    while t <= Time.parse("#{auto_entry[:day]}T#{span[1]}")
                        row[:entries] << {
                            :key => "#{t.strftime("%Y-%m-%dT%H:%M")}",
                            :deadline => entry[:auto_rows_no_signup_deadline] ? "#{(t - entry[:auto_rows_no_signup_deadline] * 3600).strftime("%Y-%m-%dT%H:%M")}" : nil,
                            :description => "#{t.strftime("%H:%M")} Uhr",
                            :capacity => 1,
                        }
                        t += auto_entry[:duration] * 60
                    end
                end
                rows << row
            end
            entry[:rows] = rows
        end
        entry
    end
end

.fix_stundenzeitenObject



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'src/ruby/credentials.rb', line 261

def self.fix_stundenzeiten
    hd = '2020-08-10'
    HOURS_FOR_KLASSE[hd] = {}
    @@klassen_order.each do |klasse|
        klassenstufe = klasse.to_i
        HOURS_FOR_KLASSE[hd][klasse] = []
        if [5, 6].include?(klassenstufe)
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:40']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:55', '10:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:50', '13:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        elsif [7, 8, 9, 10].include?(klassenstufe)
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:35', '10:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:15', '13:00']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        else
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:00', '09:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:45', '10:30']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:35', '11:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:20', '12:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:40', '13:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:30', '14:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        end
    end
                       
    hd = '2020-11-23'
    HOURS_FOR_KLASSE[hd] = {}
    @@klassen_order.each do |klasse|
        klassenstufe = klasse.to_i
        HOURS_FOR_KLASSE[hd][klasse] = []
        if klassenstufe == 5 || klassenstufe == 6 || klasse == '7e'
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:40']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:40', '10:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:15', '13:00']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        elsif [8, 9, 10].include?(klassenstufe) || ['7a', '7b', '7c'].include?(klasse)
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:40']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:55', '10:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:50', '13:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        else
            HOURS_FOR_KLASSE[hd][klasse] << ['07:15', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:00', '09:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:45', '10:30']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:35', '11:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:20', '12:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:40', '13:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:30', '14:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        end
    end
                       
    hd = '2021-01-04'
    HOURS_FOR_KLASSE[hd] = {}
    @@klassen_order.each do |klasse|
        klassenstufe = klasse.to_i
        HOURS_FOR_KLASSE[hd][klasse] = []
        if [5, 6].include?(klassenstufe) || klasse == '7e'
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:40']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:40', '10:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:15', '13:00']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        elsif [7, 8, 9, 10].include?(klassenstufe)
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['08:50', '09:40']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:55', '10:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:40', '11:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:25', '12:10']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:50', '13:35']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:40', '14:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        else
            HOURS_FOR_KLASSE[hd][klasse] << ['08:00', '08:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:00', '09:45']
            HOURS_FOR_KLASSE[hd][klasse] << ['09:45', '10:30']
            HOURS_FOR_KLASSE[hd][klasse] << ['10:35', '11:20']
            HOURS_FOR_KLASSE[hd][klasse] << ['11:20', '12:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['12:40', '13:25']
            HOURS_FOR_KLASSE[hd][klasse] << ['13:30', '14:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['14:30', '15:15']
            HOURS_FOR_KLASSE[hd][klasse] << ['15:20', '16:05']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:10', '16:55']
            HOURS_FOR_KLASSE[hd][klasse] << ['16:55', '17:40']
        end
    end
end

.force_reload_monitorsObject



23
24
25
26
27
28
# File 'src/ruby/include/monitor.rb', line 23

def self.force_reload_monitors
    (@@ws_clients[:monitor] || {}).each_pair do |client_id, info|
        ws = info[:ws]
        ws.send({:command => 'force_reload'}.to_json)
    end
end

.gen_password_for_email(email) ⇒ Object



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'src/ruby/main.rb', line 439

def self.gen_password_for_email(email)
    chars = 'BCDFGHJKMNPQRSTVWXYZ23456789'.split('')
    sha2 = Digest::SHA256.new()
    sha2 << EMAIL_PASSWORD_SALT
    sha2 << email
    srand(sha2.hexdigest.to_i(16))
    password = ''
    while true do
        if password =~ /[a-z]/ &&
        password =~ /[A-Z]/ &&
        password =~ /[0-9]/ &&
        password.include?('-')
            break
        end
        password = ''
        8.times do
            c = chars.sample.dup
            c.downcase! if [0, 1].sample == 1
            password += c
        end
        password += '-'
        4.times do
            c = chars.sample.dup
            c.downcase! if [0, 1].sample == 1
            password += c
        end
    end
    password
end

.get_all_homeschooling_usersObject



284
285
286
287
288
289
290
291
292
293
294
# File 'src/ruby/include/directory.rb', line 284

def self.get_all_homeschooling_users()
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User {homeschooling: true})
        RETURN u.email
    END_OF_QUERY
    all_homeschooling_users = Set.new()
    temp.each do |user|
        all_homeschooling_users << user['u.email']
    end
    all_homeschooling_users
end

.get_all_stream_restrictionsObject



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'src/ruby/include/lesson.rb', line 474

def self.get_all_stream_restrictions()
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (l:Lesson)
        RETURN l.key AS lesson_key, COALESCE(l.stream_restriction, []) AS restriction
    END_OF_QUERY
    results = {}
    temp.each do |entry|
        lesson_key = entry['lesson_key']
        restriction = entry['restriction']
        while restriction.size < 5
            restriction << 0
        end
        results[lesson_key] = restriction
    end
    results
end

.get_aula_eventsObject



3
4
5
6
7
8
9
10
# File 'src/ruby/include/aula.rb', line 3

def self.get_aula_events
    results = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| x['e'] }
        MATCH (e:AulaEvent)
        RETURN e
        ORDER BY e.number, e.title;
    END_OF_QUERY
    results
end

.get_current_ab_weekObject

Returns A or B or nil



308
309
310
# File 'src/ruby/include/directory.rb', line 308

def self.get_current_ab_week()
    get_switch_week_for_date(Date.today)
end

.get_current_salzh_statusObject



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'src/ruby/include/salzh.rb', line 343

def self.get_current_salzh_status
    entries = []
    temp = self.get_salzh_status_for_emails()
    temp.each_pair do |email, info|
        next unless @@user_info[email]
        next if @@user_info[email][:teacher]
        next if info[:status].nil?
        entries << {
            :email => email,
            :name => @@user_info[email][:display_name],
            :first_name => @@user_info[email][:display_first_name],
            :last_name => @@user_info[email][:display_last_name],
            :klasse => @@user_info[email][:klasse],
            :klasse_tr => tr_klasse(@@user_info[email][:klasse]),
            :status => info[:status],
            :status_end_date => info[:status_end_date]
        }
    end
    entries.sort! do |a, b|
        if a[:klasse] == b[:klasse]
            if a[:last_name] == b[:last_name]
                a[:first_name] <=> b[:first_name]
            else
                a[:last_name] <=> b[:last_name]
            end
        else
            (KLASSEN_ORDER.index(a[:klasse]) <=> KLASSEN_ORDER.index(b[:klasse]))
        end
    end
    entries
end

.get_current_salzh_status_for_all_teachersObject



387
388
389
390
391
392
393
394
395
396
397
398
# File 'src/ruby/include/salzh.rb', line 387

def self.get_current_salzh_status_for_all_teachers
    all_entries = Main.get_current_salzh_status
    result = {}
    @@lehrer_order.each do |email|
        shorthand = @@user_info[email][:shorthand]
        entries = all_entries.select do |entry|
            (@@schueler_for_teacher[shorthand] || []).include?(entry[:email])
        end
        result[shorthand] = entries
    end
    result
end

.get_current_salzh_susObject



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'src/ruby/include/salzh.rb', line 259

def self.get_current_salzh_sus
    # purge stale salzh entries !!!
    Main.purge_stale_salzh_entries(false)

    rows = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (s:Salzh)-[:BELONGS_TO]->(u:User)
        RETURN u.email, COALESCE(s.mode, 'salzh') AS smode, s.end_date;
    END_OF_QUERY

    entries = []
    rows.each do |row|
        email = row['u.email']
        mode = row['smode']
        end_date = row['s.end_date']
        entries << {
            :email => email,
            :name => @@user_info[email][:display_name],
            :first_name => @@user_info[email][:display_first_name],
            :last_name => @@user_info[email][:display_last_name],
            :klasse => @@user_info[email][:klasse],
            :klasse_tr => tr_klasse(@@user_info[email][:klasse]),
            :salzh_mode => mode,
            :salzh_end_date => end_date
        }
    end

    entries.sort! do |a, b|
        if a[:klasse] == b[:klasse]
            if a[:last_name] == b[:last_name]
                a[:first_name] <=> b[:first_name]
            else
                a[:last_name] <=> b[:last_name]
            end
        else
            (KLASSEN_ORDER.index(a[:klasse]) <=> KLASSEN_ORDER.index(b[:klasse]))
        end
    end
    entries
end

.get_homeschooling_for_user(email, datum = nil, is_homeschooling_user = nil, group2_for_email = nil) ⇒ Object



337
338
339
340
341
342
343
344
# File 'src/ruby/include/directory.rb', line 337

def self.get_homeschooling_for_user(email, datum = nil, is_homeschooling_user = nil, group2_for_email = nil)
    datum ||= Date.today.strftime('%Y-%m-%d')
    if is_homeschooling_user.nil?
        self.get_homeschooling_for_user_by_switch_week(email, datum, group2_for_email) || self.get_homeschooling_for_user_by_dauer_salzh(email)
    else
        self.get_homeschooling_for_user_by_switch_week(email, datum, group2_for_email)
    end
end

.get_homeschooling_for_user_by_dauer_salzh(email) ⇒ Object



312
313
314
315
316
317
318
319
# File 'src/ruby/include/directory.rb', line 312

def self.get_homeschooling_for_user_by_dauer_salzh(email)
    info = $neo4j.neo4j_query_expect_one(<<~END_OF_QUERY, {:email => email})
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.homeschooling, false) AS homeschooling
    END_OF_QUERY
    marked_as_homeschooling = info['homeschooling']
    marked_as_homeschooling
end

.get_homeschooling_for_user_by_switch_week(email, datum, group2_for_email) ⇒ Object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'src/ruby/include/directory.rb', line 321

def self.get_homeschooling_for_user_by_switch_week(email, datum, group2_for_email)
    group2 = nil
    if group2_for_email.nil?
        info = $neo4j.neo4j_query_expect_one(<<~END_OF_QUERY, {:email => email})
            MATCH (u:User {email: $email})
            RETURN COALESCE(u.group2, 'A') AS group2
        END_OF_QUERY
        group2 = info['group2']
    else
        group2 = group2_for_email
    end
    current_week = get_switch_week_for_date(Date.parse(datum))
    marked_as_homeschooling_by_week = (current_week != group2)
    marked_as_homeschooling_by_week
end

.get_homework_feedback_for_lesson_key(lesson_key) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'src/ruby/include/homework.rb', line 23

def self.get_homework_feedback_for_lesson_key(lesson_key)
    hf = $neo4j.neo4j_query(<<~END_OF_QUERY, :lesson_key => lesson_key).map { |x| {:offset => x['li.offset'], :hf => x['hf']} }
        MATCH (u:User)<-[:FROM]-(hf:HomeworkFeedback)-[:FOR]->(li:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $lesson_key})
        RETURN hf, li.offset
        ORDER BY li.offset;
    END_OF_QUERY
    results = {}
    hf.each do |_entry|
        entry = _entry[:hf]
        offset = _entry[:offset]
        results[offset] ||= {
            :state_histogram => {},
            :time_spent_min => nil,
            :time_spent_max => nil
        }
        if entry[:state]
            results[offset][:state_histogram][entry[:state]] ||= 0
            results[offset][:state_histogram][entry[:state]] += 1
        end
        if entry[:time_spent]
            results[offset][:time_spent_min] ||= entry[:time_spent]
            results[offset][:time_spent_min] = entry[:time_spent] if entry[:time_spent] < results[offset][:time_spent_min]
            results[offset][:time_spent_max] ||= entry[:time_spent]
            results[offset][:time_spent_max] = entry[:time_spent] if entry[:time_spent] > results[offset][:time_spent_max]
        end
    end
    final_result = {}
    results.each_pair do |offset, data|
        result = {}
        if results[offset][:time_spent_min]
            if results[offset][:time_spent_min] != results[offset][:time_spent_max]
                result[:time_spent] = "#{results[offset][:time_spent_min]}#{results[offset][:time_spent_max]} Minuten"
            else
                result[:time_spent] = "#{results[offset][:time_spent_min]} Minuten"
            end
        end
        unless results[offset][:state_histogram].empty?
            parts = []
            short_parts = []
            HOMEWORK_FEEDBACK_STATES.each do |state|
                if results[offset][:state_histogram][state]
                    parts << "#{HOMEWORK_FEEDBACK_EMOJIS[state]} × #{results[offset][:state_histogram][state]}"
                    short_parts << "#{HOMEWORK_FEEDBACK_EMOJIS[state]}"
                end
            end
            result[:state] = parts.join(', ')
            result[:short_state] = short_parts.join('')
        end
        final_result[offset] = result
    end
    final_result
end

.get_hotspot_klassenObject



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'src/ruby/include/salzh.rb', line 227

def self.get_hotspot_klassen
    # purge stale entries
    Main.purge_stale_salzh_entries(false)

    hotspot_dates = {}
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (k:Klasse)
        RETURN k.klasse, k.hotspot_end_date;
    END_OF_QUERY
    rows.each do |x|
        hotspot_dates[x['k.klasse']] = x['k.hotspot_end_date']
    end
    StringIO.open do |io|
        KLASSEN_ORDER.each do |klasse|
            next if ['11', '12'].include?(klasse)
            io.puts "<tr data-klasse='#{klasse}'>"
            io.puts "<td>#{tr_klasse(klasse)}</td>"
            # io.puts "<td>#{@@schueler_for_klasse[klasse].size}</td>"
            io.puts "<td>"
            hotspot_end_date = hotspot_dates[klasse]
            io.puts "<div class='input-group input-group-sm'><input type='date' class='form-control ti_hotspot_end_date' value='#{hotspot_end_date}' /><div class='input-group-append'><button #{hotspot_end_date.nil? ? 'disabled' : ''} class='btn #{hotspot_end_date.nil? ? 'btn-outline-secondary' : 'btn-danger'} bu_delete_hotspot_end_date'><i class='fa fa-trash'></i></button></div></div>"
            io.puts "</td>"
        io.puts "</tr>"
        end
        io.string
    end
end

.get_lesson_data(lesson_key) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'src/ruby/include/lesson.rb', line 132

def self.get_lesson_data(lesson_key)
    # purge unconfirmed tablet bookings for this lesson_key
    $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key)
        MATCH (t:Tablet)<-[:WHICH]-(b:Booking {confirmed: false})-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $key})
        DETACH DELETE b;
    END_OF_QUERY
    # purge unconfirmed tablet bookings for any lesson key older than 30 minutes
    $neo4j.neo4j_query(<<~END_OF_QUERY, :timestamp => (Time.now - 30 * 60).to_i)
        MATCH (t:Tablet)<-[:WHICH]-(b:Booking {confirmed: false})-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson)
        WHERE b.updated < $timestamp
        DETACH DELETE b;
    END_OF_QUERY
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key).map { |x| x['i'] }
        MATCH (i:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $key})
        RETURN i
        ORDER BY i.offset;
    END_OF_QUERY
    results = {}
    rows.each do |row|
        results[row[:offset]] ||= {}
        results[row[:offset]][:info] = row.reject do |k, v|
            [:offset, :updated].include?(k)
        end
    end
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key).map { |x| {:comment => x['c'], :user => x['u'], :text_comment_from => x['tcf.email'] } }
        MATCH (u:User)<-[:TO]-(c:TextComment)-[:BELONGS_TO]->(l:Lesson {key: $key})
        MATCH (c)-[:FROM]->(tcf:User)
        RETURN c, u, tcf.email
        ORDER BY c.offset;
    END_OF_QUERY
    rows.each do |row|
        results[row[:comment][:offset]] ||= {}
        results[row[:comment][:offset]][:comments] ||= {}
        results[row[:comment][:offset]][:comments][row[:user][:email]] ||= {}
        if row[:comment][:comment]
            results[row[:comment][:offset]][:comments][row[:user][:email]][:text_comment] = row[:comment][:comment]
            results[row[:comment][:offset]][:comments][row[:user][:email]][:text_comment_from] = row[:text_comment_from]
        end
    end
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key).map { |x| {:comment => x['c'], :user => x['u'], :audio_comment_from => x['acf.email'] } }
        MATCH (u:User)<-[:TO]-(c:AudioComment)-[:BELONGS_TO]->(l:Lesson {key: $key})
        MATCH (c)-[:FROM]->(acf:User)
        RETURN c, u, acf.email
        ORDER BY c.offset;
    END_OF_QUERY
    rows.each do |row|
        results[row[:comment][:offset]] ||= {}
        results[row[:comment][:offset]][:comments] ||= {}
        results[row[:comment][:offset]][:comments][row[:user][:email]] ||= {}
        if row[:comment][:tag]
            results[row[:comment][:offset]][:comments][row[:user][:email]][:audio_comment_tag] = row[:comment][:tag]
            results[row[:comment][:offset]][:comments][row[:user][:email]][:duration] = row[:comment][:duration]
            results[row[:comment][:offset]][:comments][row[:user][:email]][:audio_comment_from] = row[:audio_comment_from]
        end
    end
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key).map { |x| {:offset => x['li.offset'], :feedback => x['hf'], :user => x['u.email'] }}
        MATCH (u:User)<-[:FROM]-(hf:HomeworkFeedback)-[:FOR]->(li:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $key})
        RETURN hf, li.offset, u.email;
    END_OF_QUERY
    rows.each do |row|
        results[row[:offset]] ||= {}
        results[row[:offset]][:feedback] ||= {}
        results[row[:offset]][:feedback][:sus] ||= {}
        results[row[:offset]][:feedback][:sus][row[:user]] = row[:feedback].reject do |k, v|
            [:done].include?(k)
        end
        results[row[:offset]][:feedback][:sus][row[:user]][:name] = (@@user_info[row[:user]] || {})[:display_name]
    end
    results.each_pair do |offset, info|
        next unless info[:feedback]
        results[offset][:feedback][:summary] = 'Es liegt bisher kein Feedback zu dieser Stunde vor.'
        feedback_str = StringIO.open do |io|
            state_histogram = {}
            time_spent_values = []
            info[:feedback][:sus].each_pair do |email, feedback|
                if feedback[:state]
                    state_histogram[feedback[:state]] ||= 0
                    state_histogram[feedback[:state]] += 1
                end
                if feedback[:time_spent]
                    time_spent_values << feedback[:time_spent]
                end
            end
            unless state_histogram.empty?
                io.puts "<p>"
                parts = []
                HOMEWORK_FEEDBACK_STATES.each do |x|
                    parts << "#{HOMEWORK_FEEDBACK_EMOJIS[x]} × #{state_histogram[x]}" if state_histogram[x]
                end
                io.puts parts.join(', ')
                io.puts "</p>"
            end
            time_spent_values.sort!
            unless time_spent_values.empty?
                io.puts "<p>"
                io.puts "SuS haben zwischen #{time_spent_values.first} und #{time_spent_values.last} Minuten für diese Hausaufgabe benötigt (#{time_spent_values.size} Angabe#{time_spent_values.size == 1 ? '' : 'n'})."
                io.puts "</p>"
            end
            io.string
        end
        results[offset][:feedback][:summary] = feedback_str
    end

    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key)
        MATCH (t:Tablet)<-[:WHICH]-(b:Booking {confirmed: true})-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $key})
        RETURN t.id, i.offset;
    END_OF_QUERY
    rows.each do |entry|
        offset = entry['i.offset']
        tablet_id = entry['t.id']
        results[offset] ||= {}
        results[offset][:info] ||= {}
        results[offset][:info][:booked_tablet] = {
            :tablet_id => tablet_id,
            :lagerort => @@tablets[tablet_id][:lagerort],
            :bg_color => @@tablets[tablet_id][:bg_color],
            :fg_color => @@tablets[tablet_id][:fg_color]
        }
    end

    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, :key => lesson_key)
        MATCH (t:TabletSet)<-[:BOOKED]-(b:Booking)-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson {key: $key})
        RETURN t.id, i.offset
        ORDER BY t.id;
    END_OF_QUERY
    rows.each do |entry|
        offset = entry['i.offset']
        tablet_set_id = entry['t.id']
        results[offset] ||= {}
        results[offset][:info] ||= {}
        results[offset][:info][:booked_tablet_sets] ||= []
        results[offset][:info][:booked_tablet_sets] << tablet_set_id
        results[offset][:info][:booked_tablet_sets_tablet_count] ||= 0
        results[offset][:info][:booked_tablet_sets_tablet_count] += @@tablet_sets[tablet_set_id][:count]
    end

    results
end

.get_regular_testing_daysObject



1012
1013
1014
1015
1016
1017
1018
1019
1020
# File 'src/ruby/include/salzh.rb', line 1012

def self.get_regular_testing_days()
    pattern = $neo4j.neo4j_query_expect_one(<<~END_OF_QUERY)['pattern']
        MERGE (n:RegularTestingDays)
        RETURN COALESCE(n.pattern, 0) AS pattern;
    END_OF_QUERY
    return (0..4).map do |i|
        ((pattern >> i) & 1) == 0
    end
end

.get_salzh_status_for_emails(emails = nil, force_refresh = true) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'src/ruby/include/salzh.rb', line 32

def self.get_salzh_status_for_emails(emails = nil, force_refresh = true) 

    Main.purge_stale_salzh_entries(false)

    temp = []
    temp2 = []

    hotspot_dates = {}
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (k:Klasse)
        RETURN k.klasse, k.hotspot_end_date;
    END_OF_QUERY
    rows.each do |x|
        hotspot_dates[x['k.klasse']] = x['k.hotspot_end_date']
    end

    if emails.nil?
        temp = $neo4j.neo4j_query(<<~END_OF_QUERY)
            MATCH (s:Salzh)-[:BELONGS_TO]->(u:User)
            RETURN COALESCE(s.mode, 'salzh') AS smode, s.end_date, u.email;
        END_OF_QUERY
        temp2 = $neo4j.neo4j_query(<<~END_OF_QUERY)
            MATCH (u:User)
            RETURN u.email, u.freiwillig_salzh, COALESCE(u.testing_required, TRUE) AS testing_required,
            COALESCE(u.voluntary_testing, FALSE) AS voluntary_testing;
        END_OF_QUERY
    else
        emails = [emails] unless emails.is_a? Array
        temp = $neo4j.neo4j_query(<<~END_OF_QUERY, {:emails => emails})
            MATCH (s:Salzh)-[:BELONGS_TO]->(u:User)
            WHERE u.email IN $emails
            RETURN COALESCE(s.mode, 'salzh') AS smode, s.end_date, u.email;
        END_OF_QUERY
        temp2 = $neo4j.neo4j_query(<<~END_OF_QUERY, {:emails => emails})
            MATCH (u:User)
            WHERE u.email IN $emails
            RETURN u.email, u.freiwillig_salzh, COALESCE(u.testing_required, TRUE) AS testing_required,
            COALESCE(u.voluntary_testing, FALSE) AS voluntary_testing;
        END_OF_QUERY
    end

    result = {}
    temp2.each do |row|
        email = row['u.email']
        result[email] = {
            :freiwillig_salzh => row['u.freiwillig_salzh'], # end_date or nil
            :testing_required => row['testing_required'],
            :voluntary_testing => row['voluntary_testing']
        }
    end
    temp.each do |row|
        result[row['u.email']][:salzh] = row['smode']
        result[row['u.email']][:salzh_end_date] = row['s.end_date']
    end
    result.each_pair do |email, info|
        status = nil
        status_end_date = nil
        if info[:freiwillig_salzh]
            status = :salzh
            status_end_date = info[:freiwillig_salzh]
            if info[:salzh] == 'salzh'
                status_end_date = [status_end_date, info[:salzh_end_date]].max
            end
        else
            if info[:salzh] == 'salzh'
                status = :salzh
                status_end_date = info[:salzh_end_date]
            elsif info[:salzh] == 'contact_person'
                status = :contact_person
                status_end_date = info[:salzh_end_date]
            end
        end
        if status.nil?
            # see if it's a hotspot klasse
            if @@user_info[email]
                klasse = @@user_info[email][:klasse]
                if hotspot_dates[klasse]
                    status = :hotspot_klasse
                    status_end_date = hotspot_dates[klasse]
                end
            end
        end
        info[:status] = status
        info[:status_end_date] = status_end_date
        wday = DateTime.now.wday
        needs_testing_today = true
        unless info[:testing_required]
            needs_testing_today = false
        end
        info[:needs_testing_today] = needs_testing_today

    end
    result
end

.get_stream_restriction_for_lesson_key(lesson_key) ⇒ Object



463
464
465
466
467
468
469
470
471
472
# File 'src/ruby/include/lesson.rb', line 463

def self.get_stream_restriction_for_lesson_key(lesson_key)
    results = $neo4j.neo4j_query_expect_one(<<~END_OF_QUERY, :key => lesson_key)['restriction']
        MERGE (l:Lesson {key: $key})
        RETURN COALESCE(l.stream_restriction, []) AS restriction
    END_OF_QUERY
    while results.size < 5
        results << 0
    end
    results
end

.get_switch_week_for_date(d) ⇒ Object



296
297
298
299
300
301
302
303
304
305
# File 'src/ruby/include/directory.rb', line 296

def self.get_switch_week_for_date(d)
    ds = d.strftime('%Y-%m-%d')
    d_info = SWITCH_WEEKS.keys.sort.select do |d|
        ds >= d
    end.last
    info = SWITCH_WEEKS[d_info]
    return nil if info.nil?
    week_number = (d.strftime('%-V').to_i - Date.parse(d_info).strftime('%-V').to_i)
    return (((week_number + (info[0].ord - 'A'.ord)) % info[1]) + 'A'.ord).chr
end

.get_test_eventsObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
# File 'src/ruby/include/test_events.rb', line 2

def self.get_test_events
    ts_now = DateTime.now.strftime('%Y-%m-%d')
    # $neo4j.neo4j_query(<<~END_OF_QUERY, :today => ts_now).map { |x| x['e'] }
    #     MATCH (e:TestEvent)
    #     WHERE e.date < $today
    #     DELETE e;
    # END_OF_QUERY
    results = $neo4j.neo4j_query(<<~END_OF_QUERY, :today => ts_now).map { |x| x['e'] }
        MATCH (e:TestEvent)
        RETURN e
        ORDER BY e.date, e.title;
    END_OF_QUERY
    results
end

.get_test_list_label_type(status, regular_test_required, regular_test_day) ⇒ Object



804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'src/ruby/include/salzh.rb', line 804

def self.get_test_list_label_type(status, regular_test_required, regular_test_day)
    today = DateTime.now.strftime('%Y-%m-%d')
    if status == :salzh
        return :strike
    end
    if status == :hotspot_klasse
        return :enabled
    end
    if status == :contact_person
        return :enabled
    end
    if regular_test_required == false
        return :enabled if today >= '2022-04-01'
        return :disabled
    end
    if regular_test_required && !regular_test_day && status == nil
        return :enabled if today >= '2022-04-01'
        return :disabled
    end
    return :enabled
end

.get_voluntary_testing_susObject



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'src/ruby/include/salzh.rb', line 303

def self.get_voluntary_testing_sus
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY).select do |x|
        MATCH (u:User {voluntary_testing: true})
        RETURN u.email, u.last_skipped_voluntary_testing;
    END_OF_QUERY
        @@user_info[x['u.email']]
    end.map do |x|
        d = @@user_info[x['u.email']]
        d[:last_skipped_voluntary_testing] = x['u.last_skipped_voluntary_testing']
        d
    end

    rows.sort! do |a, b|
        if a[:klasse] == b[:klasse]
            if a[:last_name] == b[:last_name]
                a[:first_name] <=> b[:first_name]
            else
                a[:last_name] <=> b[:last_name]
            end
        else
            (KLASSEN_ORDER.index(a[:klasse]) <=> KLASSEN_ORDER.index(b[:klasse]))
        end
    end
    today_s = DateTime.now.strftime('%Y-%m-%d')
    rows.map do |info|
        {
            :email => info[:email],
            :first_name => info[:first_name],
            :last_name => info[:last_name],
            :display_name => info[:display_name],
            :skipped_today => info[:last_skipped_voluntary_testing] == today_s,
            :klasse => tr_klasse(info[:klasse])
        }
    end
end

.get_website_eventsObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'src/ruby/include/website.rb', line 11

def self.get_website_events
    ts_now = DateTime.now.strftime('%Y-%m-%d')
    $neo4j.neo4j_query(<<~END_OF_QUERY, :today => ts_now).map { |x| x['e'] }
        MATCH (e:WebsiteEvent)
        WHERE e.date_end IS NULL AND e.date < $today
        DELETE e;
    END_OF_QUERY
    $neo4j.neo4j_query(<<~END_OF_QUERY, :today => ts_now).map { |x| x['e'] }
        MATCH (e:WebsiteEvent)
        WHERE e.date_end IS NOT NULL AND e.date_end < $today
        DELETE e;
    END_OF_QUERY
    results = $neo4j.neo4j_query(<<~END_OF_QUERY, :today => ts_now).map { |x| x['e'] }
        MATCH (e:WebsiteEvent)
        RETURN e
        ORDER BY e.date, e.title;
    END_OF_QUERY
    results
end

.invite_external_user_for_event(eid, email, session_user_email) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'src/ruby/include/event.rb', line 168

def self.invite_external_user_for_event(eid, email, session_user_email)
    STDERR.puts "Sending invitation mail for event #{eid} to #{email}"
    timestamp = Time.now.to_i
    data = {}
    data[:eid] = eid
    data[:email] = email
    data[:timestamp] = timestamp
    event = nil
    # TODO: There is a potential bug here when someone adds a PredefinedExternalUser
    # as a recipient and there will be two results here where we would have expected
    # one before
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY, data).first
        MATCH (u:User)<-[:ORGANIZED_BY]-(e:Event {id: $eid})<-[rt:IS_PARTICIPANT]-(r)
        WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(e.deleted, false) = false
        RETURN e, u.email;
    END_OF_QUERY
    event = temp['e']
    session_user = @@user_info[temp['u.email']][:display_last_name]
    code = Digest::SHA2.hexdigest(EXTERNAL_USER_EVENT_SCRAMBLER + data[:eid] + data[:email]).to_i(16).to_s(36)[0, 8]
    # remove invitation request / if something goes wrong, we won't keep sending out invites blocking the queue
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, data)
        MATCH (e:Event {id: $eid})<-[rt:IS_PARTICIPANT]-(r)
        WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(e.deleted, false) = false
        REMOVE rt.invitation_requested;
    END_OF_QUERY
    begin
        deliver_mail do
            to data[:email]
            bcc SMTP_FROM
            from SMTP_FROM
            reply_to "#{@@user_info[session_user_email][:display_name]} <#{session_user_email}>"
            
            subject "Einladung: #{event[:title]}"

            StringIO.open do |io|
                io.puts "<p>Sie haben eine Einladung zu einem Termin via Jitsi Meet erhalten.</p>"
                io.puts "<p>"
                io.puts "Eingeladen von: #{session_user}<br />"
                io.puts "Titel: #{event[:title]}<br />"
                io.puts "Datum und Uhrzeit: #{WEEKDAYS[Time.parse(event[:date]).wday]}., den #{Time.parse(event[:date]).strftime('%d.%m.%Y')}, #{event[:start_time]} &ndash; #{event[:end_time]}<br />"
                link = WEB_ROOT + "/e/#{data[:eid]}/#{code}"
                io.puts "</p>"
                io.puts "<p>Link zum Termin:<br /><a href='#{link}'>#{link}</a></p>"
                io.puts "<p>Bitte geben Sie den Link nicht weiter. Er ist personalisiert und enthält Ihren Namen, den Raumnamen und ist nur am Tag des Termins gültig.</p>"
                io.puts event[:description]
                io.string
            end
        end
    rescue
        deliver_mail do
            to session_user_email
            bcc SMTP_FROM
            from SMTP_FROM
            
            subject "Einladung konnte nicht versendet werden: #{event[:title]}"

            StringIO.open do |io|
                io.puts "<p>Die Einladung für den folgenden Termin konnte nicht versendet werden:</p>"
                io.puts "<p>"
                io.puts "E-Mail: #{data[:email]}<br />"
                io.puts "Titel: #{event[:title]}<br />"
                io.puts "Datum und Uhrzeit: #{WEEKDAYS[Time.parse(event[:date]).wday]}., den #{Time.parse(event[:date]).strftime('%d.%m.%Y')}, #{event[:start_time]} &ndash; #{event[:end_time]}<br />"
                link = WEB_ROOT + "/e/#{data[:eid]}/#{code}"
                io.puts "</p>"
                io.puts "<p>Link zum Termin:<br /><a href='#{link}'>#{link}</a></p>"
                io.puts "<p>Bitte überprüfen Sie die E-Mail-Adresse, sie ist vermutlich falsch.</p>"
                io.string
            end
        end
    end
    # add timestamp to list of successfully sent invitations
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, data)
        MATCH (e:Event {id: $eid})<-[rt:IS_PARTICIPANT]-(r)
        WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(e.deleted, false) = false
        SET rt.invitations = COALESCE(rt.invitations, []) + [$timestamp];
    END_OF_QUERY
end

.invite_external_user_for_poll_run(prid, email, session_user_email) ⇒ Object



789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
# File 'src/ruby/include/poll.rb', line 789

def self.invite_external_user_for_poll_run(prid, email, session_user_email)
    STDERR.puts "Sending invitation mail for poll run #{prid} to #{email}"
    timestamp = Time.now.to_i
    data = {}
    data[:prid] = prid
    data[:email] = email
    data[:timestamp] = timestamp
    poll_run = nil
    results = $neo4j.neo4j_query(<<~END_OF_QUERY, data)
        MATCH (u:User)<-[:ORGANIZED_BY]-(p:Poll)<-[:RUNS]-(pr:PollRun {id: $prid})<-[rt:IS_PARTICIPANT]-(r)
        WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(pr.deleted, false) = false AND COALESCE(p.deleted, false) = false
        RETURN pr, p, u.email;
    END_OF_QUERY
    if results.empty?
        raise 'no results found, expected at least one!'
    end
    temp = results.first
    poll_run = temp['pr']
    poll = temp['p']
    session_user = @@user_info[temp['u.email']][:display_last_name]
    code = Digest::SHA2.hexdigest(EXTERNAL_USER_EVENT_SCRAMBLER + data[:prid] + data[:email]).to_i(16).to_s(36)[0, 8]
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY, data)
        MATCH (pr:PollRun {id: $prid})<-[rt:IS_PARTICIPANT]-(r)
        WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(pr.deleted, false) = false
        REMOVE rt.invitation_requested
    END_OF_QUERY
    begin
        deliver_mail do
            to data[:email]
            bcc SMTP_FROM
            from SMTP_FROM
            reply_to "#{@@user_info[session_user_email][:display_name]} <#{session_user_email}>"

            subject "Einladung zur Umfrage: #{poll[:title]}"

            StringIO.open do |io|
                io.puts "<p>Sie haben eine Einladung zu einer Umfrage erhalten.</p>"
                io.puts "<p>"
                io.puts "Eingeladen von: #{session_user}<br />"
                io.puts "Titel: #{poll[:title]}<br />"
                io.puts "Datum und Uhrzeit: #{WEEKDAYS[Time.parse(poll_run[:start_date]).wday]}., den #{Time.parse(poll_run[:start_date]).strftime('%d.%m.%Y')}, #{poll_run[:start_time]} &ndash; #{WEEKDAYS[Time.parse(poll_run[:end_date]).wday]}., den #{Time.parse(poll_run[:end_date]).strftime('%d.%m.%Y')}, #{poll_run[:end_time]}<br />"
                link = WEB_ROOT + "/p/#{data[:prid]}/#{code}"
                io.puts "</p>"
                io.puts "<p>Link zur Umfrage:<br /><a href='#{link}'>#{link}</a></p>"
                io.puts "<p>Bitte geben Sie den Link nicht weiter. Er ist personalisiert und nur im angegebenen Zeitraum gültig.</p>"
                io.string
            end
        end
        rows = $neo4j.neo4j_query(<<~END_OF_QUERY, data)
            MATCH (pr:PollRun {id: $prid})<-[rt:IS_PARTICIPANT]-(r)
            WHERE (r:ExternalUser OR r:PredefinedExternalUser) AND (r.email = $email) AND COALESCE(rt.deleted, false) = false AND COALESCE(pr.deleted, false) = false
            SET rt.invitations = COALESCE(rt.invitations, []) + [$timestamp];
        END_OF_QUERY
    rescue
        deliver_mail do
            to session_user_email
            bcc SMTP_FROM
            from SMTP_FROM

            subject "Einladung zur Umfrage konnte nicht versendet werden: #{poll[:title]}"

            StringIO.open do |io|
                io.puts "<p>Die Einladung für die folgende Umfrage konnte nicht versendet werden:</p>"
                io.puts "<p>"
                io.puts "E-Mail: #{data[:email]}<br />"
                io.puts "Titel: #{poll[:title]}<br />"
                io.puts "Datum und Uhrzeit: #{WEEKDAYS[Time.parse(poll_run[:start_date]).wday]}., den #{Time.parse(poll_run[:start_date]).strftime('%d.%m.%Y')}, #{poll_run[:start_time]} &ndash; #{WEEKDAYS[Time.parse(poll_run[:end_date]).wday]}., den #{Time.parse(poll_run[:end_date]).strftime('%d.%m.%Y')}, #{poll_run[:end_time]}<br />"
                link = WEB_ROOT + "/p/#{data[:prid]}/#{code}"
                io.puts "</p>"
                io.puts "<p>Link zur Umfrage:<br /><a href='#{link}'>#{link}</a></p>"
                io.puts "<p>Bitte überprüfen Sie die E-Mail-Adresse, sie ist vermutlich falsch.</p>"
                io.string
            end
        end
    end
end

.iterate_school_days(options = {}, &block) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
# File 'src/ruby/main.rb', line 422

def self.iterate_school_days(options = {}, &block)
    day = Date.parse(@@config[:first_day])
    last_day = Date.parse(@@config[:last_day])
    while day <= last_day do
        ds = day.to_s
        off_day = @@off_days.include?(ds)
        unless off_day
            yield ds, (day.wday + 6) % 7
        end
        day += 1
    end
end

.log_freiwillig_salzh_sus_for_todayObject



989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'src/ruby/include/salzh.rb', line 989

def self.log_freiwillig_salzh_sus_for_today()
    wall_time = Time.now.strftime('%H:%M')
    return if wall_time < '12:00'
    today = Date.today.strftime('%Y-%m-%d')
    path = "/internal/salzh_protocol/#{today}.txt"
    return if File.exist?(path)

    unless File.exist?(File.dirname(path))
        FileUtils.mkpath(File.dirname(path))
    end

    Main.purge_stale_salzh_entries(true)
    rows = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| x['u.email'] }
        MATCH (u:User)
        WHERE EXISTS(u.freiwillig_salzh)
        RETURN u.email;
    END_OF_QUERY
    File.open(path, 'w') do |f|
        f.puts "# watch out, only count days which were actual school days! we need to filter this..."
        f.puts rows.join("\n")
    end
end

.parse_zeugnisformulareObject



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'src/ruby/include/zeugnisse.rb', line 13

def self.parse_zeugnisformulare
    FileUtils.mkpath('/internal/lowriter_home')
    @@zeugnisse = {}
    @@zeugnisse[:formulare] ||= {}
    debug "Parsing Zeugnisformulare..."
    Dir["/data/zeugnisse/formulare/#{ZEUGNIS_SCHULJAHR}/#{ZEUGNIS_HALBJAHR}/*.docx"].each do |path|
        sha1 = Digest::SHA1.hexdigest(File.read(path))
        out_path = File.join("/internal/zeugnisse/formulare/#{sha1}")
        unless File.exist?(out_path)
            FileUtils.mkpath(File.dirname(out_path))
            system("unzip -d \"#{out_path}\" \"#{path}\"")
        end
        doc = File.read(File.join(out_path, 'word', 'document.xml'))
        tags = doc.scan(/[#\$][A-Za-z0-9_]+\./)
        key = File.basename(path).sub('.docx', '')
        debug "#{key} (#{out_path}): #{tags.to_json}"
        @@zeugnisse[:formulare][key] ||= {}
        @@zeugnisse[:formulare][key][:sha1] = sha1
        @@zeugnisse[:formulare][key][:tags] = tags.map { |x| x[0, x.size - 1] }
        @@zeugnisse[:formulare][key][:formular_fehler] = self.check_zeugnisformular(key)
        if ZEUGNIS_HALBJAHR == '2'
            unless doc.include?('<w:strike/></w:rPr><w:t>nicht</w:t>')
                @@zeugnisse[:formulare][key][:formular_fehler] ||= []
                @@zeugnisse[:formulare][key][:formular_fehler] << "fehlende Versetzungsmarkierung (<s>nicht</s>)"
            end
        end
    end

    self.determine_zeugnislisten()
end

.purge_stale_salzh_entries(force = true) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'src/ruby/include/salzh.rb', line 6

def self.purge_stale_salzh_entries(force = true)
    today = Date.today.strftime('%Y-%m-%d')
    unless force
        @@last_purge_stale_salzh_entries_date ||= ''
        if today <= @@last_purge_stale_salzh_entries_date
            return
        end
        @@last_purge_stale_salzh_entries_date = today
    end
    $neo4j.neo4j_query(<<~END_OF_QUERY, :today => today)
        MATCH (s:Salzh)-[:BELONGS_TO]->(u:User)
        WHERE s.end_date < $today
        DETACH DELETE s;
    END_OF_QUERY
    $neo4j.neo4j_query(<<~END_OF_QUERY, :today => today)
        MATCH (u:User)
        WHERE EXISTS(u.freiwillig_salzh) AND u.freiwillig_salzh < $today
        REMOVE u.freiwillig_salzh;
    END_OF_QUERY
    $neo4j.neo4j_query(<<~END_OF_QUERY, :today => today)
        MATCH (k:Klasse)
        WHERE k.hotspot_end_date < $today
        DETACH DELETE k;
    END_OF_QUERY
end

.refresh_bib_dataObject



1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
# File 'src/ruby/main.rb', line 1185

def self.refresh_bib_data()
    begin
        now = Time.now.to_i
        return if now - @@bib_summoned_books_last_ts < 60 * (DEVELOPMENT ? 1 : 60)
        @@bib_summoned_books_last_ts = now
        @@bib_summoned_books = {}
        debug "Refreshing bib data..."
        url = "#{BIB_HOST}/api/get_summoned_books"
        res = Curl.get(url) do |http|
            payload = {:exp => Time.now.to_i + 60, :email => 'timetable'}
            http.headers['X-JWT'] = JWT.encode(payload, JWT_APPKEY_BIB, "HS256")
        end
        raise 'oops' if res.response_code != 200
        @@bib_summoned_books = JSON.parse(res.body)

        @@bib_unconfirmed_books = {}
        url = "#{BIB_HOST}/api/get_unconfirmed_books"
        res = Curl.get(url) do |http|
            payload = {:exp => Time.now.to_i + 60, :email => 'timetable'}
            http.headers['X-JWT'] = JWT.encode(payload, JWT_APPKEY_BIB, "HS256")
        end
        raise 'oops' if res.response_code != 200
        @@bib_unconfirmed_books = JSON.parse(res.body)
        # debug @@bib_summoned_books.to_yaml
    rescue StandardError => e
        debug e
    end
end

.refresh_public_event_configObject



354
355
356
357
358
359
360
361
362
363
# File 'src/ruby/include/public_event.rb', line 354

def self.refresh_public_event_config()
    @@public_event_config_timestamp ||= 0
    path = '/data/public_events/public_events.yaml'
    if DEVELOPMENT || @@public_event_config_timestamp < File.mtime(path).to_i
        debug "Reloading public event config from #{path}!"
        @@public_event_config = YAML.load(File.read(path))
        self.fix_public_event_config()
        @@public_event_config_timestamp = File.mtime(path).to_i
    end
end

.sms_gateway_ready?Boolean

Returns:

  • (Boolean)


25
26
27
# File 'src/ruby/include/sms.rb', line 25

def self.sms_gateway_ready?
    (@@ws_clients[:authenticated_sms] || {}).values.size > 0
end

.stream_allowed_for_date_lesson_key_and_email(datum, lesson_key, email, restrictions = nil, is_homeschooling_user = nil, group2_for_email = nil) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'src/ruby/include/jitsi.rb', line 86

def self.stream_allowed_for_date_lesson_key_and_email(datum, lesson_key, email, restrictions = nil, is_homeschooling_user = nil, group2_for_email = nil)
    # temporarily disable all stream restrictions
    return true
    
    restrictions ||= Main.get_stream_restriction_for_lesson_key(lesson_key)
    weekday = (Date.parse(datum).wday + 6) % 7
    return true if restrictions[weekday] == 0
    user = @@user_info[email]
    return true if user.nil?
    return true if user[:teacher]
    klassenstufe = user[:klasse].to_i
    return true unless WECHSELUNTERRICHT_KLASSENSTUFEN.include?(klassenstufe)
    if restrictions[weekday] == 1
        if is_homeschooling_user.nil?
            return get_homeschooling_for_user_by_dauer_salzh(email)
        else
            return is_homeschooling_user
        end
    elsif restrictions[weekday] == 2
        return get_homeschooling_for_user(email, datum, is_homeschooling_user, group2_for_email)
    else
        true
    end
end

.tr_klasse(klasse) ⇒ Object



473
474
475
# File 'src/ruby/main.rb', line 473

def self.tr_klasse(klasse)
    KLASSEN_TR[klasse] || klasse
end

.update_antikenfahrt_groupsObject



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'src/ruby/include/directory.rb', line 346

def self.update_antikenfahrt_groups()
    results = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| {:email => x['email'], :group_af => x['group_af'] }}
        MATCH (u:User)
        RETURN u.email AS email, COALESCE(u.group_af, '') AS group_af;
    END_OF_QUERY
    groups = {}
     = @@user_info
    results.each do |row|
        next unless ['gr', 'it'].include?(row[:group_af])
         = [row[:email]]
        next unless 
        next unless [:teacher] == false
        next unless ['11', '12'].include?([:klasse])
        groups[[:klasse]] ||= {}
        groups[[:klasse]][row[:group_af]] ||= []
        groups[[:klasse]][row[:group_af]] << row[:email]
    end
    @@antikenfahrt_recipients = {
        :recipients => {},
        :groups => []
    }
    @@antikenfahrt_mailing_lists = {}
    ['11', '12'].each do |klasse|
        ['gr', 'it'].each do |group_af|
            next if ((groups[klasse] || {})[group_af] || []).empty?
            @@antikenfahrt_recipients[:groups] << "/af/#{klasse}/#{group_af}/sus"
            @@antikenfahrt_recipients[:recipients]["/af/#{klasse}/#{group_af}/sus"] = {
                :label => "Antikenfahrt #{GROUP_AF_ICONS[group_af]} – SuS #{klasse}",
                :entries => groups[klasse][group_af]
            }
            @@antikenfahrt_recipients[:groups] << "/af/#{klasse}/#{group_af}/eltern"
            @@antikenfahrt_recipients[:recipients]["/af/#{klasse}/#{group_af}/eltern"] = {
                :label => "Antikenfahrt #{GROUP_AF_ICONS[group_af]} – Eltern #{klasse} (extern)",
                :external => true,
                :entries => groups[klasse][group_af].map { |x| 'eltern.' + x }
            }
            @@antikenfahrt_mailing_lists["antikenfahrt.#{group_af}.#{klasse}@#{SCHUL_MAIL_DOMAIN}"] = {
                :label => "Antikenfahrt #{GROUP_AF_ICONS[group_af]} – SuS Klassenstufe #{klasse}",
                :recipients => groups[klasse][group_af]
            }
            @@antikenfahrt_mailing_lists["antikenfahrt.#{group_af}.eltern.#{klasse}@#{SCHUL_MAIL_DOMAIN}"] = {
                :label => "Antikenfahrt #{GROUP_AF_ICONS[group_af]} – Eltern Klassenstufe #{klasse}",
                :recipients => groups[klasse][group_af].map { |x| 'eltern.' + x }
            }
        end
    end
end

.update_mailing_listsObject



1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
# File 'src/ruby/main.rb', line 1076

def self.update_mailing_lists()
    self.update_antikenfahrt_groups()
    @@mailing_lists = {}
    all_kl = Set.new()
    @@klassen_order.each do |klasse|
        next unless @@schueler_for_klasse.include?(klasse)
        @@mailing_lists["klasse.#{klasse}@#{SCHUL_MAIL_DOMAIN}"] = {
            :label => "SuS der Klasse #{tr_klasse(klasse)}",
            :recipients => @@schueler_for_klasse[klasse]
        }
        @@mailing_lists["eltern.#{klasse}@#{SCHUL_MAIL_DOMAIN}"] = {
            :label => "Eltern der Klasse #{tr_klasse(klasse)}",
            :recipients => @@schueler_for_klasse[klasse].map do |email|
                "eltern.#{email}"
            end
        }
        @@mailing_lists["lehrer.#{klasse}@#{SCHUL_MAIL_DOMAIN}"] = {
            :label => "Lehrer der Klasse #{tr_klasse(klasse)}",
            :recipients => ((@@teachers_for_klasse[klasse] || {}).keys.sort).map do |shorthand|
                email = @@shorthands[shorthand]
            end.reject do |email|
                email.nil?
            end
        }
        if klasse.to_i > 0
            if @@klassenleiter[klasse]
                @@mailing_lists["team.#{klasse.to_i}@#{SCHUL_MAIL_DOMAIN}"] ||= {
                    :label => "Klassenleiterteam der Klassenstufe #{klasse.to_i}",
                    :recipients => []
                }
                @@klassenleiter[klasse].each do |shorthand|
                    if @@shorthands[shorthand]
                        @@mailing_lists["team.#{klasse.to_i}@#{SCHUL_MAIL_DOMAIN}"][:recipients] << @@shorthands[shorthand]
                        all_kl << @@shorthands[shorthand]
                    end
                end
            end
        end
    end
    @@mailing_lists["lehrer@#{SCHUL_MAIL_DOMAIN}"] = {
        :label => "Gesamtes Kollegium",
        :recipients => @@user_info.keys.select do |email|
            @@user_info[email][:teacher] && @@user_info[email][:can_log_in] && email != 'vorstand.gev@gymnasiumsteglitz.de'
        end
    }
    @@mailing_lists["sus@#{SCHUL_MAIL_DOMAIN}"] = {
        :label => "Alle Schülerinnen und Schüler",
        :recipients => @@user_info.keys.select do |email|
            !@@user_info[email][:teacher]
        end
    }
    @@mailing_lists["eltern@#{SCHUL_MAIL_DOMAIN}"] = {
        :label => "Alle Eltern",
        :recipients => @@user_info.keys.select do |email|
            !@@user_info[email][:teacher]
        end.map do |email|
            "eltern.#{email}"
        end
    }
    temp = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| { :email => x['u.email'] } }
        MATCH (u:User {ev: true})
        RETURN u.email;
    END_OF_QUERY
    @@mailing_lists["ev@#{SCHUL_MAIL_DOMAIN}"] = {
        :label => "Alle Elternvertreter:innen",
        :recipients => temp.map { |x| 'eltern.' + x[:email] }
    }
    @@mailing_lists["kl@#{SCHUL_MAIL_DOMAIN}"] = {
        :label => "Alle Klassenleiter:innen",
        :recipients => all_kl.to_a.sort
    }
    @@antikenfahrt_mailing_lists.each_pair do |k, v|
        @@mailing_lists[k] = v
    end
    if DEVELOPMENT
        VERTEILER_TEST_EMAILS.each do |email|
            @@mailing_lists[email] = {
                :label => "Dev-Verteiler #{email}",
                :recipients => VERTEILER_DEVELOPMENT_EMAILS
            }
        end
    end
    if DASHBOARD_SERVICE == 'ruby'
        File.open('/internal/mailing_lists.yaml.tmp', 'w') do |f|
            f.puts @@mailing_lists.to_yaml
        end
        FileUtils::mv('/internal/mailing_lists.yaml.tmp', '/internal/mailing_lists.yaml', force: true)
    end
end

.zeugnis_faecher_for_emails(emails) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'src/ruby/include/zeugnisse.rb', line 50

def self.zeugnis_faecher_for_emails(emails)
    zeugnis_keys = {}
    emails.each do |email|
        zeugnis_keys[self.zeugnis_key_for_email(email)] = true
    end
    faecher = []
    faecher_wf = []
    zeugnis_keys.keys.sort.each do |key|
        FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR][key].each do |fach|
            if fach[0] == '$'
                faecher_wf << fach unless faecher_wf.include?(fach)
            else
                faecher << fach unless faecher.include?(fach)
            end
        end
    end
    return faecher + faecher_wf
end

.zeugnis_key_for_email(email) ⇒ Object



44
45
46
47
48
# File 'src/ruby/include/zeugnisse.rb', line 44

def self.zeugnis_key_for_email(email)
    sesb = @@user_info[email][:sesb] || false
    klassenstufe = @@user_info[email][:klassenstufe]
    return "#{klassenstufe}#{sesb ? '_sesb': ''}"
end

Instance Method Details

#_include_file(name, label) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'src/ruby/include/website.rb', line 133

def _include_file(name, label)
    icon = ''
    if name[-4, 4] == '.pdf'
        icon = "<i class='file-type fa fa-file-pdf-o'></i>"
    elsif name[-4, 4] == '.doc' || name[-5, 5] == '.docx'
        icon = "<i class='file-type fa fa-file-word-o'></i>"
    elsif name[-4, 4] == '.xls' || name[-5, 5] == '.xlsx'
        icon = "<i class='file-type fa fa-file-excel-o'></i>"
    elsif name[-4, 4] == '.ppt' || name[-5, 5] == '.pptx'
        icon = "<i class='file-type fa fa-file-powerpoint-o'></i>"
    elsif name[-4, 4] == '.zip'
        icon = "<i class='file-type fa fa-file-zip-o'></i>"
    end
    "#{icon}<a href='https://#{WEBSITE_HOST}/f/#{name}' target='_blank'>#{label}</a>"
end

#_include_image(slug, mode = nil, caption = nil) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'src/ruby/include/website.rb', line 203

def _include_image(slug, mode = nil, caption = nil)
    mode = nil if (mode || '').empty?
    caption = nil if (caption|| '').empty?
    if mode.nil?
        StringIO.open do |io|
            io.puts "<div class='image'>"
#                 io.puts "<img src='https://#{WEBSITE_HOST}/gen/i/#{slug}-1024.jpg' />"
            io.puts _include_lazyload_image(slug)
            io.puts "<div class='caption'>#{caption}</div>" if caption
            io.puts "</div>"
            io.string
        end
    else
        StringIO.open do |io|
            width = mode[1]
            align = mode[0] == 'r' ? 'right' : 'left'
            io.puts "<div class='image iw-#{width} pull-#{align}'>"
#                 io.puts "<img src='https://#{WEBSITE_HOST}/gen/i/#{slug}-1024.jpg' />"
            io.puts _include_lazyload_image(slug, :cols => width.to_i)
            io.puts "<div class='caption'>#{caption}</div>" if caption
            io.puts "</div>"
            io.string
        end
#             "<img src='https://#{WEBSITE_HOST}/gen/i/#{slug}-1024.jpg' class='col-md-4 pull-left' />"
    end
end

#_include_lazyload_image(slug, options = {}) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'src/ruby/include/website.rb', line 180

def _include_lazyload_image(slug, options = {})
    dir = ''
    options[:x] ||= 50
    options[:y] ||= 50
    options[:classes] ||= []
    if options[:cols]
        options[:lg] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:lg].to_f * options[:cols] / 12 / 12 * 9).to_i}px"
        options[:md] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:md].to_f * options[:cols] / 12 / 12 * 9).to_i}px"
        options[:sm] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:sm].to_f * options[:cols] / 12 / 12 * 9).to_i}px"
        options[:xs] ||= "100vw"
    end
    StringIO.open do |io|
        io.puts "<picture>"
        ['webp', 'jpg'].each do |extension|
            mime_type = extension == 'webp' ? 'image/webp' : 'image/jpeg'
            io.puts "<source type='#{mime_type}' class='lazy' #{img_multi_attr_lazy(File.join("https://#{WEBSITE_HOST}/gen/i/#{slug}"), extension, options)} />"
        end
        io.puts "<img src='#{slug}-p.jpg' class='#{options[:classes].join(' ')}' style='object-position: #{options[:x]}% #{options[:y]}%' alt='#{options[:label]}' />"
        io.puts "</picture>"
        io.string
    end
end

#admin_2fa_hotline_logged_in?Boolean

Returns:

  • (Boolean)


76
77
78
# File 'src/ruby/include/user.rb', line 76

def admin_2fa_hotline_logged_in?
    admin_logged_in? && DATENTRESOR_HOTLINE_USERS.include?(@session_user[:email])
end

#admin_logged_in?Boolean

Returns true if an admin is logged in.

Returns:

  • (Boolean)


68
69
70
# File 'src/ruby/include/user.rb', line 68

def admin_logged_in?
    user_logged_in? && ADMIN_USERS.include?(@session_user[:email])
end

#advent_calendar_imagesObject



2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
# File 'src/ruby/main.rb', line 2289

def advent_calendar_images
    return [] unless File.exist?('advent-calendar-images.txt')
    File.read('advent-calendar-images.txt').split(/\s+/).map { |x| x.strip }.reject { |x| x.empty? }.map do |x|
        unless File.exist?("/gen/ac-#{x}.png")
            system("wget -O /gen/ac-#{x}-dl.png https://pixel.hackschule.de/raw/uploads/#{x}.png")
            system("convert /gen/ac-#{x}-dl.png -scale 1600% /gen/ac-#{x}.png")
        end
        "/gen/ac-#{x}.png"
    end
end

#advents_calendar_date_todayObject

returns 0 if before Dec 1 returns 1 .. 24 if in range returns 24 if Dec 25 .. 31



1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
# File 'src/ruby/main.rb', line 1656

def advents_calendar_date_today()
    date = DateTime.now.to_s[0, 10]
    # if DEVELOPMENT
        # date = '2021-12-24'
    # end
    return 0 if date < '2021-12-01'
    return 0 if date > '2021-12-31'
    day = date[8, 2].to_i
    day = 24 if day > 24
    return day
end

#all_sessionsObject



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'src/ruby/include/login.rb', line 280

def all_sessions
    sids = request.cookies['sid']
    users = []
    if (sids.is_a? String) && (sids =~ /^[0-9A-Za-z,]+$/)
        sids.split(',').each do |sid|
            if sid =~ /^[0-9A-Za-z]+$/
                results = neo4j_query(<<~END_OF_QUERY, :sid => sid).map { |x| {:sid => x['sid'], :email => x['email'] } }
                    MATCH (s:Session {sid: $sid})-[:BELONGS_TO]->(u:User)
                    RETURN s.sid AS sid, u.email AS email;
                END_OF_QUERY
                results.each do |entry|
                    if entry[:email] && @@user_info[entry[:email]]
                        users << {:sid => entry[:sid], :user => @@user_info[entry[:email]].dup, :method => entry[:method]}
                    end
                end
            end
        end
    end
    users
end

#already_booked_tablet_sets_for_day(datum) ⇒ Object

return all booked tablet sets for a specific day



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'src/ruby/include/tablet_set.rb', line 68

def already_booked_tablet_sets_for_day(datum)
    require_user_who_can_manage_tablets_or_teacher!
    rows = neo4j_query(<<~END_OF_QUERY, { :datum => datum }).map { |x| {:tablet_set_id => x['t.id'], :lesson_key => x['l.key'], :start_time => x['b.start_time'], :end_time => x['b.end_time'], :email => x['u.email'] } }
        MATCH (t:TabletSet)<-[:BOOKED]-(b:Booking {datum: $datum})-[:BOOKED_BY]->(u:User)
        OPTIONAL MATCH (b)-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson)
        RETURN t.id, l.key, b.start_time, b.end_time, u.email;
    END_OF_QUERY
    result = {}
    rows.each do |row|
        result[row[:tablet_set_id]] ||= []
        result[row[:tablet_set_id]] << {
            :lesson_key => row[:lesson_key],
            :email => row[:email],
            :display_name => (@@user_info[row[:email]] || {})[:display_last_name_dativ] || 'NN',
            :start_time => row[:start_time],
            :end_time => row[:end_time]
        }
    end
    result
end

#already_booked_tablet_sets_for_timespan(datum, start_time, end_time) ⇒ Object

check whether we can book a list of tablet sets for a specific time span



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'src/ruby/include/tablet_set.rb', line 42

def already_booked_tablet_sets_for_timespan(datum, start_time, end_time)
    require_user_who_can_manage_tablets_or_teacher!
    data = {
        :datum => datum,
        :start_time => start_time,
        :end_time => end_time
    }
    rows = neo4j_query(<<~END_OF_QUERY, data).map { |x| {:tablet_set_id => x['t.id'], :lesson_key => x['l.key'], :email => x['u.email'] } }
        MATCH (t:TabletSet)<-[:BOOKED]-(b:Booking {datum: $datum})-[:BOOKED_BY]->(u:User)
        WHERE NOT ((b.end_time <= $start_time) OR (b.start_time >= $end_time))
        OPTIONAL MATCH (b)-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson)
        RETURN t.id, l.key, u.email;
    END_OF_QUERY
    result = {}
    rows.each do |row|
        result[row[:tablet_set_id]] ||= []
        result[row[:tablet_set_id]] << {
            :lesson_key => row[:lesson_key],
            :email => row[:email],
            :display_name => (@@user_info[row[:email]] || {})[:display_last_name_dativ] || 'NN'
        }
    end
    result
end

#assert(condition, message = 'assertion failed', suppress_backtrace = false, delay = nil) ⇒ Object



1338
1339
1340
1341
1342
1343
1344
1345
1346
# File 'src/ruby/main.rb', line 1338

def assert(condition, message = 'assertion failed', suppress_backtrace = false, delay = nil)
    unless condition
        debug_error message
        e = StandardError.new(message)
        e.set_backtrace([]) if suppress_backtrace
        sleep delay unless delay.nil?
        raise e
    end
end

#assert_with_delay(condition, message = 'assertion failed', suppress_backtrace = false) ⇒ Object



1348
1349
1350
# File 'src/ruby/main.rb', line 1348

def assert_with_delay(condition, message = 'assertion failed', suppress_backtrace = false)
    assert(condition, message, suppress_backtrace, DEVELOPMENT ? 0.0 : 3.0)
end

#book_tablet_set_for_lesson(datum, start_time, end_time, tablet_sets = [], lesson_key, offset) ⇒ Object

book a list of tablet sets for a specific lesson, or unbook all tablet sets



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'src/ruby/include/tablet_set.rb', line 90

def book_tablet_set_for_lesson(datum, start_time, end_time, tablet_sets = [], lesson_key, offset)
    require_user_who_can_manage_tablets_or_teacher!
    conflicting_tablets = []
    unless tablet_sets.empty?
        # check if it's bookable
        temp = already_booked_tablet_sets_for_timespan(datum, start_time, end_time)
        conflicting_tablets = temp.keys.reject do |x|
            temp[x].reject do |y|
                y[:lesson_key] == lesson_key
            end.empty?
        end
        conflicting_tablets.select! { |x| tablet_sets.include?(x) }
    end
    if conflicting_tablets.empty?
        transaction do
            # make sure tablet sets exist in database
            tablet_sets.each do |tablet_set_id|
                neo4j_query("MERGE (:TabletSet {id: '#{tablet_set_id}'})")
            end
            timestamp = Time.now.to_i
            data = {
                :email => @session_user[:email],
                :lesson_key => lesson_key,
                :offset => offset,
                :tablet_set_ids => tablet_sets,
                :timestamp => timestamp,
                :datum => datum,
                :start_time => start_time,
                :end_time => end_time
            }
            # create booking node
            neo4j_query(<<~END_OF_QUERY, data)
                MATCH (u:User {email: $email})
                MERGE (l:Lesson {key: $lesson_key})
                MERGE (i:LessonInfo {offset: $offset})-[:BELONGS_TO]->(l)
                MERGE (b:Booking)-[:FOR]->(i)
                MERGE (u)<-[:BOOKED_BY]-(b)
                SET i.updated = $timestamp
                SET b.updated = $timestamp
                SET b.datum = $datum
                SET b.start_time = $start_time
                SET b.end_time = $end_time

                WITH b
                MATCH (b)-[r:BOOKED]->(:TabletSet)
                DELETE r
            END_OF_QUERY
            # connect booked tablet sets to booking node
            neo4j_query(<<~END_OF_QUERY, data)
                MATCH (l:Lesson {key: $lesson_key})
                MATCH (i:LessonInfo {offset: $offset})-[:BELONGS_TO]->(l)
                MATCH (b:Booking)-[:FOR]->(i)
                MATCH (t:TabletSet)
                WHERE t.id IN $tablet_set_ids
                MERGE (b)-[:BOOKED]->(t)
            END_OF_QUERY
        end
    else
        debug "Cannot book tablet sets because of these:"
        debug conflicting_tablets.to_yaml
        raise :unable_to_book_tablet_sets
    end
end

#book_tablet_set_for_timespan(datum, start_time, end_time, tablet_sets) ⇒ Object

book a list of tablet sets for a specific lesson, or unbook all tablet sets



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'src/ruby/include/tablet_set.rb', line 155

def book_tablet_set_for_timespan(datum, start_time, end_time, tablet_sets)
    require_user_who_can_manage_tablets!
    conflicting_tablets = []
    unless tablet_sets.empty?
        # check if it's bookable
        temp = already_booked_tablet_sets_for_timespan(datum, start_time, end_time)
        conflicting_tablets = temp.keys
        conflicting_tablets.select! { |x| tablet_sets.include?(x) }
    end
    if conflicting_tablets.empty?
        transaction do
            # make sure tablet sets exist in database
            tablet_sets.each do |tablet_set_id|
                neo4j_query("MERGE (:TabletSet {id: '#{tablet_set_id}'})")
            end
            timestamp = Time.now.to_i
            data = {
                :email => @session_user[:email],
                :tablet_set_ids => tablet_sets,
                :timestamp => timestamp,
                :datum => datum,
                :start_time => start_time,
                :end_time => end_time
            }
            # create booking node
            neo4j_query(<<~END_OF_QUERY, data)
                MATCH (u:User {email: $email})
                CREATE (u)<-[:BOOKED_BY]-(b:Booking {datum: $datum, start_time: $start_time, end_time: $end_time})
                SET b.updated = $timestamp

                WITH b
                MATCH (t:TabletSet)
                WHERE t.id IN $tablet_set_ids
                CREATE (b)-[:BOOKED]->(t)
            END_OF_QUERY
        end
    else
        debug "Cannot book tablet sets because of these:"
        debug conflicting_tablets.to_yaml
        raise :unable_to_book_tablet_sets
    end
end

#bytes_to_str(ai_Size) ⇒ Object



1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
# File 'src/ruby/main.rb', line 1985

def bytes_to_str(ai_Size)
    if ai_Size < 1024
        return "#{ai_Size} B"
    elsif ai_Size < 1024 * 1024
        return "#{sprintf('%1.1f', ai_Size.to_f / 1024.0)} kB"
    elsif ai_Size < 1024 * 1024 * 1024
        return "#{sprintf('%1.1f', ai_Size.to_f / 1024.0 / 1024.0)} MB"
    elsif ai_Size < 1024 * 1024 * 1024 * 1024
        return "#{sprintf('%1.1f', ai_Size.to_f / 1024.0 / 1024.0 / 1024.0)} GB"
    end
    return "#{sprintf('%1.1f', ai_Size.to_f / 1024.0 / 1024.0 / 1024.0 / 1024.0)} TB"
end

#caesar(s, shift) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'src/ruby/include/cypher.rb', line 157

def caesar(s, shift)
    t = ''
    s.each_char do |c|
        code = c.upcase.ord
        if code >= 'A'.ord && code <= 'Z'.ord
            i = code - 'A'.ord
            i = (i + shift) % 26
            c = (i + 'A'.ord).chr
        end
        t += c
    end
    t
end

#can_checkout_books(email) ⇒ Object



42
43
44
# File 'src/ruby/include/lehrbuchverein.rb', line 42

def can_checkout_books(email)
    determine_lehrmittelverein_state_for_email(email) > 0
end

#can_manage_agr_app_logged_in?Boolean

Returns:

  • (Boolean)


165
166
167
# File 'src/ruby/include/user.rb', line 165

def can_manage_agr_app_logged_in?
    user_logged_in? && CAN_MANAGE_AGR_APP.include?(@session_user[:email])
end

#can_manage_bib_logged_in?Boolean

Returns:

  • (Boolean)


169
170
171
172
173
174
175
176
177
178
179
# File 'src/ruby/include/user.rb', line 169

def can_manage_bib_logged_in?
    flag = user_logged_in? && CAN_MANAGE_BIB.include?(@session_user[:email])
    if flag
        unless teacher_logged_in?
            unless device_logged_in?
                flag = false
            end
        end
    end
    flag
end

#can_manage_bib_members_logged_in?Boolean

Returns:

  • (Boolean)


189
190
191
# File 'src/ruby/include/user.rb', line 189

def can_manage_bib_members_logged_in?
    user_logged_in? && (CAN_MANAGE_BIB_MEMBERS.include?(@session_user[:email]))
end

#can_manage_bib_payment_logged_in?Boolean

Returns:

  • (Boolean)


193
194
195
# File 'src/ruby/include/user.rb', line 193

def can_manage_bib_payment_logged_in?
    user_logged_in? && (CAN_MANAGE_BIB_PAYMENT.include?(@session_user[:email]))
end

#can_manage_bib_special_access_logged_in?Boolean

Returns:

  • (Boolean)


181
182
183
# File 'src/ruby/include/user.rb', line 181

def can_manage_bib_special_access_logged_in?
    user_logged_in? && CAN_MANAGE_BIB_SPECIAL_ACCESS.include?(@session_user[:email])
end

#can_manage_salzh_logged_in?Boolean

Returns true if a user who can see all timetables is logged in.

Returns:

  • (Boolean)


86
87
88
# File 'src/ruby/include/user.rb', line 86

def can_manage_salzh_logged_in?
    user_logged_in? && (admin_logged_in? || @session_user[:can_manage_salzh])
end

#can_see_all_timetables_logged_in?Boolean

Returns true if a user who can see all timetables is logged in.

Returns:

  • (Boolean)


81
82
83
# File 'src/ruby/include/user.rb', line 81

def can_see_all_timetables_logged_in?
    user_logged_in? && (admin_logged_in? || @session_user[:can_see_all_timetables])
end

#check_has_technikamt(email) ⇒ Object



3
4
5
6
7
8
9
# File 'src/ruby/include/techpost.rb', line 3

def check_has_technikamt(email)
    results = neo4j_query(<<~END_OF_QUERY, :email => email)
        MATCH (u:User {email: $email})-[:HAS_AMT {amt: 'technikamt'}]->(v:Techpost)
        RETURN CASE WHEN EXISTS((u)-[:HAS_AMT {amt: 'technikamt'}]->(v)) THEN true ELSE false END AS hasRelation;
    END_OF_QUERY
    return results
end


1412
1413
1414
1415
1416
1417
1418
1419
# File 'src/ruby/main.rb', line 1412

def class_stream_link_for_session_user
    require_user!
    if PROVIDE_CLASS_STREAM && (!@session_user[:teacher]) && (!['11', '12'].include?(@session_user[:klasse]))
        "/jitsi/Klassenstreaming#{@session_user[:klasse]}"
    else
        nil
    end
end

#color_palette_for_color_scheme(color_scheme) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'src/ruby/include/color.rb', line 115

def color_palette_for_color_scheme(color_scheme)
    primary_color = '#' + color_scheme[7, 6]
    light = luminance(primary_color) > 160
    primary_color_much_lighter = rgb_to_hex(mix(hex_to_rgb(desaturate(primary_color)), hex_to_rgb('#ffffff'), 0.8))
    primary_color_darker = darken(primary_color, 0.8)
    primary_color_much_darker = rgb_to_hex(mix(hex_to_rgb(desaturate(primary_color)), hex_to_rgb('#000000'), 0.5))
    desaturated_color = darken(desaturate(primary_color), 0.9)
    if light
        desaturated_color = rgb_to_hex(mix(hex_to_rgb(desaturate(primary_color)), hex_to_rgb('#ffffff'), 0.1))
    end
    desaturated_color_darker = darken(desaturate(primary_color), 0.3)
    disabled_color = light ? rgb_to_hex(mix(hex_to_rgb(primary_color), [255, 255, 255], 0.5)) : rgb_to_hex(mix(hex_to_rgb(primary_color), [192, 192, 192], 0.5))
    darker_color = rgb_to_hex(mix(hex_to_rgb(primary_color), [0, 0, 0], 0.6))
    shifted_color = shift_hue(primary_color, 350)
    main_text_color = light ? rgb_to_hex(mix(hex_to_rgb(primary_color), [0, 0, 0], 0.7)) : rgb_to_hex(mix(hex_to_rgb(primary_color), [255, 255, 255], 0.8))
    contrast_color = rgb_to_hex(mix(hex_to_rgb(primary_color), color_scheme[0] == 'l' ? [0, 0, 0] : [255, 255, 255], 0.7))
    color_palette = {
        :is_light => light,
        :primary => primary_color,
        :primary_color_much_lighter => primary_color_much_lighter,
        :primary_color_darker => primary_color_darker,
        :primary_color_much_darker => primary_color_much_darker,
        :disabled => disabled_color,
        :darker => darker_color,
        :shifted => desaturated_color,
        :left => '#' + color_scheme[1, 6],
        :right => '#' + color_scheme[13, 6],
        :main_text => main_text_color,
        :contrast => contrast_color
    }
    color_palette
end

#create_device_token(device, expire_hours) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'src/ruby/include/login.rb', line 34

def create_device_token(device, expire_hours)
    token = RandomTag::generate(24)
    assert(token =~ /^[0-9A-Za-z]+$/)
    data = {:device => device,
            :token => token,
            :expires => (DateTime.now() + expire_hours / 24.0).to_s}

    neo4j_query_expect_one(<<~END_OF_QUERY, :data => data)
        CREATE (t:DeviceToken $data)
        RETURN t;
    END_OF_QUERY
    token
end

#create_session(email, expire_hours) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'src/ruby/include/login.rb', line 5

def create_session(email, expire_hours)
    sid = RandomTag::generate(24)
    assert(sid =~ /^[0-9A-Za-z]+$/)
    data = {:sid => sid,
            :expires => (DateTime.now() + expire_hours / 24.0).to_s}
    begin
        ua = USER_AGENT_PARSER.parse(request.env['HTTP_USER_AGENT'])
        usa = "#{ua.family} #{ua.version.segments.first} (#{ua.os.family}"
        usa += " / #{ua.device.family}" if ua.device.family.downcase != 'other'
        usa += ')'
        data[:user_agent] = usa
    rescue
    end

    all_sessions().each do |session|
        other_sid = session[:sid]
        result = neo4j_query(<<~END_OF_QUERY, :email => email, :other_sid => other_sid).map { |x| x['sid'] }
            MATCH (s:Session {sid: $other_sid})-[:BELONGS_TO]->(u:User {email: $email})
            DETACH DELETE s;
        END_OF_QUERY
    end
    neo4j_query_expect_one(<<~END_OF_QUERY, :email => email, :data => data)
        MATCH (u:User {email: $email})
        CREATE (s:Session $data)-[:BELONGS_TO]->(u)
        RETURN s;
    END_OF_QUERY
    sid
end

#css_for_font(font) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
# File 'src/ruby/include/theme.rb', line 2

def css_for_font(font)
    if font == 'Alegreya'
        {'font-family' => 'AlegreyaSans', 'letter-spacing' => 'unset'}
    elsif font == 'Billy'
        {'font-family' => 'Billy', 'letter-spacing' => 'unset'}
    elsif font == 'Riffic'
        {'font-family' => 'Riffic', 'letter-spacing' => '0.05em'}
    else
        {'font-family' => 'Roboto', 'letter-spacing' => 'unset'}
    end
end

#current_jitsi_roomsObject



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'src/ruby/include/jitsi.rb', line 428

def current_jitsi_rooms()
    @@current_jitsi_rooms ||= nil
    @@current_jitsi_rooms_timestamp ||= Time.now
    if @@current_jitsi_rooms.nil? || Time.now > @@current_jitsi_rooms_timestamp + 10
        @@current_jitsi_rooms_timestamp = Time.now
        begin
            debug "Refreshing Jitsi presence!"
            c = Curl::Easy.new(JITSI_ALL_ROOMS_URL)
            c.perform
            if c.status.to_i == 200
                @@current_jitsi_rooms = JSON.parse(c.body_str)['rooms']
            else
                @@current_jitsi_rooms = nil
            end
        rescue
            @@current_jitsi_rooms = nil
        end
    end
    return @@current_jitsi_rooms
end

#cypher_contentObject



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'src/ruby/include/cypher.rb', line 263

def cypher_content
    require_user!
    parts = request.env['REQUEST_PATH'].split('/')
    provided_password = (parts[2] || '').strip.downcase
    provided_password = nil if provided_password.empty?
    debug "provided: [#{provided_password}]"
    result = neo4j_query_expect_one(<<~END_OF_QUERY, {:email => @session_user[:email]})
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.cypher_level, 0) AS cypher_level,
        u.cypher_seed AS cypher_seed,
        COALESCE(u.failed_cypher_tries, 0) AS failed_cypher_tries,
        COALESCE(u.cypher_name, '') AS cypher_name;
    END_OF_QUERY
    @cypher_level = result['cypher_level']
    @cypher_seed = result['cypher_seed']
    @cypher_name = result['cypher_name'].strip
    failed_cypher_tries = result['failed_cypher_tries']
    @tries_left = 3 - failed_cypher_tries
    if @cypher_level == 7
        @tries_left = 50 - failed_cypher_tries
    end
    if @cypher_seed.nil? || (@cypher_level == 0 && provided_password.nil?)
        @cypher_seed = Time.now.to_i
        result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email], :cypher_seed => @cypher_seed})
            MATCH (u:User {email: $email})
            SET u.cypher_seed = $cypher_seed;
        END_OF_QUERY
    end

    get_next_cypher_password()

    STDERR.puts "CYPHER // #{@session_user[:email]} // level: #{@cypher_level}, next password: #{@cypher_next_password}#{@cypher_next_password.nil? ? '(nil)':''}"

    unless provided_password.nil? || @cypher_next_password.nil?
        result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email], :cypher_provided => provided_password })
            MATCH (u:User {email: $email})
            SET u.cypher_provided = $cypher_provided;
        END_OF_QUERY
        if provided_password.downcase == @cypher_next_password.downcase
            @cypher_level += 1
            result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email], :cypher_level => @cypher_level})
                MATCH (u:User {email: $email})
                SET u.cypher_level = $cypher_level,
                u.failed_cypher_tries = 0
                REMOVE u.cypher_provided;
            END_OF_QUERY
        else
            result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]})
                MATCH (u:User {email: $email})
                SET u.failed_cypher_tries = COALESCE(u.failed_cypher_tries, 0) + 1;
            END_OF_QUERY
            if @tries_left <= 1
                result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]})
                    MATCH (u:User {email: $email})
                    SET u.cypher_level = 0, u.failed_cypher_tries = 0, u.cypher_name = ''
                    REMOVE u.cypher_provided;
                END_OF_QUERY
            end
        end
    end
    unless provided_password.nil?
        debug "REDIRECTING!"
        redirect "#{WEB_ROOT}/cyph3r", 302
    end

    get_next_cypher_password()

    StringIO.open do |io|
        # io.puts "<p style='text-align: left; margin-top: 0;'><b>#{@session_user[:first_name]}</b> &lt;#{@session_user[:email]}&gt;</p>"
        if @cypher_level == MAX_CYPHER_LEVEL
            # io.puts "<p style='float: right; margin-top: 0;'><a href='/hackers'>=&gt; Hall of Fame</a></p>"
            io.puts File.read('/static/cypher/hall_of_fame.html')
            result = neo4j_query(<<~END_OF_QUERY)
                MATCH (u:User)
                WHERE COALESCE(u.cypher_level, 0) > 0
                RETURN u.email, u.cypher_level, COALESCE(u.cypher_name, '') AS cypher_name
            END_OF_QUERY
            io.puts "<p>"
            io.puts "Bisher #{result.size == 1 ? 'hat' : 'haben'} <b>#{result.size} Code Cracker:in#{result.size == 1 ? '' : 'nen'}</b> versucht, die Aufgaben zu lösen."
            histogram = {}
            names = []
            result.each do |row|
                histogram[row['u.cypher_level']] ||= []
                histogram[row['u.cypher_level']] << row['u.email']
                names << row['cypher_name'] unless row['cypher_name'].strip.empty?
            end
            names.sort!
            parts = []
            histogram.keys.sort.each do |level|
                l = 'in der <b>Hall of Fame</b>'
                if level + 1 <= MAX_CYPHER_LEVEL
                    l = "in <b>Level #{level + 1}</b>"
                end
                parts << "<b>#{histogram[level].size} Person#{histogram[level].size == 1 ? '' : 'en'}</b> #{l}"
            end
            io.puts "Davon befinde#{histogram[histogram.keys.sort.first].size == 1 ? 't' : 'n'} sich #{join_with_sep(parts, ', ', ' und ')}."
            io.puts "</p>"
            io.puts "<hr style='margin-bottom: 15px;'/>"
            # io.puts "<p>"
            # io.puts "Hier kannst du festlegen, ob und wie du in der Hall of Fame erscheinen möchtest:"
            # io.puts "</p>"
            # io.puts "<hr />"
            possible_names = []
            possible_names << @session_user[:first_name]
            unless @session_user[:teacher]
                possible_names << "#{@session_user[:first_name]} (#{@session_user[:klasse]})"
            end
            possible_names << @session_user[:display_name]
            unless @session_user[:teacher]
                possible_names << "#{@session_user[:display_name]} (#{@session_user[:klasse]})"
            else
                possible_names << "#{@session_user[:display_last_name]}"
            end

            io.puts "<p style='text-align: left; margin-top: 0;'>"
            io.puts "<span class='name-pref' data-name=''>[#{@cypher_name == '' ? 'x': ' '}] Ich möchte <b>nicht</b> aufgelistet werden.</span><br />"
            possible_names.each do |name|
                io.puts "<span class='name-pref' data-name=\"#{name}\">[#{@cypher_name == name ? 'x': ' '}] Ich möchte als <b>»#{name}«</b> erscheinen.</span><br />"
            end
            io.puts "<span class='cypher-reset text-danger'>[!] Ich möchte meinen <b>Fortschritt löschen</b> und von vorn beginnen.</span>"
            io.puts "<span class='text-danger' id='cypher_reset_confirm' style='display: none;'><br />&nbsp;&nbsp;&nbsp;&nbsp;Bist du sicher? <span id='cypher_reset_confirm_yes'><b>[Ja]</b></span> <span id='cypher_reset_confirm_no'><b>[Nein]</b></span></span>"
            io.puts "</p>"
            io.puts "<h2><b>Hall of Fame</b></h2>"
            names.each do |name|
                io.puts "<p class='name'>#{name}</p>"
            end
            io.puts File.read('/static/cypher/hall_of_fame_foot.html')
        else
            io.puts File.read('/static/cypher/title.html').gsub('#{next_cypher_level}', (@cypher_level + 1).to_s)
            if failed_cypher_tries == 0
                if @cypher_level > 0
                    io.puts "<p><b>Gut gemacht!</b></p>"
                    io.puts "<hr />"
                end
            else
                if @cypher_level > 0
                    io.puts "<p class='text-danger'><b>Achtung!</b> Deine letzte Antwort war leider falsch. Du hast noch <b>#{@tries_left} Versuch#{@tries_left == 1 ? '' : 'e'}</b>, bevor du wieder von vorn beginnen musst.</p>"
                    io.puts "<hr />"
                else
                    io.puts "<p class='text-danger'><b>Achtung!</b> Deine letzte Antwort war leider falsch.</p>"
                    io.puts "<hr />"
                end
            end
            path = "/static/cypher/level_#{@cypher_level + 1}.html"
            if File.exist?(path)
                io.puts File.read(path)
            end
            io.puts File.read('/static/cypher/form.html')
        end
        io.string
    end
end

#darken(c, f = 0.2) ⇒ Object



91
92
93
94
95
# File 'src/ruby/include/color.rb', line 91

def darken(c, f = 0.2)
    hsv = rgb_to_hsv(hex_to_rgb(c))
    hsv[2] *= f
    rgb_to_hex(hsv_to_rgb(hsv))
end

#delete_audio_comment(tag) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
# File 'src/ruby/include/comment.rb', line 2

def delete_audio_comment(tag)
    require_teacher!
    if tag && tag.class == String && tag =~ /^[0-9a-zA-Z]+$/
        dir = tag[0, 2]
        filename = tag[2, tag.size - 2]
        ['.ogg', '.mp3'].each do |ext|
            path = "/raw/uploads/audio_comment/#{dir}/#{filename}#{ext}"
            if File.exist?(path)
                STDERR.puts "DELETING #{path}"
                FileUtils::rm_f(path)
            end
        end
    end
end


2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'src/ruby/include/ical.rb', line 2

def delete_session_user_ical_link()
    require_user!
    result = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email]).map { |x| x['u.ical_token'] }
        MATCH (u:User {email: $email})
        WHERE EXISTS(u.ical_token)
        RETURN u.ical_token;
    END_OF_QUERY
    result.each do |token|
        path = "/gen/ical/#{token}.ics"
        STDERR.puts path
        if File.exist?(path)
            FileUtils.rm(path)
        end
    end
    result = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email])
        MATCH (u:User {email: $email})
        REMOVE u.ical_token;
    END_OF_QUERY
end

#delete_session_user_otp_tokenObject



2
3
4
5
6
7
8
9
10
# File 'src/ruby/include/otp.rb', line 2

def delete_session_user_otp_token()
    require_user!
    result = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email])
        MATCH (u:User {email: $email})
        REMOVE u.otp_token
        REMOVE u.otp_token_changed
        REMOVE u.preferred_login_method;
    END_OF_QUERY
end

#desaturate(c) ⇒ Object



78
79
80
81
82
83
# File 'src/ruby/include/color.rb', line 78

def desaturate(c)
    hsv = rgb_to_hsv(hex_to_rgb(c))
    hsv[1] *= 0.7
    hsv[2] *= 0.9
    rgb_to_hex(hsv_to_rgb(hsv))
end

#determine_lehrmittelverein_state_for_email(email) ⇒ Object

returns bits 1 for paid, 2 for zahlungsbefreit, 4 for lehrmittelfreiheit



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'src/ruby/include/lehrbuchverein.rb', line 26

def determine_lehrmittelverein_state_for_email(email)
    result = 0
    temp = neo4j_query(<<~END_OF_QUERY, {:email => email})
        MATCH (u:User {email: $email, lmv_no_pay: true})
        RETURN u.email;
    END_OF_QUERY
    result += 2 if temp.size > 0
    # result += 4 if [5, 6].include?(((@@user_info[email] || {})[:klasse] || '').to_i)
    temp = neo4j_query(<<~END_OF_QUERY, {:email => email, :jahr => LEHRBUCHVEREIN_JAHR})
        MATCH (u:User {email: $email})-[:PAID_FOR]->(j:Lehrbuchvereinsjahr {jahr: $jahr})
        RETURN u.email;
    END_OF_QUERY
    result += 1 if temp.size > 0
    return result
end

#device_logged_in?Boolean

Returns true if a device is logged in.

Returns:

  • (Boolean)


101
102
103
# File 'src/ruby/include/user.rb', line 101

def device_logged_in?
    !@session_device.nil?
end

#eval_lilypads(_content) ⇒ Object



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'src/ruby/include/website.rb', line 251

def eval_lilypads(_content)
    content = _content.dup
    while true
        index = content.index('#{')
        break if index.nil?
        length = 2
        balance = 1
        while index + length < content.size && balance > 0
            c = content[index + length]
            balance -= 1 if c == '}'
            balance += 1 if c == '{'
            length += 1
        end
        code = content[index + 2, length - 3]
        begin
            # STDERR.puts code
            content[index, length] = eval(code).to_s || ''
        rescue
            STDERR.puts "Error while evaluating:"
            STDERR.puts code
            raise
        end
    end
    content
end

#external_user_logged_in?Boolean

Returns:

  • (Boolean)


197
198
199
# File 'src/ruby/include/user.rb', line 197

def external_user_logged_in?
    user_logged_in? && (EXTERNAL_USERS.include?(@session_user[:email]))
end

#external_users_for_session_userObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'src/ruby/include/ext_user.rb', line 2

def external_users_for_session_user
    result = {:groups => [], :recipients => {}, :order => []}
    return result unless user_who_can_manage_tablets_or_teacher_logged_in?
    # add pre-defined external users
    @@predefined_external_users[:groups].each do |x|
        result[:groups] << x
    end
    @@predefined_external_users[:recipients].each_pair do |k, v|
        result[:recipients][k] = v
    end
    
    # add external users from user's address book
    ext_users = neo4j_query(<<~END_OF_QUERY, :session_email => @session_user[:email]).map { |x| x['e'] }
        MATCH (u:User {email: $session_email})-[:ENTERED_EXT_USER]->(e:ExternalUser)
        RETURN e
        ORDER BY e.name
    END_OF_QUERY
    ext_users.each do |entry|
        result[:recipients][entry[:email]] = {:label => entry[:name]}
        result[:order] << entry[:email]
    end

    result
end

#fach_for_lesson_key(lesson_key) ⇒ Object



203
204
205
# File 'src/ruby/include/tablet_set.rb', line 203

def fach_for_lesson_key(lesson_key)
    (@@lessons[:lesson_keys][lesson_key] || {})[:pretty_folder_name] || 'NN'
end

#find_available_tablet_sets(datum, start_time, end_time, lesson_key = nil, offset = nil) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'src/ruby/include/tablet_set.rb', line 213

def find_available_tablet_sets(datum, start_time, end_time, lesson_key = nil, offset = nil)
    available_tablet_sets = []
    @@tablet_sets.keys.each do |tablet_id|
        available_tablet_sets << tablet_id
    end

    booked_tablet_sets_timespan = already_booked_tablet_sets_for_timespan(datum, start_time, end_time)
    booked_tablet_sets_day = already_booked_tablet_sets_for_day(datum)
    klasse5or6 = nil

    if lesson_key
        # consider klasse
        klassen = @@lessons[:lesson_keys][lesson_key][:klassen]
        klasse5or6 = klassen.any? { |x| [5, 6].include?(x.to_i) }

        # sort by :prio_unterstufe
        # available_tablet_sets.sort! do |a, b|
        #     a_prio = (!!@@tablet_sets[a][:prio_unterstufe]) ? 1 : 0
        #     b_prio = (!!@@tablet_sets[b][:prio_unterstufe]) ? 1 : 0
        #     dir = a_prio <=> b_prio
        #     if dir == 0
        #         a <=> b
        #     else
        #         dir * (klasse5or6 ? -1 : 1)
        #     end
        # end

        # also consider room
        begin
            timetable_date = @@lessons[:start_date_for_date][datum]
            wday = (Date.parse(datum).wday + 6) % 7
            raum = @@lessons[:timetables][timetable_date][lesson_key][:stunden][wday].values.first[:raum]
            available_tablet_sets.select! do |x|
                if @@tablet_sets[x][:only_these_rooms_strict]
                    if @@tablet_sets[x][:only_these_rooms]
                        @@tablet_sets[x][:only_these_rooms].include?(raum)
                    else
                        true
                    end
                else
                    true
                end
            end
        rescue StandardError => e
            debug("An error occured while trying to determine the room for a lesson: #{e}, ignoring the room now...")
        end
    end

    tablet_sets = {}
    available_tablet_sets.each do |x|
        blocked_by = booked_tablet_sets_timespan[x]
        if lesson_key
            if blocked_by
                blocked_by.reject! { |x| x[:lesson_key] == lesson_key}
            end
        end
        tablet_sets[x] = {
            :count => @@tablet_sets[x][:count],
            :standort => @@tablet_sets[x][:standort],
            :is_tablet_set => @@tablet_sets[x][:is_tablet_set],
            :label => @@tablet_sets[x][:label],
            :blocked_by => blocked_by
        }
        hints = []
        if booked_tablet_sets_timespan[x]
            booked_tablet_sets_timespan[x].to_a.each do |entry|
                if entry[:lesson_key]
                    pretty_fach = fach_for_lesson_key(entry[:lesson_key])
                    if @@tablet_sets[x][:is_tablet_set] == true
                        hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Dieser Tabletsatz wurde bereits von <b>#{entry[:display_name]}</b> gebucht#{entry[:lesson_key] ? ': ' + pretty_fach : ''}."
                    else
                        hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Dieses Gerät wurde bereits von <b>#{entry[:display_name]}</b> gebucht#{entry[:lesson_key] ? ': ' + pretty_fach : ''}."
                    end
                else
                    if @@tablet_sets[x][:is_tablet_set] == true
                        hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Dieser Tabletsatz wurde bereits von <b>#{entry[:display_name]}</b> gebucht."
                    else
                        hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Dieses Gerät wurde bereits von <b>#{entry[:display_name]}</b> gebucht."
                    end
                end
            end
        elsif booked_tablet_sets_day[x]
            bookings_before = []
            bookings_after = []
            booked_tablet_sets_day[x].each do |entry|
                if entry[:end_time] <= start_time
                    bookings_before << entry
                else
                    bookings_after << entry
                end
            end
            unless bookings_before.empty?
                booking = bookings_before.last
                t = hh_mm_to_i(start_time) - hh_mm_to_i(booking[:end_time])
                if t <= TABLET_SET_WARNING_BEFORE_MINUTES
                    pretty_fach = fach_for_lesson_key(booking[:lesson_key])
                    if @@tablet_sets[x][:is_tablet_set] == true
                        hints << "<span class='text-danger'><i class='fa fa-clock-o'></i></span>&nbsp;&nbsp;Dieser Tabletsatz wird bis #{t} Minuten vor Stundenbeginn noch von <b>#{booking[:display_name]}</b> benötigt#{booking[:lesson_key] ? ': ' + pretty_fach : ''}"
                    else
                        hints << "<span class='text-danger'><i class='fa fa-clock-o'></i></span>&nbsp;&nbsp;Dieses Gerät wird bis #{t} Minuten vor Stundenbeginn noch von <b>#{booking[:display_name]}</b> benötigt#{booking[:lesson_key] ? ': ' + pretty_fach : ''}"
                    end
                end
            end
            unless bookings_after.empty?
                booking = bookings_after.first
                t = hh_mm_to_i(booking[:start_time]) - hh_mm_to_i(end_time)
                if t <= TABLET_SET_WARNING_AFTER_MINUTES
                    pretty_fach = fach_for_lesson_key(booking[:lesson_key])
                    if @@tablet_sets[x][:is_tablet_set] == true
                        hints << "<span class='text-danger'><i class='fa fa-clock-o'></i></span>&nbsp;&nbsp;Dieser Tabletsatz wird bereits #{t} Minuten nach Stundenende von <b>#{booking[:display_name]}</b> benötigt#{booking[:lesson_key] ? ': ' + pretty_fach : ''}"
                    else
                        hints << "<span class='text-danger'><i class='fa fa-clock-o'></i></span>&nbsp;&nbsp;Dieses Gerät wird bereits #{t} Minuten nach Stundenende von <b>#{booking[:display_name]}</b> benötigt#{booking[:lesson_key] ? ': ' + pretty_fach : ''}"
                    end
                end
            end
        end
        if lesson_key && @@tablet_sets[x][:only_these_rooms]
            timetable_date = @@lessons[:start_date_for_date][datum]
            wday = (Date.parse(datum).wday + 6) % 7
            raum = nil
            begin
                raum = @@lessons[:timetables][timetable_date][lesson_key][:stunden][wday].values.first[:raum]
            rescue
            end
            if raum
                unless @@tablet_sets[x][:only_these_rooms].include?(raum)
                    hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Dieser Tabletsatz ist weit vom Raum #{raum} entfernt. Bitte wählen Sie deshalb – falls möglich – einen anderen Tabletsatz."
                end
            end
        end
        unless klasse5or6.nil?
            if klasse5or6 && !@@tablet_sets[x][:prio_unterstufe]
                hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Sie buchen einen Tabletsatz für eine Unterstufenklasse, dieser Tabletsatz ist allerdings für die Unterstufe nicht so leicht zu transportieren. Bitte wählen Sie deshalb – falls möglich – einen anderen Tabletsatz."
            elsif !klasse5or6 && @@tablet_sets[x][:prio_unterstufe]
                hints << "<span class='text-danger'><i class='fa fa-warning'></i></span>&nbsp;&nbsp;Sie buchen einen Tabletsatz für die Mittel- oder Oberstufe, dieser Tabletsatz ist allerdings für die Unterstufe besonders leicht zu transportieren. Bitte wählen Sie deshalb – falls möglich – einen anderen Tabletsatz."
            end
        end
        if hints.empty?
            if @@tablet_sets[x][:is_tablet_set] == true
                hints << "<span class='text-success'><i class='fa fa-check'></i></span>&nbsp;&nbsp;Dieser Tabletsatz ist eine gute Wahl für Ihre Unterrichtsstunde."
            end
        end
        if @@tablet_sets[x][:hint]
            hints << "<span class='text-info'><i class='fa fa-info-circle'></i></span>&nbsp;&nbsp;#{@@tablet_sets[x][:hint]}"
        end
        unless hints.empty?
            tablet_sets[x][:hint] = hints.join('<br />')
        end
    end
    return tablet_sets, available_tablet_sets
end

#finished_zeugniskonferenzenObject



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'src/ruby/include/monitor.rb', line 173

def finished_zeugniskonferenzen
    sha1_list = neo4j_query(<<~END_OF_QUERY, {:t1 => Time.now.to_i}).map { |x| x['m.sha1'] }
        MATCH (m:MonitorZeugniskonferenzState)
        WHERE m.t1 IS NOT NULL
        RETURN m.sha1;
    END_OF_QUERY
    sha1_list = Set.new(sha1_list)
    result = []
    today = Date.today.strftime('%Y-%m-%d')
    (ZEUGNISKONFERENZEN[today] || []).each do |entry|
        sha1 = Digest::SHA1.hexdigest([today, entry].to_json)[0, 16]
        if sha1_list.include?(sha1)
            result << entry[0]
        end
    end
    result
end

#fix_images(s) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'src/ruby/include/website.rb', line 230

def fix_images(s)
    s.gsub(/image{[^}]+}/) do |x|
        m = x.match(/^image{([^}]+)}$/)
        parts = m[1].split(',')
        slug = parts[0].strip
        pos = (parts[1] || '').strip
        caption = nil
        if ['l3', 'l4', 'l5', 'l6', 'r3', 'r4', 'r5', 'r6'].include?(pos)
            caption = (parts[2, parts.size - 2] || []).join(',').strip
        else
            caption = (parts[1, parts.size - 1] || []).join(',').strip
            pos = nil
        end
        caption = (caption[1, caption.size - 2] || '').strip
        '#{' + "_include_image(\"#{slug}\", \"#{pos}\", \"#{caption.gsub('"', '\\"')}\")}"
    end.gsub(/file{[^}]+}{[^}]+}/) do |x|
        m = x.match(/^file{([^}]+)}{([^}]+)}$/)
        '#{_include_file("' + m[1] + '", "' + m[2] + '")}'
    end
end

#gen_jitsi_data(path) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'src/ruby/include/jitsi.rb', line 111

def gen_jitsi_data(path)
    ua = USER_AGENT_PARSER.parse(request.env['HTTP_USER_AGENT'])
    browser_icon = 'fa-microphone'
    browser_name = 'Browser'
    ['edge', 'firefox', 'chrome', 'safari', 'opera'].each do |x|
        if ua.family.downcase.include?(x)
            browser_icon = x
            browser_name = x.capitalize
        end
    end
    os_family = ua.os.family.downcase.gsub(/\s+/, '').strip
    result = {:html => "<p class='alert alert-danger'>Der Videochat konnte nicht gefunden werden.</p>"}
    room_name = nil
    can_enter_room = false
    eid = nil
    ext_name = nil
    begin
        presence_token = nil
        event_stream_jwt = nil
        if path == 'Lehrerzimmer'
            if teacher_logged_in?
                room_name = 'Lehrerzimmer'
                can_enter_room = true
                result[:html] = ''
                result[:html] += "<div class='alert alert-warning'><strong>Hinweis:</strong> Wenn Sie das Lehrerzimmer betreten, wird allen Kolleginnen und Kollegen über dem Stundenplan angezeigt, dass Sie momentan im Lehrerzimmer sind. Das Lehrerzimmer steht nicht nur Lehrkräften, sondern auch unseren Kolleg*innen aus dem Otium und dem Sekretariat zur Verfügung. Für Schülerinnen und Schüler ist der Zutritt nicht möglich.</div>"
            end
        elsif path[0, 6] == 'event/'
            result[:html] = ''
            # it's an event!
            parts = path.split('/')
            eid = parts[1]
            code = parts[2]
            invitation = nil
            organizer_email = nil
            event = nil
            data = neo4j_query_expect_one(<<~END_OF_QUERY, :eid => eid)
                MATCH (e:Event {id: $eid})-[:ORGANIZED_BY]->(ou:User)
                WHERE COALESCE(e.deleted, false) = false
                RETURN e, ou.email;
            END_OF_QUERY
            organizer_email = data['ou.email']
            event = data['e']
            room_name = room_name_for_event(event[:title], eid)
            
            if code
                # EVENT - EXTERNAL USER WITH CODE
                rows = neo4j_query(<<~END_OF_QUERY, :eid => eid)
                    MATCH (u)-[rt:IS_PARTICIPANT]->(e:Event {id: $eid})-[:ORGANIZED_BY]->(ou:User)
                    WHERE (u:ExternalUser OR u:PredefinedExternalUser) AND COALESCE(e.deleted, false) = false AND COALESCE(rt.deleted, false) = false
                    RETURN e, ou.email, u;
                END_OF_QUERY
                invitation = rows.select do |row|
                    row_code = Digest::SHA2.hexdigest(EXTERNAL_USER_EVENT_SCRAMBLER + row['e'][:id] + row['u'][:email]).to_i(16).to_s(36)[0, 8]
                    code == row_code
                end.first
                ext_name = invitation['u'][:name]
                event_stream_jwt = gen_jwt_for_stream(ext_name) if event[:stream]
            else
                # EVENT - INTERNAL USER
                require_user!
                begin
                    # EVENT - INTERNAL USER IS ORGANIZER
                    invitation = neo4j_query_expect_one(<<~END_OF_QUERY, :eid => eid, :email => @session_user[:email])
                        MATCH (e:Event {id: $eid})-[:ORGANIZED_BY]->(ou:User {email: $email})
                        WHERE COALESCE(e.deleted, false) = false
                        RETURN e, ou.email;
                    END_OF_QUERY
                rescue
                    # EVENT - INTERNAL USER IS INVITED
                    invitation = neo4j_query_expect_one(<<~END_OF_QUERY, :eid => eid, :email => @session_user[:email])
                        MATCH (u:User {email: $email})-[rt:IS_PARTICIPANT]->(e:Event {id: $eid})-[:ORGANIZED_BY]->(ou:User)
                        WHERE COALESCE(e.deleted, false) = false AND COALESCE(rt.deleted, false) = false
                        RETURN e, ou.email, u;
                    END_OF_QUERY
                end
                event_stream_jwt = gen_jwt_for_stream(@session_user[:display_name]) if event[:stream]
            end
            assert(invitation != nil)

            now = Time.now
            event_start = Time.parse("#{event[:date]}T#{event[:start_time]}")
            event_end = Time.parse("#{event[:date]}T#{event[:end_time]}")
            result[:html] += "<b class='key'>Termin:</b>#{event[:title]}<br />\n"
            result[:html] += "<b class='key'>Eingeladen von:</b>#{(@@user_info[organizer_email] || {})[:display_name]}<br />\n"
            event_date = Date.parse(event[:date])
            result[:html] += "<b class='key'>Datum:</b>#{WEEKDAYS[event_date.wday]}, #{event_date.strftime('%d.%m.%Y')}<br />\n"
            result[:html] += "<b class='key'>Zeit:</b>#{event[:start_time]} &ndash; #{event[:end_time]} Uhr<br />\n"
            if now < event_start - JITSI_EVENT_PRE_ENTRY_TOLERANCE * 60
                result[:html] += "<div class='alert alert-warning'>Sie können den Raum erst #{JITSI_EVENT_PRE_ENTRY_TOLERANCE} Minuten vor Beginn betreten. Bitte laden Sie die Seite dann neu, um in den Raum zu gelangen.</div>"
                # room can't yet be entered (too early)
            elsif now > event_end + JITSI_EVENT_POST_ENTRY_TOLERANCE * 60
                # room can't be entered anymore (too late)
                result[:html] += "<div class='alert alert-danger'>Der Termin liegt in der Vergangenheit. Sie können den Videochat deshalb nicht mehr betreten.</div>"
            else
                can_enter_room = true
            end
            can_enter_room = true if admin_logged_in?
            result[:html] += "<hr />"
        else
            # it's a lesson, only allow between 07:00 and 18:00
            result[:html] = ''
            require_user!
            can_enter_room = true
            room_name = path
            if room_name.index('Klassenstream') == 0
                if (!@session_user[:teacher]) && (!Main.get_homeschooling_for_user(@session_user[:email])) && room_name.index('Klassenstream') == 0
                    result[:html] += "<div class='alert alert-danger'>Du bist momentan nicht für den Klassenstream freigeschaltet, da du in Gruppe #{@session_user[:group2]} eingeteilt bist und auch nicht als »zu Hause« markiert bist. Deine Klassenleiterin oder dein Klassenleiter kann dich freischalten.</div>"
                    can_enter_room = false
                end
                if can_enter_room
                    now_s = Time.now.strftime('%H:%M')
                    if now_s < '07:00' || now_s > '18:00'
                        result[:html] += "<div class='alert alert-warning'>Der #{PROVIDE_CLASS_STREAM ? 'Klassenstream' : 'Stream'} ist nur von 07:00 bis 18:00 Uhr geöffnet.</div>"
                        can_enter_room = false
                    end
                end
            else
                if teacher_tablet_logged_in?
                    ext_name = path.split('@')[1]
                    ext_name = URI.decode_www_form(ext_name).first.first
                    path = path.split('@')[0]
                end
                if kurs_tablet_logged_in?
                    ext_name = 'Kursraum'
                    path = path.split('@')[0]
                end
                timetable_id = @session_user[:id]
                if @session_user[:is_tablet]
                    tablet_id = @session_user[:tablet_id]
                    if (@@tablets[tablet_id] || {})[:school_streaming]
                        # determine teacher who has booked the tablet now
                        today = DateTime.now.strftime('%Y-%m-%d')
                        results = neo4j_query(<<~END_OF_QUERY, {:tablet_id => tablet_id, :today => today})
                            MATCH (t:Tablet {id: $tablet_id})<-[:WHICH]-(b:Booking {datum: $today, confirmed: true})-[:FOR]->(i:LessonInfo)-[:BELONGS_TO]->(l:Lesson)
                            RETURN b, i, l
                        END_OF_QUERY
                        now = DateTime.now.strftime('%Y-%m-%dT%H:%M')
                        found_teachers = Set.new()
                        results.each do |item|
                            booking = item['b']
                            lesson = item['l']
                            lesson_key = lesson[:key]
                            lesson_info = item['i']
                            lesson_data = @@lessons[:lesson_keys][lesson_key]
                            start_time = "#{booking[:datum]}T#{booking[:start_time]}"
                            end_time = "#{booking[:datum]}T#{booking[:end_time]}"
                            start_time = (DateTime.parse("#{start_time}:00") - STREAMING_TABLET_BOOKING_TIME_PRE / 24.0 / 60.0).strftime('%Y-%m-%dT%H:%M')
                            end_time = (DateTime.parse("#{start_time}:00") + STREAMING_TABLET_BOOKING_TIME_POST / 24.0 / 60.0).strftime('%Y-%m-%dT%H:%M')
                            if now >= start_time && now <= end_time
                                found_teachers |= Set.new(lesson_data[:lehrer])
                            end
                        end
                        unless found_teachers.empty?
                            # force timetable_id to teacher who's currently running the lesson
                            timetable_id = @@user_info[@@shorthands[found_teachers.to_a.sort.first]][:id]
                        end
                    end
                end
                result[:html] = ''
                lesson_key = path.split('/')[1]
                if kurs_tablet_logged_in? || teacher_tablet_logged_in? || klassenraum_logged_in?
                    timetable_id = @@user_info[@@shorthands[@@lessons[:lesson_keys][lesson_key][:lehrer].first]][:id]
                end
                breakout_room_name = path.split('/')[2]
                # TODO: use code from get_jitsi_room_name_for_lesson_key
                p_ymd = Date.today.strftime('%Y-%m-%d')
                p_yw = Date.today.strftime('%Y-%V')
                assert(user_logged_in?)
                timetable_path = "/gen/w/#{timetable_id}/#{p_yw}.json.gz"
                timetable = nil
                # debug timetable_path
                Zlib::GzipReader.open(timetable_path) do |f|
                    timetable = JSON.parse(f.read)
                end
                assert(!(timetable.nil?))
                timetable = timetable['events'].select do |entry|
                    entry['lesson'] && 
                            entry['lesson_key'] == lesson_key && 
                            ((entry['datum'] == p_ymd) || DEVELOPMENT || admin_logged_in?) && 
                            (entry['data'] || {})['lesson_jitsi']
                end.sort { |a, b| a['start'] <=> b['start'] }
                now_time = Time.now
                old_timetable_size = timetable.size
                # unless admin_logged_in?
                    timetable = timetable.reject do |entry|
                        t = Time.parse("#{entry['end']}:00") + JITSI_LESSON_POST_ENTRY_TOLERANCE * 60
                        now_time > t
                    end
                # end
                if timetable.empty?
                    if old_timetable_size > 0
                        result[:html] += "<div class='alert alert-warning'>Dieser Jitsi-Raum ist heute nicht mehr geöffnet.</div>"
                    else
                        result[:html] += "<div class='alert alert-warning'>Dieser Jitsi-Raum ist heute nicht geöffnet.</div>"
                    end
                    if tablet_logged_in?
                        result[:html] += "<div class='alert alert-info'>Falls Sie Jitsi gerade erst aktiviert haben sollten, versuchen Sie bitte, die Seite neu zu laden, da es manchmal ein paar Sekunden dauern kann, bis der Raum tatsächlich aktiviert ist.</div>"
                    end
                    can_enter_room = false
                else
                    # check if we have streaming restrictions for this lesson
                    lesson_info = timetable.first

                    # debug lesson_info.to_yaml
                    
                    unless self.class.stream_allowed_for_date_lesson_key_and_email(Date.today.strftime('%Y-%m-%d'), lesson_info['lesson_key'], @session_user[:email])
                        result[:html] += "<div class='alert alert-info'>Du bist für diesen Jitsi-Raum leider nicht freigeschaltet.</div>"
                        can_enter_room = false
                    else
                        t = Time.parse("#{lesson_info['start']}:00") - JITSI_LESSON_PRE_ENTRY_TOLERANCE * 60
                        room_name = lesson_info['label_lehrer_lang'].gsub(/<[^>]+>/, '') + ' ' + lesson_info['klassen'].first.map { |x| tr_klasse(x) }.join(', ')
                        unless ((lesson_info['data'] || {})['breakout_rooms'] || []).empty?
                            if teacher_logged_in?
                                presence_token = RandomTag::generate(24)
                            else
                                # SuS is logged in, generate a presence token if we have roaming breakout rooms
                                if (lesson_info['data'] || {})['breakout_rooms_roaming']
                                    presence_token = RandomTag::generate(24)
                                end
                            end
                            if presence_token
                                query_data = {
                                    :token => presence_token, 
                                    :lesson_key => lesson_info['lesson_key'], 
                                    :offset => lesson_info['lesson_offset'], 
                                    :email => @session_user[:email], 
                                    :timestamp => (Time.now + PRESENCE_TOKEN_EXPIRY_TIME).to_i
                                }
                                if DEVELOPMENT
                                    debug "Generating presence token"
                                    debug query_data.to_yaml
                                end
                                neo4j_query_expect_one(<<~END_OF_QUERY, query_data)
                                    MATCH (u:User {email: $email}), (i:LessonInfo {offset: $offset})-[:BELONGS_TO]->(l:Lesson {key: $lesson_key})
                                    CREATE (n:PresenceToken {token: $token, expiry: $timestamp})
                                    CREATE (i)<-[:FOR]-(n)-[:BELONGS_TO]->(u)
                                    RETURN n;
                                END_OF_QUERY
                            end
                        end
                        if breakout_room_name
                            room_name += " #{breakout_room_name}"
                        end
                        if now_time >= t
                            can_enter_room = true
                        else
                            timediff = ((t - now_time).to_f / 60.0).ceil
                            tds = "#{timediff} Minute#{timediff == 1 ? '' : 'n'}"
                            tds = "#{timediff / 60} Stunde#{timediff / 60 == 1 ? '' : 'n'} und #{timediff % 60} Minute#{(timediff % 60) == 1 ? '' : 'n'}" if timediff > 60
                            if timediff == 1
                                tds = 'einer Minute'
                            elsif timediff == 2
                                tds = 'zwei Minuten'
                            elsif timediff == 3
                                tds = 'drei Minuten'
                            elsif timediff == 4
                                tds = 'vier Minuten'
                            elsif timediff == 5
                                tds = 'fünf Minuten'
                            end
                            result[:html] += "<div class='alert alert-warning'>Der Jitsi-Raum <strong>»#{room_name}«</strong> ist erst ab #{t.strftime('%H:%M')} Uhr geöffnet. Du kannst ihn in #{tds} betreten.</div>"
                            can_enter_room = false
                        end
                    end
                end
                room_name = CGI.unescape(room_name)
            end
        end
        if can_enter_room
            # room can be entered now
            if ext_name
                temp_name = ext_name.dup
                if teacher_tablet_logged_in?
                    temp_name = @@user_info[@@shorthands[ext_name]][:display_last_name]
                end
                result[:html] += "<p>Sie können dem Videochat jetzt als <b>#{temp_name}</b> beitreten.</p>\n"
            end
            result[:html] += "<div class='alert alert-secondary'>\n"
            result[:html] += "<p>Ich habe die <a href='/api/jitsi_terms'>Nutzerordnung</a> und die <a href='/api/jitsi_dse'>Datenschutzerklärung</a> zur Kenntnis genommen und willige ein.</p>\n"
            room_name = CGI.escape(remove_accents(room_name).gsub(/[\:\?#\[\]@!$&\\'()*+,;=><\/"]/, '')).gsub('+', '%20')
            jwt = gen_jwt_for_room(room_name, eid, ext_name)
            result[:html] += "<div class='go_div'>\n"
            # edge firefox chrome safari opera internet-explorer
            if os_family == 'ios' || os_family == 'macosx'
                result[:html] += "<a class='btn btn-success' href='org.jitsi.meet://#{JITSI_HOST}/#{room_name}?jwt=#{jwt}'><i class='fa fa-apple'></i>&nbsp;&nbsp;Jitsi-Raum mit Jitsi Meet betreten (iPhone und iPad)</a>"
                result[:html] += "<p style='font-size: 90%;'><em>Installieren Sie bitte die Jitsi Meet-App aus dem <a href='https://apps.apple.com/de/app/jitsi-meet/id1165103905' target='_blank'>App Store</a>.</em></p>"
                unless browser_icon == 'safari'
                    result[:html] += "<a class='btn btn-outline-secondary' href='https://#{JITSI_HOST}/#{room_name}?#{presence_token ? "presence_token=#{presence_token}&" : ''}jwt=#{jwt}'><i class='fa fa-#{browser_icon}'></i>&nbsp;&nbsp;Jitsi-Raum mit #{browser_name} betreten</a>"
                end
                if browser_icon == 'safari'
                    result[:html] += "<p style='font-size: 90%;'><em>Falls Sie einen Mac verwenden: Leider funktioniert Jitsi Meet nicht mit Safari. Verwenden Sie bitte einen anderen Web-Browser wie <a href='https://www.google.com/intl/de_de/chrome/' target='_blank'>Google Chrome</a> oder <a href='https://www.mozilla.org/de/firefox/new/' target='_blank'>Firefox</a>.</em></p>"
                end
            elsif os_family == 'android'
                result[:html] += "<a class='btn btn-success' href='intent://#{JITSI_HOST}/#{room_name}?jwt=#{jwt}#Intent;scheme=org.jitsi.meet;package=org.jitsi.meet;end'><i class='fa fa-microphone'></i>&nbsp;&nbsp;Jitsi-Raum mit Jitsi Meet für Android betreten</a>"
                result[:html] += "<p style='font-size: 90%;'><em>Installieren Sie bitte die Jitsi Meet-App aus dem <a href='https://play.google.com/store/apps/details?id=org.jitsi.meet' target='_blank'>Google Play Store</a> oder via <a href='https://f-droid.org/en/packages/org.jitsi.meet/' target='_blank' style=''>F&#8209;Droid</a>.</em></p>"
                result[:html] += "<a class='btn btn-outline-secondary' href='https://#{JITSI_HOST}/#{room_name}?#{presence_token ? "presence_token=#{presence_token}&" : ''}jwt=#{jwt}'><i class='fa fa-#{browser_icon}'></i>&nbsp;&nbsp;Jitsi-Raum mit #{browser_name} betreten</a>"
            else
                result[:html] += "<a class='btn btn-success' href='https://#{JITSI_HOST}/#{room_name}?#{presence_token ? "presence_token=#{presence_token}&" : ''}jwt=#{jwt}'><i class='fa fa-#{browser_icon}'></i>&nbsp;&nbsp;Jitsi-Raum mit #{browser_name} betreten</a>"
            end
            if event_stream_jwt
                result[:html] += "<div class='alert alert-warning'>"
                result[:html] += "<p>Falls Sie im Jitsi-Raum <strong>Verbindungsprobleme</strong> oder <strong>Zeitverzögerungen</strong> erleben sollten, probieren Sie bitte den Livestream, der für diesen Termin bereitgestellt wird. Sie finden dort einen Chat, über den Sie Wortmeldungen und Fragen senden können.</p>"
                result[:html] += "<a class='btn btn-warning' href='/livestream?jwt=#{event_stream_jwt}'><i class='fa fa-video-camera'></i>&nbsp;&nbsp;Zum Livestream…</a>"
                result[:html] += "</div>"
            end
            result[:html] += "</div>\n"
            result[:html] += "</div>\n"
        end
    rescue StandardError => e
        debug "gen_jitsi_data failed for path [#{path}]"
        debug e
        debug e.backtrace
        result = {:html => "<p class='alert alert-danger'>Der Videochat konnte nicht gefunden werden.</p>"}
    end
    result
end

#gen_jwt_for_room(room = '', eid = nil, user = nil, email = nil) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'src/ruby/include/jitsi.rb', line 13

def gen_jwt_for_room(room = '', eid = nil, user = nil, email = nil)
    payload = {
        :context => { :user => {}},
        :aud => JWT_APPAUD,
        :iss => JWT_APPISS,
        :sub => JWT_SUB,
        :room => room.strip,
        :exp => DateTime.parse("#{Time.now.strftime('%Y-%m-%d')} 00:00:00").to_time.to_i + 24 * 60 * 60,
        :moderator => teacher_logged_in?
    }
    payload[:context][:user][:name] = user if user
    payload[:context][:user][:email] = email if email
    if user_logged_in?
        use_user = @session_user
        # don't generate JWT if user is on negative list (/data/schueler/disable-jitsi.txt)
        if @@user_info[@session_user[:email]] && @@user_info[@session_user[:email]][:jitsi_disabled]
            raise 'no jitsi for you'
        end
        if teacher_tablet_logged_in?
            use_user = @@user_info[@@shorthands[user]]
        elsif kurs_tablet_logged_in?
            use_user = {:display_name => 'Kursraum'}
        elsif klassenraum_logged_in?
            use_user = {:display_name => 'Klassenraum'}
        elsif tablet_logged_in?
            if @@tablets[@session_user[:tablet_id]][:klassen_stream]
                use_user = {:display_name => "Klassenstreaming-Tablet #{@@tablets[@session_user[:tablet_id]][:klassen_stream]}"}
                payload[:moderator] = true
            else
                use_user = {:display_name => 'Tablet'}
            end
        end
        payload[:context][:user][:name] = use_user[:teacher] ? use_user[:display_last_name] : use_user[:display_name]
        payload[:context][:user][:email] = use_user[:email]
        payload[:context][:user][:avatar] = "#{NEXTCLOUD_URL}/index.php/avatar/#{use_user[:nc_login]}/128"
        if eid
            organizer_email = neo4j_query_expect_one(<<~END_OF_QUERY, :eid => eid, :session_email => use_user[:email])['ou.email']
                MATCH (e:Event {id: $eid})-[:ORGANIZED_BY]->(ou:User)
                WHERE COALESCE(e.deleted, false) = false
                RETURN ou.email;
            END_OF_QUERY
            payload[:moderator] = admin_logged_in? || (organizer_email == use_user[:email])
        end
    end
    assert(!(payload[:context][:user][:name].nil?))
    assert(payload[:context][:user][:name].strip.size > 0)
    assert(room.strip.size > 0)
    
    debug "Generated Jitsi token for #{payload[:context][:user][:name]} for #{payload[:room]}" if DEVELOPMENT || true
    
    token = JWT.encode payload, JWT_APPKEY, algorithm = 'HS256', header_fields = {:typ => 'JWT'}
    token
end

#gen_jwt_for_stream(name) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'src/ruby/include/jitsi.rb', line 67

def gen_jwt_for_stream(name)
    payload = {
        :context => { :user => { :name => name }},
        :aud => JWT_APPAUD_STREAM,
        :iss => JWT_APPISS,
        :sub => JWT_SUB,
        :exp => DateTime.parse("#{Time.now.strftime('%Y-%m-%d')} 00:00:00").to_time.to_i + 24 * 60 * 60
    }
    assert(!(payload[:context][:user][:name].nil?))
    assert(payload[:context][:user][:name].strip.size > 0)

    token = JWT.encode payload, JWT_APPKEY_STREAM, algorithm = 'HS256', header_fields = {:typ => 'JWT'}
    token
end

#gen_poll_data(path) ⇒ Object



940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
# File 'src/ruby/include/poll.rb', line 940

def gen_poll_data(path)
    result = {}
    result[:html] = ''
    parts = path.sub('/poll/', '').split('/')
    prid = parts[0]
    code = parts[1]
    assert((prid.is_a? String) && (!code.empty?))
    assert((code.is_a? String) && (!code.empty?))
    rows = neo4j_query(<<~END_OF_QUERY, :prid => prid)
        MATCH (u)-[rt:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(ou:User)
        WHERE (u:ExternalUser OR u:PredefinedExternalUser) AND COALESCE(pr.deleted, false) = false AND COALESCE(rt.deleted, false) = false
        RETURN pr, ou.email, u, p;
    END_OF_QUERY
    invitation = rows.select do |row|
        row_code = Digest::SHA2.hexdigest(EXTERNAL_USER_EVENT_SCRAMBLER + row['pr'][:id] + row['u'][:email]).to_i(16).to_s(36)[0, 8]
        code == row_code
    end.first
    if invitation.nil?
        redirect "#{WEB_ROOT}/poll_not_found", 302
        return
    end
    ext_name = invitation['u'][:name]
    poll = invitation['p']
    poll_run = invitation['pr']
    now = "#{Date.today.strftime('%Y-%m-%d')}T#{Time.now.strftime('%H:%M')}:00"
    start_time = "#{poll_run[:start_date]}T#{poll_run[:start_time]}:00"
    end_time = "#{poll_run[:end_date]}T#{poll_run[:end_time]}:00"
    result[:organizer] = (@@user_info[invitation['ou.email']] || {})[:display_last_name]
    result[:organizer_icon] = user_icon(invitation['ou.email'], 'avatar-fill')
    result[:title] = poll[:title]
    result[:end_date] = poll_run[:end_date]
    result[:end_time] = poll_run[:end_time]
    result[:prid] = prid
    result[:code] = code
    result[:external_user_name] = ext_name
    if now < start_time
        result[:disable_launch_button] = true
        result[:html] += "Die Umfrage öffnet erst am"
        result[:html] += " #{Date.parse(poll_run[:start_date]).strftime('%d.%m.%Y')} um #{poll_run[:start_time]} Uhr (in <span class='moment-countdown' data-target-timestamp='#{poll_run[:start_date]}T#{poll_run[:start_time]}:00' data-before-label='' data-after-label=''></span>)."
    elsif now > end_time
        result[:disable_launch_button] = true
        result[:html] += "Die Umfrage ist bereits beendet."
    else
        result[:disable_launch_button] = false
        result[:html] += "Sie können noch bis zum #{Date.parse(poll_run[:end_date]).strftime('%d.%m.%Y')} um #{poll_run[:end_time]} Uhr teilnehmen (die Umfrage <span class='moment-countdown' data-target-timestamp='#{poll_run[:end_date]}T#{poll_run[:end_time]}:00' data-before-label='läuft noch' data-after-label='ist vorbei'></span>)."
    end
    result
end

#get_current_jitsi_users_for_lesson(lesson_key, offset, user = nil) ⇒ Object



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'src/ruby/include/jitsi.rb', line 481

def get_current_jitsi_users_for_lesson(lesson_key, offset, user = nil)
    lesson_info = neo4j_query_expect_one(<<~END_OF_QUERY, {:lesson_key => lesson_key, :offset => offset})['i']
        MATCH (i:LessonInfo {offset: $offset})-[:BELONGS_TO]->(l:Lesson {key: $lesson_key})
        RETURN i;
    END_OF_QUERY
    room_name = get_jitsi_room_name_for_lesson_key(lesson_key, user)
    assert(!(room_name.nil?), 'not today!', true)
    lesson_room_name = CGI.escape(room_name.gsub(/[\:\?#\[\]@!$&\\'()*+,;=><\/"]/, '')).gsub('+', '%20').downcase

    jitsi_rooms = current_jitsi_rooms()
    room_participants = []
    breakout_room_index = {}
    breakout_room_urls = []
    (lesson_info[:breakout_rooms] || []).each.with_index do |room_name, i|
        room_participants << []
        escaped_room_name = CGI.escape(room_name.gsub(/[\:\?#\[\]@!$&\\'()*+,;=><\/"]/, '')).gsub('+', '%20').downcase
        escaped_room_name = "#{lesson_room_name}%20#{escaped_room_name}"
        breakout_room_urls << escaped_room_name
        breakout_room_index[escaped_room_name] = {
            :room_name => room_name,
            :index => i
        }
    end
    lesson_room_participants = []
    present_sus = Set.new()
    if jitsi_rooms
        jitsi_rooms.each do |room|
            entry = breakout_room_index[room['roomName'].downcase]
            if entry
                room_participants[entry[:index]] = room['participants'].select do |x|
                    !((@@user_info[x['jwtEMail']] || {})[:teacher])
                end.map do |x|
                    present_sus << x['jwtEMail']
                    x['jwtName']
                end.sort.uniq
            end
            if room['roomName'].downcase == lesson_room_name
                lesson_room_participants = room['participants'].select do |x|
                    !((@@user_info[x['jwtEMail']] || {})[:teacher])
                end.map do |x|
                    present_sus << x['jwtEMail']
                    x['jwtName']
                end.sort.uniq
            end
        end
    end
    missing_sus = (Set.new((@@schueler_for_lesson[lesson_key] || [])) - present_sus).map do |email|
        @@user_info[email][:display_name]
    end.sort
    {:lesson_room => lesson_room_participants, 
     :breakout_rooms => room_participants, 
     :missing_sus => (@@user_info[user] || {})[:teacher] ? missing_sus : nil,
     :breakout_room_names => lesson_info[:breakout_rooms],
     :lesson_room_name => lesson_room_name,
     :breakout_room_index => breakout_room_index,
     :breakout_room_urls => breakout_room_urls}
end

#get_current_salzh_statusObject



375
376
377
# File 'src/ruby/include/salzh.rb', line 375

def get_current_salzh_status
    Main.get_current_salzh_status
end

#get_current_salzh_status_for_logged_in_teacherObject



379
380
381
382
383
384
385
# File 'src/ruby/include/salzh.rb', line 379

def get_current_salzh_status_for_logged_in_teacher
    entries = get_current_salzh_status
    entries.select! do |entry|
        (@@schueler_for_teacher[@session_user[:shorthand]] || []).include?(entry[:email])
    end
    entries
end

#get_current_salzh_susObject



299
300
301
# File 'src/ruby/include/salzh.rb', line 299

def get_current_salzh_sus
    Main.get_current_salzh_sus
end

#get_current_user_sessionsObject



514
515
516
517
# File 'src/ruby/include/login.rb', line 514

def get_current_user_sessions()
    require_user!
    get_sessions_for_user(@session_user[:email])
end

#get_gradient(colors, t) ⇒ Object



105
106
107
108
109
110
111
112
113
# File 'src/ruby/include/color.rb', line 105

def get_gradient(colors, t)
    i = (t * (colors.size - 1)).to_i
    i = colors.size - 2 if i == colors.size - 1
    f = (t * (colors.size - 1)) - i
    f1 = 1.0 - f
    a = html_to_rgb(colors[i])
    b = html_to_rgb(colors[i + 1])
    rgb_to_html([a[0] * f1 + b[0] * f, a[1] * f1 + b[1] * f, a[2] * f1 + b[2] * f])
end

#get_gradientsObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'src/ruby/include/theme.rb', line 42

def get_gradients()
    results = neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User) 
        WITH COALESCE(u.color_scheme, '#{@@standard_color_scheme}') AS scheme
        RETURN  scheme, count(scheme) AS count ORDER BY count DESC, scheme DESC;
    END_OF_QUERY
    histogram = {}
    results.each do |entry|
        entry['scheme'] ||= @@standard_color_scheme
        histogram[entry['scheme'][1, 18]] ||= 0
        histogram[entry['scheme'][1, 18]] += entry['count']
    end
    histogram_style = {}
    results.each do |entry|
        entry['scheme'] ||= @@standard_color_scheme
        style = (entry['scheme'][19] || '0').to_i
        histogram_style[style] ||= 0
        histogram_style[style] += entry['count']
    end
    color_schemes = @@color_scheme_colors.map do |x|
        paint_colors = x[0, 3].map do |c|
            rgb_to_hex(mix(hex_to_rgb(c), [255, 255, 255], 0.3))
        end
        [x[1], x[0, 3], paint_colors, x[3], x[4], x[5], histogram[x[0, 3].join('').gsub('#', '')], color_palette_for_color_scheme("l#{x[0, 3].join('').gsub('#', '')}")]
    end
    {:color_schemes => color_schemes,
     :style_histogram => histogram_style}
end

#get_ha_amt_lesson_keysObject



68
69
70
71
72
73
74
75
76
# File 'src/ruby/include/lesson.rb', line 68

def get_ha_amt_lesson_keys()
    return [] unless user_logged_in?
    return [] if @session_user[:teacher]
    results = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email])
        MATCH (u:User {email: $email})-[r:HAS_AMT {amt: 'hausaufgaben'}]->(l:Lesson)
        RETURN l.key;
    END_OF_QUERY
    return results.map { |x| x['l.key'] }
end

#get_hotspot_klassenObject



255
256
257
# File 'src/ruby/include/salzh.rb', line 255

def get_hotspot_klassen
    Main.get_hotspot_klassen
end

#get_jitsi_room_name_for_lesson_key(lesson_key, user = nil) ⇒ Object



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'src/ruby/include/jitsi.rb', line 449

def get_jitsi_room_name_for_lesson_key(lesson_key, user = nil)
    p_ymd = Date.today.strftime('%Y-%m-%d')
    p_yw = Date.today.strftime('%Y-%V')
    user_id = @@user_info[user || @session_user[:email]][:id]
    timetable_path = "/gen/w/#{user_id}/#{p_yw}.json.gz"
    timetable = nil
    Zlib::GzipReader.open(timetable_path) do |f|
        timetable = JSON.parse(f.read)
    end
    assert(!(timetable.nil?))
    timetable = timetable['events'].select do |entry|
        entry['lesson'] && 
                (entry['lesson_key'] == lesson_key) && 
                ((entry['datum'] == p_ymd) || DEVELOPMENT || (user && ADMIN_USERS.include?(user))) && 
                (entry['data'] || {})['lesson_jitsi']
    end.sort { |a, b| a['start'] <=> b['start'] }
    now_time = Time.now
    old_timetable_size = timetable.size
    unless (user && ADMIN_USERS.include?(user))
        timetable = timetable.reject do |entry|
            t = Time.parse("#{entry['end']}:00") + JITSI_LESSON_POST_ENTRY_TOLERANCE * 60
            now_time > t
        end
    end
    if timetable.empty?
        return nil
    else
        room_name = timetable.first['label_lehrer_lang'].gsub(/<[^>]+>/, '') + ' ' + timetable.first['klassen'].first.map { |x| tr_klasse(x) }.join(', ')
        return room_name
    end
end

#get_login_statsObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'src/ruby/include/stats.rb', line 2

def 
     = {}
    LOGIN_STATS_D.each do |d|
         = neo4j_query(<<~END_OF_QUERY, :today => (Date.today - d).to_s)
            MATCH (u:User) WHERE EXISTS(u.last_access) AND u.last_access >= $today
            RETURN u.email;
        END_OF_QUERY
        .map { |x| x['u.email'] }.each do |email|
            [email] ||= {}
            [email][d] = true
        end
    end
     = {}
    @@klassen_order.each do |klasse|
        [klasse] = {:total => (@@schueler_for_klasse[klasse] || []).size, :count => {}}
    end
    teacher_count = 0
    sus_count = 0
    @@user_info.each_pair do |email, user|
        if user[:teacher]
            teacher_count += 1 
        else
            sus_count += 1 
        end
    end
    [:lehrer] = {:total => teacher_count, :count => {}}
    [:sus] = {:total => sus_count, :count => {}}
    .each_pair do |email, seen|
        user = @@user_info[email]
        next if user.nil?
        seen.keys.each do |d|
            if user[:teacher]
                [:lehrer][:count][d] ||= 0
                [:lehrer][:count][d] += 1
            else
                [:sus][:count][d] ||= 0
                [:sus][:count][d] += 1
                if [user[:klasse]]
                    [user[:klasse]][:count][d] ||= 0
                    [user[:klasse]][:count][d] += 1
                end
            end
        end
    end
    
end

#get_monitor_messagesObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'src/ruby/include/monitor.rb', line 5

def get_monitor_messages()
    data = {}
    if File.exist?(MONITOR_MESSAGE_PATH)
        data = JSON.parse(File.read(MONITOR_MESSAGE_PATH))
    end
    result = {
        :messages => [],
        :images => []
    }
    [:messages, :images].each do |key|
        (data[key.to_s] || '').split("\n").each do |line|
            line.strip!
            result[key] << line unless line.empty?
        end
    end
    result
end

#get_monitor_zeugniskonferenzenObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'src/ruby/include/monitor.rb', line 120

def get_monitor_zeugniskonferenzen
    rows = neo4j_query(<<~END_OF_QUERY)
        MATCH (m:MonitorZeugniskonferenz)
        RETURN m.key, COALESCE(m.value, FALSE) AS value;
    END_OF_QUERY
    result = {}
    rows.each do |row|
        result[row['m.key']] = row['value']
    end
    result['flur'] ||= false
    result['lz'] ||= false
    result['sek'] ||= false
    result
end

#get_next_cypher_passwordObject



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'src/ruby/include/cypher.rb', line 194

def get_next_cypher_password
    @cypher_next_password = nil
    srand(@cypher_seed)
    languages = CYPHER_LANGUAGES.shuffle
    if @cypher_level == 0
        lang = languages[@cypher_level]
        line = line_for_lang(lang)
        @cypher_next_password = lang
        @cypher_token = caesar(line, 3)
    elsif @cypher_level == 1
        lang = languages[@cypher_level]
        line = line_for_lang(lang)
        @cypher_next_password = lang
        @cypher_token = skytale(line, [3, 4, 5, 6].sample)
    elsif @cypher_level == 2
        lang = languages[@cypher_level]
        line = line_for_lang(lang)
        @cypher_next_password = lang
        @cypher_token = line
    elsif @cypher_level == 3
        lang = languages[@cypher_level]
        @cypher_next_password = lang
        @cypher_token = nil
    elsif @cypher_level == 4
        lang = languages[@cypher_level]
        @cypher_next_password = lang
        @cypher_token = nil
    elsif @cypher_level == 5
        lang = languages[@cypher_level]
        line = line_for_lang(lang)
        @cypher_next_password = lang
        @cypher_token = line
    elsif @cypher_level == 6
        lang = languages[@cypher_level]
        tag = Digest::SHA1.hexdigest("#{lang.downcase}-cypher").to_i(16).to_s(36)[0, 4]
        @cypher_next_password = lang
        @cypher_token = tag
    elsif @cypher_level == 7
        pin = (0..3).map { |x| rand(10).to_s }.join('')
        tag = ''
        provided_password = neo4j_query_expect_one(<<~END_OF_QUERY, {:email => @session_user[:email]})['provided']
            MATCH (u:User {email: $email})
            RETURN COALESCE(u.cypher_provided, '') AS provided;
        END_OF_QUERY
        srand(Time.now.to_i)
        provided_password = '    ' if provided_password.strip.empty?
        unless provided_password == '    '
            t = rand(5) + 1
            (0...4).each do |i|
                t += rand(10) + 55
                break unless provided_password[i] == pin[i]
            end
            tag = "Die Überprüfung der PIN dauerte #{t} µs."
        end
        @cypher_next_password = pin
        @cypher_token = tag
    elsif @cypher_level == 8
        lang = languages[@cypher_level]
        line = "Das naechste Loesungswort lautet #{lang}"
        @cypher_next_password = lang
        @cypher_token = line
    elsif @cypher_level == 9
        lang = languages[@cypher_level].upcase
        # line = line_for_lang(lang)
        @cypher_next_password = lang
        @cypher_token = nil
    end
end

#get_next_passwordObject



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'src/ruby/include/hack.rb', line 94

def get_next_password
    @hack_next_password = nil
    srand(@hack_seed)
    if @hack_level == 0
        names = NAMES.shuffle
        @hack_next_password = names[@hack_level]
    elsif @hack_level == 1
        number = [1, 2].sample * 1000 + [1,2,3,4,5,6,7,8,9].sample * 100 + [1,2,3,4,5,6,7,8,9].sample * 10 + [1,2,3,4,5,6,7,8,9].sample
        @hack_next_password = number.to_roman.downcase
        @hack_token = "#{number}"
    elsif @hack_level == 2
        names = NAMES.shuffle
        @hack_next_password = names[@hack_level]
    elsif @hack_level == 3
        notes = [['C', 'Cis', 'D', 'Es', 'E', 'F', 'Fis', 'G', 'As', 'A', 'B', 'H'],
                 ['c', 'cis', 'd', 'es', 'e', 'f', 'fis', 'g', 'as', 'a', 'b', 'h']]

        index = (0...(notes[0].size)).to_a.sample
        mode = (0..1).to_a.sample
        available_indices = (0...(notes[0].size)).to_a
        a = (index + 0) % 12
        b = (index + ((mode == 0) ? 4 : 3)) % 12
        c = (index + 7) % 12
        available_indices.delete(a)
        available_indices.delete(b)
        available_indices.delete(c)
        available_indices.shuffle!
        x = available_indices.shift()
        y = available_indices.shift()
        z = available_indices.shift()

        chord = "#{notes[mode][index]}-#{mode == 0 ? 'Dur' : 'Moll'}"
        correct_note = [b, c].sample

        @hack_next_password = notes[1][correct_note]
        n0, n1, n2, n3 = *([correct_note, x, y, z].shuffle)
        @hack_description = "<span style='font-size: 150%;'><b>#{notes[1][n0]}</b>, <b>#{notes[1][n1]}</b>, <b>#{notes[1][n2]}</b> oder <b>#{notes[1][n3]}</b></span>"
        @hack_token = chord
    elsif @hack_level == 4
        names = NAMES.shuffle
        @hack_next_password = names[@hack_level]
    elsif @hack_level == 5
        space_events = SPACE_EVENTS.keys.shuffle
        date = space_events.first
        @hack_token = SPACE_EVENTS[date].first
        @hack_description = "#{SPACE_EVENTS[date][1].gsub('__DATE__', "<b>#{SPACE_EVENTS[date].first}</b>")} Wie viele Tage sind seitdem vergangen?"
        @hack_next_password = (Date.today - Date.parse(date)).to_i.to_s
    elsif @hack_level == 6
        ascii = '@#$%&*()[]{}'.split('').shuffle
        @hack_token = ascii.first
        @hack_next_password = sprintf('%02x', ascii.first.ord)
    elsif @hack_level == 7
        primes = PRIMES.shuffle
        ps = primes[0, 6]
        p = ps.sample
        @hack_token = ps.inject(1) { |_, x| _ * x } * p
        @hack_next_password = p.to_s
    elsif @hack_level == 8
        names = NAMES.shuffle
        @hack_next_password = names[@hack_level]
        response.headers['X-Dashboard-Hackers-Passwort'] = @hack_next_password
    elsif @hack_level == 9
        fruit = FRUIT.shuffle
        @hack_next_password = fruit.first
        @hack_token = Digest::MD5.hexdigest(@hack_next_password)
    end
    @hack_next_password
end

#get_omit_ical_typesObject



470
471
472
473
474
475
476
# File 'src/ruby/include/user.rb', line 470

def get_omit_ical_types
    types = neo4j_query_expect_one(<<~END_OF_QUERY, :email => @session_user[:email])['types']
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.omit_ical_types, []) AS types;
    END_OF_QUERY
    types
end

#get_open_doors_for_userObject



2280
2281
2282
2283
2284
2285
2286
2287
# File 'src/ruby/main.rb', line 2280

def get_open_doors_for_user()
    require_user!
    doors = neo4j_query_expect_one(<<~END_OF_QUERY, :email => @session_user[:email])['doors']
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.advent_calendar_doors, 0) AS doors;
    END_OF_QUERY
    doors
end

#get_poll_run(prid, external_code = nil) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'src/ruby/include/poll.rb', line 4

def get_poll_run(prid, external_code = nil)
    result = {}
    if external_code && !external_code.empty?
        rows = neo4j_query(<<~END_OF_QUERY, :prid => prid)
            MATCH (u)-[rt:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(au:User)
            WHERE (u:ExternalUser OR u:PredefinedExternalUser) AND COALESCE(p.deleted, false) = false AND COALESCE(pr.deleted, false) = false AND COALESCE(rt.deleted, false) = false
            RETURN u.email, pr.id, ID(u) AS unid, ID(pr) AS prnid;
        END_OF_QUERY
        invitation = rows.select do |row|
            row_code = Digest::SHA2.hexdigest(EXTERNAL_USER_EVENT_SCRAMBLER + row['pr.id'] + row['u.email']).to_i(16).to_s(36)[0, 8]
            external_code == row_code
        end.first
        assert(!(invitation.nil?))
        result = neo4j_query_expect_one(<<~END_OF_QUERY, :prid => prid, :email => invitation['u.email'], :unid => invitation['unid'], :prnid => invitation['prnid'])
            MATCH (u)-[rt:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(au:User)
            WHERE ID(u) = $unid AND ID(pr) = $prnid
            MATCH (ou)-[rt2:IS_PARTICIPANT]->(pr2:PollRun {id: $prid})-[:RUNS]->(p2:Poll)-[:ORGANIZED_BY]->(au2:User)
            WHERE COALESCE(rt2.deleted, false) = false
            RETURN u, pr, p, au.email, COUNT(ou) AS total_participants;
        END_OF_QUERY
    else
        require_user!
        result = neo4j_query_expect_one(<<~END_OF_QUERY, {:prid => prid, :email => @session_user[:email]})
            MATCH (u:User {email: $email})-[:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(au:User)
            WITH pr, p, au
            MATCH (ou)-[rt2:IS_PARTICIPANT]->(pr2:PollRun {id: $prid})-[:RUNS]->(p2:Poll)-[:ORGANIZED_BY]->(au2:User)
            WHERE COALESCE(rt2.deleted, false) = false
            RETURN pr, p, au.email, COUNT(ou) AS total_participants
        END_OF_QUERY
    end
    poll = result['p']
    poll.delete(:items)
    poll_run = result['pr']
    poll_run[:items] = JSON.parse(poll_run[:items])
    return poll, poll_run, result['au.email'], result['total_participants']
end

#get_poll_run_results(prid) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'src/ruby/include/poll.rb', line 161

def get_poll_run_results(prid)
    require_teacher_or_sv!
    unless admin_logged_in?
        # make sure we have the right user unless an admin is logged in
        temp = neo4j_query_expect_one(<<~END_OF_QUERY, {:prid => prid, :email => @session_user[:email]})
            MATCH (pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(au:User {email: $email})
            RETURN au.email;
        END_OF_QUERY
    end
    temp = neo4j_query_expect_one(<<~END_OF_QUERY, {:prid => prid})
        MATCH (pu)-[rt:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(au:User)
        WHERE COALESCE(p.deleted, false) = false
        AND COALESCE(pr.deleted, false) = false
        AND COALESCE(rt.deleted, false) = false
        RETURN au.email, pr, p, COUNT(pu) AS participant_count;
    END_OF_QUERY
    participants = neo4j_query(<<~END_OF_QUERY, {:prid => prid})
        MATCH (pu)-[rt:IS_PARTICIPANT]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)
        WHERE COALESCE(p.deleted, false) = false
        AND COALESCE(pr.deleted, false) = false
        AND COALESCE(rt.deleted, false) = false
        RETURN labels(pu), pu.email, pu.name
    END_OF_QUERY
    participants = Hash[participants.map do |x|
        [x['pu.email'], x['pu.name'] || (@@user_info[x['pu.email']] || {})[:display_name] || 'NN']
    end]
    poll = temp['p']
    poll_run = temp['pr']
    poll[:organizer] = (@@user_info[temp['au.email']] || {})[:display_last_name] || temp['au.email']
    poll_run[:items] = JSON.parse(poll_run[:items])
    poll_run[:participant_count] = temp['participant_count']
    poll_run[:participants] = participants
    responses = neo4j_query(<<~END_OF_QUERY, {:prid => prid}).map { |x| {:response => JSON.parse(x['prs.response']), :email => x['u.email']} }
        MATCH (u)<-[:RESPONSE_BY]-(prs:PollResponse)-[:RESPONSE_TO]->(pr:PollRun {id: $prid})-[:RUNS]->(p:Poll)
        WHERE (u:User OR u:ExternalUser OR u:PredefinedExternalUser)
        RETURN u.email, prs.response;
    END_OF_QUERY
    responses.sort! { |a, b| participants[a] <=> participants[b] }
    return poll, poll_run, responses
end

#get_sessions_for_user(email) ⇒ Object



500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'src/ruby/include/login.rb', line 500

def get_sessions_for_user(email)
    require_user!
    sessions = neo4j_query(<<~END_OF_QUERY, :email => email).map { |x| x['s'] }
        MATCH (s:Session)-[:BELONGS_TO]->(u:User {email: $email})
        RETURN s
        ORDER BY s.last_access DESC;
    END_OF_QUERY
    sessions.map do |s|
        s[:scrambled_sid] = Digest::SHA2.hexdigest(SESSION_SCRAMBLER + s[:sid]).to_i(16).to_s(36)[0, 16]
        s[:method] ||= 'email'
        s
    end
end

#get_sign_ups_for_public_event(event_key) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'src/ruby/include/public_event.rb', line 82

def get_sign_ups_for_public_event(event_key)
    event = @@public_event_config.select { |x| x[:key] == event_key }.first
    assert(!event.nil?)
    tracks = []
    event[:rows].each do |_row|
        _row[:entries].each do |_entry|
            tracks << "#{event_key}/#{_entry[:key]}"
        end
    end

    entries = neo4j_query(<<~END_OF_QUERY, :tracks => tracks).map { |x| {:person => x['n'], :track => x['t.track']} }
        MATCH (n:PublicEventPerson)-[:SIGNED_UP_FOR]->(t:PublicEventTrack)
        WHERE t.track in $tracks
        RETURN t.track, n
        ORDER BY n.timestamp ASC;
    END_OF_QUERY
    result = {}
    entries.each do |entry|
        result[entry[:track].sub("#{event[:key]}/", '')] ||= []
        result[entry[:track].sub("#{event[:key]}/", '')] << entry[:person]
    end
    result
end

#get_technikamtObject



11
12
13
14
15
16
17
18
# File 'src/ruby/include/techpost.rb', line 11

def get_technikamt
    results = neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User)-[:HAS_AMT {amt: 'technikamt'}]->(v:Techpost)
        RETURN u.email;
    END_OF_QUERY
    debug results
    return results.map { |result| result["u.email"] }
end

#get_test_regime_htmlObject



826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
# File 'src/ruby/include/salzh.rb', line 826

def get_test_regime_html()
    StringIO.open do |io|
        [true, false].each do |regular_test_day|
            [true, false].each do |regular_test_required|
                [:salzh, :contact_person, :hotspot_klasse, nil].each do |status|
                    io.puts "<tr>"
                    io.puts "<td><span style='position: relative; top: -1px;' class='salzh-badge salzh-badge-big bg-#{SALZH_MODE_COLORS[status]}'><i class='fa #{SALZH_MODE_ICONS[status]}'></i></span>#{SALZH_MODE_LABEL[status] || '&ndash;'}</td>"
                    io.puts "<td>#{regular_test_required ? 'notwendig' : 'nicht notwendig'}</td>"
                    io.puts "<td>#{regular_test_day ? 'ja' : 'nein'}</td>"
                    label_type = Main.get_test_list_label_type(status, regular_test_required, regular_test_day)
                    if label_type == :enabled
                        io.puts "<td>Vorname Nachname</td>"
                    elsif label_type == :disabled
                        io.puts "<td style='color: #aaa;'>(Vorname Nachname)</td>"
                    elsif label_type == :strike
                        io.puts "<td><s style='color: #000;'>Vorname Nachname</s></td>"
                    else
                        io.puts "<td>???</td>"
                    end
                    io.puts "</tr>"
                end
            end
        end
        io.string
    end
end

#get_unread_messages(now) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'src/ruby/include/message.rb', line 116

def get_unread_messages(now)
    require_user!
    # don't show messages which are not at least 5 minutes old
    rows = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email], :now => now).map { |x| x['c.id'] }
        MATCH (c)-[ruc:TO]->(u:User {email: $email}) 
        WHERE ((c:TextComment AND EXISTS(c.comment)) OR 
                (c:AudioComment AND EXISTS(c.tag)) OR
                (c:Message AND EXISTS(c.id))) AND
                c.updated < $now AND COALESCE(ruc.seen, false) = false
        RETURN c.id
    END_OF_QUERY
    rows
end

#get_voluntary_testing_susObject



339
340
341
# File 'src/ruby/include/salzh.rb', line 339

def get_voluntary_testing_sus
    Main.get_voluntary_testing_sus
end

#gev_logged_in?Boolean

Returns true if GEV is logged in.

Returns:

  • (Boolean)


96
97
98
# File 'src/ruby/include/user.rb', line 96

def gev_logged_in?
    user_logged_in? && (GEV_USERS.include?(@session_user[:email]) || admin_logged_in?)
end

#good_bad_icon(flag) ⇒ Object



10
11
12
13
14
15
16
# File 'src/ruby/include/admin.rb', line 10

def good_bad_icon(flag)
    if flag == true
        "<i class='fa fa-check text-success'></i>"
    elsif flag == false
        "<i class='fa fa-warning text-danger'></i>"
    end
end

#hack_contentObject



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'src/ruby/include/hack.rb', line 163

def hack_content 
    require_user!
    parts = request.env['REQUEST_PATH'].split('/')
    provided_password = (parts[2] || '').strip.downcase
    provided_password = nil if provided_password.empty?
    result = neo4j_query_expect_one(<<~END_OF_QUERY, {:email => @session_user[:email]})
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.hack_level, 0) AS hack_level, 
        u.hack_seed AS hack_seed,
        COALESCE(u.failed_tries, 0) AS failed_tries,
        COALESCE(u.hack_name, '') AS hack_name;
    END_OF_QUERY
    @hack_level = result['hack_level']
    @hack_seed = result['hack_seed']
    @hack_name = result['hack_name'].strip
    failed_tries = result['failed_tries']
    tries_left = 3 - failed_tries
    if @hack_seed.nil? || (@hack_level == 0 && provided_password.nil?)
        @hack_seed = Time.now.to_i
        result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email], :hack_seed => @hack_seed})
            MATCH (u:User {email: $email})
            SET u.hack_seed = $hack_seed;
        END_OF_QUERY
    end
    
    get_next_password()

    STDERR.puts "HACK // #{@session_user[:email]} // level: #{@hack_level}, next password: #{@hack_next_password}#{@hack_next_password.nil? ? '(nil)':''}"

    unless provided_password.nil? || @hack_next_password.nil?
        if provided_password.downcase == @hack_next_password.downcase
            @hack_level += 1
            result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email], :hack_level => @hack_level})
                MATCH (u:User {email: $email})
                SET u.hack_level = $hack_level,
                u.failed_tries = 0;
            END_OF_QUERY
        else
            result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]})
                MATCH (u:User {email: $email})
                SET u.failed_tries = COALESCE(u.failed_tries, 0) + 1;
            END_OF_QUERY
            if tries_left <= 1
                result = neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]})
                    MATCH (u:User {email: $email})
                    SET u.hack_level = 0, u.failed_tries = 0, u.hack_name = '';
                END_OF_QUERY
            end
        end
    end
    unless provided_password.nil?                
        redirect "#{WEB_ROOT}/h4ck", 302
    end

    get_next_password()

    StringIO.open do |io|
        # io.puts "<p style='text-align: left; margin-top: 0;'><b>#{@session_user[:first_name]}</b> &lt;#{@session_user[:email]}&gt;</p>"
        if @hack_level == MAX_HACK_LEVEL
            # io.puts "<p style='float: right; margin-top: 0;'><a href='/hackers'>=&gt; Hall of Fame</a></p>"
            io.puts File.read('/static/hack/hall_of_fame.html')
            result = neo4j_query(<<~END_OF_QUERY)
                MATCH (u:User)
                WHERE COALESCE(u.hack_level, 0) > 0
                RETURN u.email, u.hack_level, COALESCE(u.hack_name, '') AS hack_name
            END_OF_QUERY
            io.puts "<p>"
            io.puts "Bisher #{result.size == 1 ? 'hat' : 'haben'} <b>#{result.size} Hacker:in#{result.size == 1 ? '' : 'nen'}</b> versucht, die Aufgaben zu lösen."
            histogram = {}
            names = []
            result.each do |row|
                histogram[row['u.hack_level']] ||= []
                histogram[row['u.hack_level']] << row['u.email']
                names << row['hack_name'] unless row['hack_name'].strip.empty?
            end
            names.sort!
            parts = []
            histogram.keys.sort.each do |level|
                l = 'in der <b>Hall of Fame</b>'
                if level + 1 <= MAX_HACK_LEVEL
                    l = "in <b>Level #{level + 1}</b>"
                end
                parts << "<b>#{histogram[level].size} Person#{histogram[level].size == 1 ? '' : 'en'}</b> #{l}"
            end
            io.puts "Davon befinde#{histogram[histogram.keys.sort.first].size == 1 ? 't' : 'n'} sich #{join_with_sep(parts, ', ', ' und ')}."
            io.puts "</p>"
            io.puts "<hr style='margin-bottom: 15px;'/>"
            # io.puts "<p>"
            # io.puts "Hier kannst du festlegen, ob und wie du in der Hall of Fame erscheinen möchtest:"
            # io.puts "</p>"
            # io.puts "<hr />"
            possible_names = []
            possible_names << @session_user[:first_name]
            unless @session_user[:teacher]
                possible_names << "#{@session_user[:first_name]} (#{@session_user[:klasse]})"
            end
            possible_names << @session_user[:display_name]
            unless @session_user[:teacher]
                possible_names << "#{@session_user[:display_name]} (#{@session_user[:klasse]})"
            else
                possible_names << "#{@session_user[:display_last_name]}"
            end

            io.puts "<p style='text-align: left; margin-top: 0;'>"
            io.puts "<span class='name-pref' data-name=''>[#{@hack_name == '' ? 'x': ' '}] Ich möchte <b>nicht</b> aufgelistet werden.</span><br />"
            possible_names.each do |name|
                io.puts "<span class='name-pref' data-name=\"#{name}\">[#{@hack_name == name ? 'x': ' '}] Ich möchte als <b>»#{name}«</b> erscheinen.</span><br />"
            end
            io.puts "<span class='hack-reset text-danger'>[!] Ich möchte meinen <b>Fortschritt löschen</b> und von vorn beginnen.</span>"
            io.puts "<span class='text-danger' id='hack_reset_confirm' style='display: none;'><br />&nbsp;&nbsp;&nbsp;&nbsp;Bist du sicher? <span id='hack_reset_confirm_yes'><b>[Ja]</b></span> <span id='hack_reset_confirm_no'><b>[Nein]</b></span></span>"
            io.puts "</p>"
            io.puts "<h2><b>Hall of Fame</b></h2>"
            names.each do |name|
                io.puts "<p class='name'>#{name}</p>"
            end
            io.puts File.read('/static/hack/hall_of_fame_foot.html')
        else
            io.puts File.read('/static/hack/title.html').gsub('#{next_hack_level}', (@hack_level + 1).to_s)
            if failed_tries == 0
                if @hack_level > 0
                    io.puts "<p><b>Gut gemacht!</b></p>"
                    io.puts "<hr />"
                end
            else
                if @hack_level > 0
                    tries_left = 3 - failed_tries
                    io.puts "<p class='text-danger'><b>Achtung!</b> Deine letzte Antwort war leider falsch. Du hast noch <b>#{tries_left} Versuch#{tries_left == 1 ? '' : 'e'}</b>, bevor du wieder von vorn beginnen musst.</p>"
                    io.puts "<hr />"
                else
                    io.puts "<p class='text-danger'><b>Achtung!</b> Deine letzte Antwort war leider falsch.</p>"
                    io.puts "<hr />"
                end
            end
            path = "/static/hack/level_#{@hack_level + 1}.html"
            if File.exist?(path)
                io.puts File.read(path)
            end
            io.puts File.read('/static/hack/form.html')
        end
        io.string
    end
end

#helloObject



6
7
8
# File 'src/ruby/pry.rb', line 6

def hello
    puts 'hello, world!'
end

#hex_to_rgb(c) ⇒ Object



55
56
57
58
59
60
# File 'src/ruby/include/color.rb', line 55

def hex_to_rgb(c)
    r = c[1, 2].downcase.to_i(16)
    g = c[3, 2].downcase.to_i(16)
    b = c[5, 2].downcase.to_i(16)
    [r, g, b]
end

#hh_mm_to_i(s) ⇒ Object



198
199
200
201
# File 'src/ruby/include/tablet_set.rb', line 198

def hh_mm_to_i(s)
    parts = s.split(':').map { |x| x.to_i }
    parts[0] * 60 + parts[1]
end

#hsv_to_rgb(c) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# File 'src/ruby/include/color.rb', line 2

def hsv_to_rgb(c)
    h, s, v = c[0].to_f / 360, c[1].to_f / 100, c[2].to_f / 100
    h_i = (h * 6).to_i
    f = h * 6 - h_i
    p = v * (1 - s)
    q = v * (1 - f * s)
    t = v * (1 - (1 - f) * s)
    r, g, b = v, t, p if h_i == 0
    r, g, b = q, v, p if h_i == 1
    r, g, b = p, v, t if h_i == 2
    r, g, b = p, q, v if h_i == 3
    r, g, b = t, p, v if h_i == 4
    r, g, b = v, p, q if h_i == 5
    [(r * 255).to_i, (g * 255).to_i, (b * 255).to_i]
end

#html_to_rgb(x) ⇒ Object



97
98
99
# File 'src/ruby/include/color.rb', line 97

def html_to_rgb(x)
    [x[1, 2].to_i(16), x[3, 2].to_i(16), x[5, 2].to_i(16)]
end

#htmlentities(s) ⇒ Object



1604
1605
1606
1607
# File 'src/ruby/main.rb', line 1604

def htmlentities(s)
    @html_entities_coder ||= HTMLEntities.new
    @html_entities_coder.encode(s)
end

#img_multi_attr_lazy(path, extension, resolutions, lazy = true) ⇒ Object



173
174
175
176
177
178
# File 'src/ruby/include/website.rb', line 173

def img_multi_attr_lazy(path, extension, resolutions, lazy = true)
    h = img_multi_attr_lazy_hash(path, extension, resolutions, lazy)
    h.keys.map do |k|
        "#{k}='#{h[k]}'"
    end.join(' ')
end

#img_multi_attr_lazy_hash(path, extension, resolutions, lazy = true) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'src/ruby/include/website.rb', line 149

def img_multi_attr_lazy_hash(path, extension, resolutions, lazy = true)
    if resolutions[:cols]
        resolutions[:lg] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:lg].to_f * resolutions[:cols] / 12).to_i}px"
        resolutions[:md] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:md].to_f * resolutions[:cols] / 12).to_i}px"
        resolutions[:sm] ||= "#{(@@BOOTSTRAP_BREAKPOINTS[:sm].to_f * resolutions[:cols] / 12).to_i}px"
        resolutions[:xs] ||= "100vw"
    end
    srcset_entries = @@GEN_IMAGE_WIDTHS.map do |w|
        "#{path}-#{w}.#{extension} #{w}w"
    end
    sizes_entries = @@BOOTSTRAP_BREAKPOINTS.map do |key, min_width|
        entry = resolutions[key]
        [:xs, :sm, :md, :lg].each do |k|
            entry ||= resolutions[k]
        end
        entry ||= '100vw'
        "(min-width: #{min_width}px) #{entry}"
    end
    {
        'srcset' => srcset_entries.join(', '),
        'sizes' => sizes_entries.join(', ')
    }
end

#iterate_directory(which, first_key = :last_name, second_key = :first_name, &block) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'src/ruby/include/directory.rb', line 410

def iterate_directory(which, first_key = :last_name, second_key = :first_name, &block)
    email_list = @@schueler_for_klasse[which]
    if email_list.nil?
        # try lesson key
        email_list = @@schueler_for_lesson[which]
    end
    email_list ||= []
    (email_list).sort do |a, b|
        (@@user_info[a][first_key] == @@user_info[b][first_key]) ?
        (@@user_info[a][second_key] <=> @@user_info[b][second_key]) :
        (@@user_info[a][first_key] <=> @@user_info[b][first_key])
    end.each.with_index do |email, i|
        yield email, i
    end
end

#iterate_school_days(options = {}, &block) ⇒ Object



435
436
437
# File 'src/ruby/main.rb', line 435

def iterate_school_days(options = {}, &block)
    self.class.iterate_school_days(options, &block)
end

#klasse_for_susObject



459
460
461
462
463
464
465
466
467
468
# File 'src/ruby/include/user.rb', line 459

def klasse_for_sus
    require_user_who_can_manage_tablets_or_teacher!
    result = {}
    @@user_info.each_pair do |email, info|
        next if info[:teacher]
        next unless info[:klasse]
        result[email] = Main.tr_klasse(info[:klasse])
    end
    result
end

#klassen_for_session_userObject



416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'src/ruby/include/user.rb', line 416

def klassen_for_session_user()
    require_teacher!
    if can_see_all_timetables_logged_in?
        @@klassen_order.dup
    else
        klassen = []
        @@klassen_order.each do |klasse|
            next unless (@@klassen_for_shorthand[@session_user[:shorthand]] || Set.new()).include?(klasse)
            klassen << klasse
        end
        klassen
    end
end

#klassenleiter_for_klasse_logged_in?(klasse) ⇒ Boolean

Returns true if a klassenleiter for a given klasse is logged in.

Returns:

  • (Boolean)


136
137
138
139
# File 'src/ruby/include/user.rb', line 136

def klassenleiter_for_klasse_logged_in?(klasse)
    return false unless @@klassenleiter[klasse]
    teacher_logged_in? && @@klassenleiter[klasse].include?(@session_user[:shorthand])
end

#klassenleiter_for_klasse_or_admin_logged_in?(klasse) ⇒ Boolean

Returns true if a klassenleiter for a given klasse is logged in.

Returns:

  • (Boolean)


142
143
144
145
# File 'src/ruby/include/user.rb', line 142

def klassenleiter_for_klasse_or_admin_logged_in?(klasse)
    return false unless @@klassenleiter[klasse]
    admin_logged_in? || (teacher_logged_in? && @@klassenleiter[klasse].include?(@session_user[:shorthand]))
end

#klassenraum_logged_in?Boolean

Returns true if a kurs tablet is logged in.

Returns:

  • (Boolean)


121
122
123
# File 'src/ruby/include/user.rb', line 121

def klassenraum_logged_in?
    user_logged_in? && @session_user[:is_tablet] && @session_user[:tablet_type] == :klassenraum
end

#kurs_tablet_logged_in?Boolean

Returns true if a kurs tablet is logged in.

Returns:

  • (Boolean)


116
117
118
# File 'src/ruby/include/user.rb', line 116

def kurs_tablet_logged_in?
    user_logged_in? && @session_user[:is_tablet] && @session_user[:tablet_type] == :kurs
end

#lessons_for_session_user_and_klasse(klasse) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'src/ruby/include/user.rb', line 430

def lessons_for_session_user_and_klasse(klasse)
    require_teacher!
    faecher = Set.new()
    (@@lessons_for_shorthand[@session_user[:shorthand]] || []).each do |lesson_key|
        lesson_info = @@lessons[:lesson_keys][lesson_key]
        next unless lesson_info[:klassen].include?(klasse)
        faecher << lesson_info[:fach]
    end
    faecher = faecher.to_a.sort do |a, b|
        a = @@faecher[a] if @@faecher[a]
        b = @@faecher[b] if @@faecher[b]
        a <=> b
    end
    {:fach_order => faecher, :fach_tr => @@faecher}
end

#line_for_lang(lang) ⇒ Object



184
185
186
187
188
189
190
191
192
# File 'src/ruby/include/cypher.rb', line 184

def line_for_lang(lang)
    [
        "Das naechste Loesungswort lautet #{lang}",
        "Das Passwort lautet #{lang}",
        "Versuch es mal mit #{lang}",
        "Wenn du #{lang} eingibst dann sollte es klappen",
        "#{lang} ist das naechste Passwort"
    ].sample
end

#logoutObject



342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'src/ruby/include/login.rb', line 342

def logout()
    sid = request.cookies['sid']
    if sid =~ /^[0-9A-Za-z,]+$/
        current_sid = sid.split(',').first
        if current_sid =~ /^[0-9A-Za-z]+$/
            result = neo4j_query(<<~END_OF_QUERY, :sid => current_sid)
                MATCH (s:Session {sid: $sid})
                DETACH DELETE s;
            END_OF_QUERY
        end
    end
    purge_missing_sessions()
end

#luminance(color) ⇒ Object



62
63
64
65
# File 'src/ruby/include/color.rb', line 62

def luminance(color) 
    r, g, b = hex_to_rgb(color)
    return r * 0.299 + g * 0.587 + b * 0.114
end

#mail_addresses_table(klasse) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'src/ruby/include/directory.rb', line 2

def mail_addresses_table(klasse)
    require_teacher!
    klassenleiter_logged_in = (@@klassenleiter[klasse] || []).include?(@session_user[:shorthand]) || admin_logged_in?
    all_homeschooling_users = Main.get_all_homeschooling_users()
    salzh_status = Main.get_salzh_status_for_emails(Main.class_variable_get(:@@schueler_for_klasse)[klasse] || [])
    StringIO.open do |io|
        io.puts "<div class='row'>"
        io.puts "<div class='col-md-12'>"
        # io.puts "<div class='alert alert-warning'>"
        # io.puts "Bitte überprüfen Sie die <strong>Gruppenzuordnung (A/B)</strong> und markieren Sie alle Kinder, die aus gesundheitlichen Gründen / Quarantäne nicht in die Schule kommen können, als <strong>»zu Hause«</strong>."
#             io.puts "Auf die Jitsi-Streams können momentan nur SuS zugreifen, die laut ihrer Gruppenzuordnung in der aktuellen Woche zu Hause sind oder explizit als »zu Hause« markiert sind."
        # io.puts "</div>"
        io.puts "<div class='pull-right' style='position: relative; top: 10px;'>"
        [:salzh, :contact_person, :hotspot_klasse].each do |status|
            salzh_label = "<span style='margin-left: 2em;'><span class='salzh-badge salzh-badge-big bg-#{SALZH_MODE_COLORS[status]}'><i class='fa #{SALZH_MODE_ICONS[status]}'></i></span>&nbsp;#{SALZH_MODE_LABEL[status]}</span>"
            io.puts salzh_label
        end
        io.puts "</div>"
        io.puts "<h3>Klasse #{tr_klasse(klasse)}</h3>"
        io.puts "<p>"
        io.puts "</p>"
        # <div style='max-width: 100%; overflow-x: auto;'>
        # <table class='table' style='width: unset; min-width: 100%;'>

        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='klassen_table table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Nr.</th>"
        io.puts "<th></th>"
        io.puts "<th>Name</th>"
        io.puts "<th>Vorname</th>"
        io.puts "<th>Geburtsdatum</th>"
        # io.puts "<th>Status</th>"
        # if can_manage_salzh_logged_in?
        #     io.puts "<th>Reguläre Testung</th>"
        #     io.puts "<th>Freiwilliges saLzH bis</th>"
        # end
        # if klassenleiter_logged_in
        #     io.puts "<th>Freiwillige Testung</th>"
        # end
        io.puts "<th>E-Mail-Adresse</th>"
        # io.puts "<th style='width: 140px;'>Homeschooling</th>"
        if ['11', '12'].include?(klasse)
            io.puts "<th>Antikenfahrt</th>"
        end
        io.puts "<th>A/B</th>"
        io.puts "<th>Letzter Zugriff</th>"
        io.puts "<th>Eltern-E-Mail-Adresse</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        results = neo4j_query(<<~END_OF_QUERY, :email_addresses => @@schueler_for_klasse[klasse])
            MATCH (u:User)
            WHERE u.email IN $email_addresses
            RETURN u.email, u.last_access, COALESCE(u.group2, 'A') AS group2, COALESCE(u.group_af, '') AS group_af;
        END_OF_QUERY
        last_access = {}
        group2_for_email = {}
        group_af_for_email = {}
        results.each do |x|
            last_access[x['u.email']] = x['u.last_access']
            group2_for_email[x['u.email']] = x['group2']
            group_af_for_email[x['u.email']] = x['group_af']
        end

        (@@schueler_for_klasse[klasse] || []).sort do |a, b|
            (@@user_info[a][:last_name] == @@user_info[b][:last_name]) ?
            (@@user_info[a][:first_name] <=> @@user_info[b][:first_name]) :
            (@@user_info[a][:last_name] <=> @@user_info[b][:last_name])
        end.each.with_index do |email, _|
            record = @@user_info[email]
            io.puts "<tr class='user_row' data-email='#{email}'>"
            io.puts "<td>#{_ + 1}.</td>"
            io.puts "<td>#{user_icon(email, 'avatar-md')}</td>"
            salzh_style = ''
            salzh_class = ''
            if salzh_status[email] && [:contact_person, :salzh].include?(salzh_status[email][:status])
                salzh_style = 'padding: 2px 4px; margin: -2px -4px; display: inline-block; border-radius: 4px;'
                salzh_class = "bg-#{SALZH_MODE_COLORS[(salzh_status[email] || {})[:status]]}"
            end
            io.puts "<td><div class='#{salzh_class}' style='#{salzh_style}'>#{record[:last_name]}</div></td>"
            io.puts "<td><div class='#{salzh_class}' style='#{salzh_style}'>#{record[:first_name]}</div></td>"
            io.puts "<td>#{Date.parse(record[:geburtstag]).strftime('%d.%m.%Y')}</td>"
            # salzh_label = ''
            # if salzh_status[email][:status]
            #     salzh_label = "<span class='salzh-badge salzh-badge-big bg-#{SALZH_MODE_COLORS[(salzh_status[email] || {})[:status]]}'><i class='fa #{SALZH_MODE_ICONS[(salzh_status[email] || {})[:status]]}'></i></span>&nbsp;&nbsp;bis #{Date.parse(salzh_status[email][:status_end_date]).strftime('%d.%m.')}"

            #     # salzh_label = "<div class='bg-#{SALZH_MODE_COLORS[(salzh_status[email] || {})[:status]]}' style='text-align: center; padding: 4px; margin: -4px; border-radius: 4px;'><i class='fa #{SALZH_MODE_ICONS[(salzh_status[email] || {})[:status]]}'></i>&nbsp;&nbsp;bis #{Date.parse(salzh_status[email][:status_end_date]).strftime('%d.%m.')}</div>"
            # end
            # io.puts "<td>#{salzh_label}</td>"
            # if can_manage_salzh_logged_in?
            #     io.puts "<td>"
            #     testing_required = salzh_status[email][:testing_required]
            #     if testing_required
            #         io.puts "<button class='btn btn-sm btn-success bu_toggle_testing_required'><i class='fa fa-check'></i>&nbsp;&nbsp;notwendig</button>"
            #     else
            #         io.puts "<button class='btn btn-sm btn-outline-secondary bu_toggle_testing_required'><i class='fa fa-times'></i>&nbsp;&nbsp;nicht notwendig</button>"
            #     end
            #     io.puts "</td>"

            #     io.puts "<td>"
            #     freiwillig_salzh = salzh_status[email][:freiwillig_salzh]
            #     io.puts "<div class='input-group'><input type='date' class='form-control ti_freiwillig_salzh' value='#{freiwillig_salzh}' /><div class='input-group-append'><button #{freiwillig_salzh.nil? ? 'disabled' : ''} class='btn #{freiwillig_salzh.nil? ? 'btn-outline-secondary' : 'btn-danger'} bu_delete_freiwillig_salzh'><i class='fa fa-trash'></i></button></div></div>"
            #     io.puts "</td>"
            # end
            # if klassenleiter_logged_in
            #     io.puts "<td>"
            #     voluntary_testing = salzh_status[email][:voluntary_testing]
            #     if voluntary_testing
            #         io.puts "<button class='btn btn-sm btn-success bu_toggle_voluntary_testing'><i class='fa fa-check'></i>&nbsp;&nbsp;nimmt teil</button>"
            #     else
            #         io.puts "<button class='btn btn-sm btn-outline-secondary bu_toggle_voluntary_testing'><i class='fa fa-times'></i>&nbsp;&nbsp;nimmt nicht teil</button>"
            #     end
            #     io.puts "</td>"
            # end
            io.puts "<td>"
            print_email_field(io, record[:email])
            io.puts "</td>"
            homeschooling_button_disabled = klassenleiter_logged_in ? '' : 'disabled'
            # if all_homeschooling_users.include?(email)
            #     io.puts "<td><button #{homeschooling_button_disabled} class='btn btn-info btn-xs btn-toggle-homeschooling' data-email='#{email}'><i class='fa fa-home'></i>&nbsp;&nbsp;zu Hause</button></td>"
            # else
            #     io.puts "<td><button #{homeschooling_button_disabled} class='btn btn-secondary btn-xs btn-toggle-homeschooling' data-email='#{email}'><i class='fa fa-building'></i>&nbsp;&nbsp;Präsenz</button></td>"
            # end
            if ['11', '12'].include?(klasse)
                io.puts "<td><div class='group-af-button #{user_who_can_manage_antikenfahrt_logged_in? ? '' : 'disabled'}' data-email='#{email}'>#{GROUP_AF_ICONS[group_af_for_email[email]]}</div></td>"
            end
            io.puts "<td><div class='group2-button group2-#{group2_for_email[email]}' data-email='#{email}'>#{group2_for_email[email]}</div></td>"
            la_label = 'noch nie angemeldet'
            today = Date.today.to_s
            if last_access[email]
                days = (Date.today - Date.parse(last_access[email])).to_i
                if days == 0
                    la_label = 'heute'
                elsif days == 1
                    la_label = 'gestern'
                elsif days == 2
                    la_label = 'vorgestern'
                elsif days == 3
                    la_label = 'vor 3 Tagen'
                elsif days == 4
                    la_label = 'vor 4 Tagen'
                elsif days == 5
                    la_label = 'vor 5 Tagen'
                elsif days == 6
                    la_label = 'vor 6 Tagen'
                elsif days < 14
                    la_label = 'vor 1 Woche'
                elsif days < 21
                    la_label = 'vor 2 Wochen'
                elsif days < 28
                    la_label = 'vor 3 Wochen'
                elsif days < 35
                    la_label = 'vor 4 Wochen'
                else
                    la_label = 'vor mehreren Wochen'
                end
            end
            io.puts "<td>#{la_label}</td>"
            io.puts "<td>"
            print_email_field(io, "eltern.#{record[:email]}")
            io.puts "</td>"
            io.puts "</tr>"
        end
        io.puts "<tr>"
        io.puts "<td colspan='3'></td>"
        io.puts "<td colspan='2'><b>E-Mail an die Klasse #{tr_klasse(klasse)}</b></td>"
        io.puts "<td></td>"
        io.puts "<td colspan='3'><b>E-Mail an alle Eltern der Klasse #{tr_klasse(klasse)}</b></td>"
        io.puts "</tr>"
        io.puts "<tr class='user_row'>"
        io.puts "<td colspan='3'></td>"
        io.puts "<td colspan='2'>"
        print_email_field(io, "klasse.#{klasse}@#{SCHUL_MAIL_DOMAIN}")
        io.puts "</td>"
        io.puts "<td></td>"
        io.puts "<td colspan='3'>"
        print_email_field(io, "eltern.#{klasse}@#{SCHUL_MAIL_DOMAIN}")
        io.puts "</td>"
        io.puts "</tr>"
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<a class='btn btn-primary' href='/show_login_codes/#{klasse}'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Live-Anmeldungen der Klasse zeigen</a>"
        # io.puts print_stream_restriction_table(klasse)
        io.puts "<hr style='margin: 3em 0;'/>"
        io.puts "<h3>Schülerlisten Klasse #{tr_klasse(klasse)}</h3>"
#             io.puts "<div style='text-align: center;'>"
        io.puts "<a href='/api/directory_xlsx/#{klasse}' class='btn btn-primary'><i class='fa fa-file-excel-o'></i>&nbsp;&nbsp;Excel-Tabelle herunterladen</a>"
        io.puts "<a href='/api/directory_timetex_pdf/by_last_name/#{klasse}' class='btn btn-primary'><i class='fa fa-file-pdf-o'></i>&nbsp;&nbsp;Timetex-PDF herunterladen</a>"
        io.puts "<a href='/api/directory_timetex_pdf/by_first_name/#{klasse}' class='btn btn-primary'><i class='fa fa-file-pdf-o'></i>&nbsp;&nbsp;Timetex-PDF herunterladen (nach Vornamen sortiert)</a>"
        io.puts "<a href='/api/directory_json/#{klasse}' class='btn btn-primary'><i class='fa fa-file-code-o'></i>&nbsp;&nbsp;JSON herunterladen</a>"
        io.puts "<hr style='margin: 3em 0;'/>"
        io.puts "<h3>Stundenpläne der Klasse #{tr_klasse(klasse)} zum Ausdrucken</h3>"
        io.puts "<a href='/api/get_timetable_pdf_for_klasse/#{klasse}' target='_blank' class='btn btn-primary'><i class='fa fa-file-pdf-o'></i>&nbsp;&nbsp;PDF herunterladen (#{@@schueler_for_klasse[klasse].size} Seiten)</a>"
        io.puts "<hr style='margin: 3em 0;'/>"
        io.puts "<h3>Lehrer der Klasse #{tr_klasse(klasse)}</h3>"
        io.puts "<div class='table-responsive'>"
        io.puts "<table class='table table-condensed table-striped narrow'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Kürzel</th>"
        io.puts "<th>Name</th>"
        io.puts "<th>Fächer (Wochenstunden)</th>"
        io.puts "<th>E-Mail-Adresse</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        old_is_klassenleiter = true
        (@@teachers_for_klasse[klasse] || {}).keys.sort do |a, b|
            name_comp = begin
                @@user_info[@@shorthands[a]][:last_name] <=> @@user_info[@@shorthands[b]][:last_name]
            rescue
                a <=> b
            end

            a_kli = (@@klassenleiter[klasse] || []).index(a)
            b_kli = (@@klassenleiter[klasse] || []).index(b)
            
            if a_kli.nil?
                if b_kli.nil?
                    name_comp
                else
                    1
                end
            else
                if b_kli.nil?
                    -1
                else
                    a_kli <=> b_kli
                end
            end
        end.each do |shorthand|
            lehrer = @@user_info[@@shorthands[shorthand]]
            next if lehrer.nil?
            is_klassenleiter = (@@klassenleiter[klasse] || []).include?(shorthand)
            
            if old_is_klassenleiter && !is_klassenleiter
                io.puts "<tr class='sep user_row'>"
            else
                io.puts "<tr class='user_row'>"
            end
            old_is_klassenleiter = is_klassenleiter
            io.puts "<td>#{shorthand}#{is_klassenleiter ? ' (KL)' : ''}</td>"
#                 io.puts "<td>#{((lehrer[:titel] || '') + ' ' + (lehrer[:last_name] || shorthand)).strip}</td>"
            io.puts "<td>#{lehrer[:display_name] || ''}</td>"
            hours = @@teachers_for_klasse[klasse][shorthand].keys.sort do |a, b|
                @@teachers_for_klasse[klasse][shorthand][b] <=> @@teachers_for_klasse[klasse][shorthand][a]
            end.map do |x|
                fach = x.gsub('.', '')
                fach = @@faecher[fach] if @@faecher[fach]
                "#{fach} (#{@@teachers_for_klasse[klasse][shorthand][x]})"
            end.join(', ')
            io.puts "<td>#{hours}</td>"
            if lehrer.empty?
                io.puts "<td></td>"
            else
                io.puts "<td>"
                print_email_field(io, lehrer[:email])
                io.puts "</td>"
            end
            io.puts "</tr>"
        end
        io.puts "<tr>"
        io.puts "<td colspan='3'></td>"
        io.puts "<td><b>E-Mail an alle Lehrer/innen der Klasse #{tr_klasse(klasse)}</b></td>"
        io.puts "</tr>"
        io.puts "<tr class='user_row'>"
        io.puts "<td colspan='3'></td>"
        io.puts "<td>"
        print_email_field(io, "lehrer.#{klasse}@#{SCHUL_MAIL_DOMAIN}")
        io.puts "</td>"
        io.puts "</tr>"
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.puts "</div>"
        io.string
    end
end

#matrix_get(path, access_token = nil) ⇒ Object



32
33
34
# File 'src/ruby/include/matrix.rb', line 32

def matrix_get(path, access_token = nil)
    matrix_request(:get, path, nil, access_token)
end

#matrix_login(user, password) {|access_token| ... } ⇒ Object

Yields:

  • (access_token)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'src/ruby/include/matrix.rb', line 40

def (user, password, &block)
    # login
    response = matrix_post("/_matrix/client/r0/login", {
        :type => 'm.login.password',
        :user => user,
        :password => password
    })
    access_token = response['access_token'] || ''
    assert(!access_token.empty?)

    yield(access_token)

    matrix_post("/_matrix/client/r0/logout", {}, access_token)
end

#matrix_post(path, data = {}, access_token = nil) ⇒ Object



36
37
38
# File 'src/ruby/include/matrix.rb', line 36

def matrix_post(path, data = {}, access_token = nil)
    matrix_request(:post, path, data, access_token)
end

#matrix_request(method, path, data = {}, access_token = nil) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'src/ruby/include/matrix.rb', line 3

def matrix_request(method, path, data = {}, access_token = nil)
    assert([:get, :post].include?(method))
    response = nil
    success = false
    methods = {:get => Curl.method(:get),
               :post => Curl.method(:post)}
    3.times do
        c = methods[method].call("https://#{MATRIX_DOMAIN}#{path}", data.nil? ? nil : data.to_json) do |http|
            http.headers['Authorization'] = "Bearer #{access_token}" if access_token
        end
        begin
            response = JSON.parse(c.body_str)
        rescue JSON::ParserError => e
            STDERR.puts c.body_str
            raise e
        end
        if response['retry_after_ms']
            sleep response['retry_after_ms'].to_f / 1000.0
        else
            success = true
            break
        end
    end
    unless success
        raise "unable to complete matrix_request: #{path} / #{data.to_json}"
    end
    response
end

#may_edit_lessons?(lesson_key) ⇒ Boolean

Returns:

  • (Boolean)


2237
2238
2239
# File 'src/ruby/main.rb', line 2237

def may_edit_lessons?(lesson_key)
    teacher_logged_in? && (@@lessons_for_shorthand[@session_user[:shorthand]].include?(lesson_key) || (@@lessons[:historic_lessons_for_shorthand][@session_user[:shorthand]].include?(lesson_key)))
end

#mix(a, b, t) ⇒ Object



67
68
69
70
71
72
# File 'src/ruby/include/color.rb', line 67

def mix(a, b, t)
    t1 = 1.0 - t
    return [a[0] * t1 + b[0] * t,
            a[1] * t1 + b[1] * t,
            a[2] * t1 + b[2] * t]
end

#monitor_logged_in?Boolean

Returns true if a kurs tablet is logged in.

Returns:

  • (Boolean)


126
127
128
# File 'src/ruby/include/user.rb', line 126

def monitor_logged_in?
    user_logged_in? && @session_user[:is_monitor]
end


1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
# File 'src/ruby/main.rb', line 1668

def nav_items(primary_color, now, new_messages_count)
    if tablet_logged_in?
        tablet_id = @session_user[:tablet_id]
        tablet = @@tablets[tablet_id] || {}
        tablet_id_span = "<span class='tablet-id-indicator' style='background-color: #{tablet[:bg_color]}; color: #{tablet[:fg_color]}'>#{tablet_id}</span>"
        if teacher_tablet_logged_in?
            return "<div style='margin-right: 15px;'><b>Lehrer-Tablet-Modus</b>#{tablet_id_span}</div>"
        elsif kurs_tablet_logged_in?
            return "<div style='margin-right: 15px;'><b>Kurs-Tablet-Modus</b>#{tablet_id_span}</div>"
        elsif klassenraum_logged_in?
            return "<div style='margin-right: 15px;'><b>Klassenraum-Modus</b>#{tablet_id_span}</div>"
        elsif tablet_logged_in?
            description = ''
            if tablet[:klassen_stream]
                description = " (Klassenstreaming #{tablet[:klassen_stream]})"
            end
            if @session_user[:tablet_type] == :bib_mobile
                return ""
            else
                return "<div style='margin-right: 15px;'><b>Tablet-Modus</b>#{description}#{tablet_id_span}</div>"
            end
        end
    end
    if monitor_logged_in?
        return ''
    end
    StringIO.open do |io|
        new_messages_count_s = nil
        nav_items = []
        if user_logged_in?
            nav_items << ['/', 'Stundenplan', 'fa fa-calendar']
            if teacher_logged_in?
                nav_items << :kurse
                nav_items << :directory
            end
            # if user_who_can_upload_files_logged_in? || user_who_can_manage_news_logged_in?
            #     nav_items << :website
            # end
            # if user_who_can_manage_monitors_logged_in?
            #     nav_items << :monitor
            # end
            nav_items << :messages
            if user_is_eligible_for_tresor?
                nav_items << :tresor
            end
            if admin_logged_in? || user_who_can_upload_files_logged_in? || user_who_can_manage_news_logged_in? || user_who_can_manage_monitors_logged_in? || user_who_can_manage_tablets_logged_in?
                nav_items << :admin
            end
            if technikteam_logged_in?
                unless admin_logged_in?
                    nav_items << :aula
                end
            end
            if user_who_can_report_tech_problems_logged_in?
                unless admin_logged_in?
                    nav_items << :techteam
                end
            end
            # nav_items << :advent_calendar #if advents_calendar_date_today > 0
            nav_items << :profile
            new_messages_count_s = new_messages_count.to_s
            new_messages_count_s = '99+' if new_messages_count > 99
            if new_messages_count > 0
                io.puts "<a href='/messages' class='new-messages-indicator-mini'><i class='fa fa-comment' style='color: #{primary_color};'></i><span>#{new_messages_count_s}</span></a>"
            end
        else
            nav_items << ['/hilfe', 'Hilfe', 'fa fa-question-circle']
            nav_items << ['/', 'Anmelden', 'fa fa-sign-in']
        end
        if external_user_logged_in?
            nav_items = []
            if can_manage_bib_payment_logged_in?
                nav_items << ['/', 'Lehrmittelverein', 'fa fa-book']
                nav_items << :profile
            end
        end
        return nil if nav_items.empty?
        io.puts "<button class='navbar-toggler' type='button' data-toggle='collapse' data-target='#navbarTogglerDemo02' aria-controls='navbarTogglerDemo02' aria-expanded='false' aria-label='Toggle navigation'>"
        io.puts "<span class='navbar-toggler-icon'></span>"
        io.puts "</button>"
        io.puts "<div class='collapse navbar-collapse my-0 flex-grow-0' id='navbarTogglerDemo02'>"
        io.puts "<ul class='navbar-nav mr-auto'>"
        nav_items.each do |x|
            if x == :admin
                io.puts "<li class='nav-item dropdown'>"
                io.puts "<a class='nav-link nav-icon dropdown-toggle' href='#' id='navbarDropdownAdmin' role='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>"
                io.puts "<div class='icon'><i class='fa fa-wrench'></i></div>Administration"
                io.puts "</a>"
                io.puts "<div class='dropdown-menu dropdown-menu-right' aria-labelledby='navbarDropdownAdmin'>"
                printed_something = false
                if user_who_can_manage_news_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/manage_news'><div class='icon'><i class='fa fa-newspaper-o'></i></div><span class='label'>News verwalten</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/manage_calendar'><div class='icon'><i class='fa fa-calendar'></i></div><span class='label'>Termine verwalten</span></a>"
                    # io.puts "<a class='dropdown-item nav-icon' href='/anmeldungen'><div class='icon'><i class='fa fa-group'></i></div><span class='label'>Anmeldungen einsehen</span></a>"
                    printed_something = true
                end
                if user_who_can_upload_files_logged_in?
                    io.puts "<div class='dropdown-divider'></div>" if printed_something
                    io.puts "<a class='dropdown-item nav-icon' href='/upload_images'><div class='icon'><i class='fa fa-photo'></i></div><span class='label'>Bilder hochladen</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/upload_files'><div class='icon'><i class='fa fa-file-pdf-o'></i></div><span class='label'>Dateien hochladen</span></a>"
                    printed_something = true
                end
                if user_who_can_manage_monitors_logged_in?
                    io.puts "<div class='dropdown-divider'></div>" if printed_something
                    io.puts "<a class='dropdown-item nav-icon' href='/manage_monitor'><div class='icon'><i class='fa fa-tv'></i></div><span class='label'>Monitore verwalten</span></a>"
                    printed_something = true
                end
                # if user_who_can_manage_ags_logged_in?
                #     io.puts "<div class='dropdown-divider'></div>" if printed_something
                #     io.puts "<a class='dropdown-item nav-icon' href='/manage_ags'><div class='icon'><i class='fa fa-community'></i></div><span class='label'>AGs verwalten</span></a>"
                #     printed_something = true
                # end
                if admin_logged_in?
                    io.puts "<div class='dropdown-divider'></div>" if printed_something
                    io.puts "<a class='dropdown-item nav-icon' href='/admin'><div class='icon'><i class='fa fa-wrench'></i></div><span class='label'>Administration</span></a>"
                end
                if user_who_can_manage_tablets_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/bookings'><div class='icon'><i class='fa fa-tablet'></i></div><span class='label'>Buchungen</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/techpostadmin'><div class='icon'><i class='fa fa-laptop'></i></div><span class='label'>Technikamt (Admin)</span></a>"
                end
                if admin_logged_in?
                    io.puts "<div class='dropdown-divider'></div>"
                    io.puts "<a class='dropdown-item nav-icon' href='/show_all_login_codes'><div class='icon'><i class='fa fa-key-modern'></i></div><span class='label'>Live-Anmeldungen</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/email_accounts'><div class='icon'><i class='fa fa-envelope'></i></div><span class='label'>E-Mail-Postfächer</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/stats'><div class='icon'><i class='fa fa-bar-chart'></i></div><span class='label'>Statistiken</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/aula'><div class='icon'><i class='fa fa-music'></i></div><span class='label'>Aula</span></a>"
                    printed_something = true
                end
                io.puts "</div>"
                io.puts "</li>"
            elsif x == :advent_calendar
                unless admin_logged_in?
                    io.puts "<li class='nav-item text-nowrap'>"
                    io.puts "<a class='bu-launch-adventskalender nav-link nav-icon'><div class='icon'><i class='fa fa-snowflake-o'></i></div>Adventskalender</a>"
                    io.puts "</li>"
                end
            elsif x == :profile
                io.puts "<li class='nav-item dropdown'>"
                io.puts "<a class='nav-link nav-icon dropdown-toggle' href='#' id='navbarDropdownProfile' role='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>"
                display_name = htmlentities(@session_user[:display_name])
                if @session_user[:klasse]
                    temp = [tr_klasse(@session_user[:klasse])]
                    # if @session_user[:group2]
                    #     temp << @session_user[:group2]
                    # end
                    display_name += " (#{temp.join('/')})"
                end
                io.puts "<div class='icon nav_avatar'>#{user_icon(@session_user[:email], 'avatar-md')}</div><span class='menu-user-name'>#{display_name}</span>"
                io.puts "</a>"
                io.puts "<div class='dropdown-menu dropdown-menu-right' aria-labelledby='navbarDropdownProfile'>"
                unless external_user_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/profil'><div class='icon'>#{user_icon(@session_user[:email], 'avatar-sm')}</div><span class='label'>Profil</span></a>"
                end
                sessions = all_sessions()
                if sessions.size > 1
                    unless external_user_logged_in?
                        io.puts "<div class='dropdown-divider'></div>"
                    end
                    sessions[1, sessions.size - 1].each.with_index do |entry, _|
                        display_name = htmlentities(entry[:user][:display_name])
                        if entry[:user][:klasse]
                            display_name += " (#{tr_klasse(entry[:user][:klasse])})"
                        end
                        io.puts "<a class='dropdown-item nav-icon switch-session' data-sidindex='#{_ + 1}' href='#'><div class='icon'>#{user_icon(entry[:user][:email], 'avatar-sm')}</div><span class='label'>#{display_name}</span></a>"
                    end
                end
                io.puts "<a class='dropdown-item nav-icon' href='/login'><div class='icon'><i class='fa fa-sign-in'></i></div><span class='label'>Zusätzliche Anmeldung…</span></a>"
                unless external_user_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/login_nc'><div class='icon'><i class='fa fa-nextcloud'></i></div><span class='label'>In Nextcloud anmelden…</span></a>"
                    # if can_manage_agr_app_logged_in? || can_manage_bib_members_logged_in? || can_manage_bib_logged_in? || teacher_logged_in?
                        io.puts "<div class='dropdown-divider'></div>"
                        if gev_logged_in?
                            io.puts "<a class='dropdown-item nav-icon' href='/gev'><div class='icon'><i class='fa fa-users'></i></div><span class='label'>Gesamtelternvertretung</span></a>"
                        end
                        if can_manage_agr_app_logged_in?
                            io.puts "<a class='dropdown-item nav-icon' href='/agr_app'><div class='icon'><i class='fa fa-mobile'></i></div><span class='label'>Altgriechisch-App</span></a>"
                        end
                        if can_manage_bib_members_logged_in?
                            io.puts "<a class='dropdown-item nav-icon' href='/lehrbuchverein'><div class='icon'><i class='fa fa-book'></i></div><span class='label'>Lehrmittelverein</span></a>"
                        end
                        # if can_manage_bib_logged_in? || teacher_logged_in?
                            io.puts "<a class='dropdown-item nav-icon' href='/bibliothek'><div class='icon'><i class='fa fa-book'></i></div><span class='label'>Bibliothek</span></a>"
                        # end
                    # end
                    if teacher_or_sv_logged_in?
                        io.puts "<div class='dropdown-divider'></div>"
                        if teacher_or_sv_logged_in?
                            if teacher_logged_in?
                                if can_manage_salzh_logged_in?
                                    io.puts "<a class='dropdown-item nav-icon' href='/salzh'><div class='icon'><i class='fa fa-home'></i></div><span class='label'>Testungen</span></a>"
                                end
                                io.puts "<a class='dropdown-item nav-icon' href='/tests'><div class='icon'><i class='fa fa-file-text-o'></i></div><span class='label'>Klassenarbeiten</span></a>"
                            end
                            io.puts "<a class='dropdown-item nav-icon' href='/events'><div class='icon'><i class='fa fa-calendar-check-o'></i></div><span class='label'>Termine</span></a>"
                            io.puts "<a class='dropdown-item nav-icon' href='/polls'><div class='icon'><i class='fa fa-bar-chart'></i></div><span class='label'>Umfragen</span></a>"
                            io.puts "<a class='dropdown-item nav-icon' href='/prepare_vote'><div class='icon'><i class='fa fa-hand-paper-o'></i></div><span class='label'>Abstimmungen</span></a>"
                            io.puts "<a class='dropdown-item nav-icon' href='/mailing_lists'><div class='icon'><i class='fa fa-envelope'></i></div><span class='label'>E-Mail-Verteiler</span></a>"
                            io.puts "<a class='dropdown-item nav-icon' href='/groups'><div class='icon'><i class='fa fa-group'></i></div><span class='label'>Gruppen</span></a>"
                        end
                    end
                    # if @session_user[:can_upload_vplan]
                    #     io.puts "<div class='dropdown-divider'></div>"
                    #     io.puts "<a class='dropdown-item nav-icon' href='/upload_vplan_html'><div class='icon'><i class='fa fa-upload'></i></div><span class='label'>Vertretungsplan hochladen</span></a>"
                    # end
                    io.puts "<div class='dropdown-divider'></div>"
                    # if true
                    #     io.puts "<a class='dropdown-item nav-icon' href='/h4ck'><div class='icon'><i class='fa fa-rocket'></i></div><span class='label'>Dashboard Hackers</span></a>"
                    # end
                    # if admin_logged_in?
                    #     io.puts "<a class='bu-launch-adventskalender dropdown-item nav-icon'><div class='icon'><i class='fa fa-snowflake-o'></i></div><span class='label'>Adventskalender</span></a>"
                    # end
                end
                io.puts "<a class='dropdown-item nav-icon' href='/hilfe'><div class='icon'><i class='fa fa-question-circle'></i></div><span class='label'>Hilfe</span></a>"
                io.puts "<div class='dropdown-divider'></div>"
                io.puts "<a class='dropdown-item nav-icon' href='#' onclick='perform_logout();'><div class='icon'><i class='fa fa-sign-out'></i></div><span class='label'>Abmelden</span></a>"
                io.puts "</div>"
                io.puts "</li>"
            elsif x == :kurse
                unless (@@lessons_for_shorthand[@session_user[:shorthand]] || []).empty? && (@@lessons[:historic_lessons_for_shorthand][@session_user[:shorthand]] || []).empty?
                    io.puts "<li class='nav-item dropdown'>"
                    io.puts "<a class='nav-link nav-icon dropdown-toggle' href='#' id='navbarDropdownKurse' role='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>"
                    io.puts "<div class='icon'><i class='fa fa-address-book'></i></div>Kurse"
                    io.puts "</a>"
                    io.puts "<div class='dropdown-menu' aria-labelledby='navbarDropdownKurse'>"
                    taken_lesson_keys = Set.new()
                    (@@lessons_for_shorthand[@session_user[:shorthand]] || []).each do |lesson_key|
                        lesson_info = @@lessons[:lesson_keys][lesson_key]
                        if lesson_info
                            fach = lesson_info[:fach]
                            fach = @@faecher[fach] if @@faecher[fach]
                            io.puts "<a class='dropdown-item nav-icon' href='/lessons/#{CGI.escape(lesson_key)}'><div class='icon'><i class='fa fa-address-book'></i></div><span class='label'>#{lesson_info[:pretty_folder_name]}</span></a>"
                            taken_lesson_keys << lesson_key
                        end
                    end
                    remaining_lesson_keys = ((@@lessons[:historic_lessons_for_shorthand][@session_user[:shorthand]] || Set.new()) - taken_lesson_keys)
                    unless remaining_lesson_keys.empty?
                        lesson_keys_with_data = neo4j_query(<<~END_OF_QUERY, {:lesson_keys => remaining_lesson_keys}).map { |x| x['l.key'] }
                            MATCH (l:Lesson)
                            WHERE l.key IN $lesson_keys
                            RETURN l.key;
                        END_OF_QUERY
                        remaining_lesson_keys &= Set.new(lesson_keys_with_data)
                        unless remaining_lesson_keys.empty?
                            io.puts "<div class='dropdown-divider'></div>"
                            remaining_lesson_keys.to_a.sort.each do |lesson_key|
                                lesson_info = @@lessons[:lesson_keys][lesson_key]
                                if lesson_info
                                    fach = lesson_info[:fach]
                                    fach = @@faecher[fach] if @@faecher[fach]
                                    io.puts "<a class='dropdown-item nav-icon' href='/lessons/#{CGI.escape(lesson_key)}'><div class='icon'><i class='fa fa-address-book'></i></div><span class='label'>#{fach} (#{lesson_info[:klassen].map { |x| tr_klasse(x) }.join(', ')})</span></a>"
                                end
                            end
                        end
                    end
                    io.puts "</div>"
                    io.puts "</li>"
                end
            elsif x == :directory
                klassen = @@klassen_for_shorthand[@session_user[:shorthand]] || []
                if user_who_can_manage_antikenfahrt_logged_in?
                    klassen << '11'
                    klassen << '12'
                    klassen.uniq!
                end
                unless klassen.empty?
                    io.puts "<li class='nav-item dropdown'>"
                    io.puts "<a class='nav-link nav-icon dropdown-toggle' href='#' id='navbarDropdownKlassen' role='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>"
                    io.puts "<div class='icon'><i class='fa fa-address-book'></i></div>Klassen"
                    io.puts "</a>"
                    io.puts "<div class='dropdown-menu' aria-labelledby='navbarDropdownKlassen'>"
                    if can_see_all_timetables_logged_in?
                        klassen = @@klassen_order
                    end
                    klassen.each do |klasse|
                        io.puts "<a class='dropdown-item nav-icon' href='/directory/#{klasse}'><div class='icon'><i class='fa fa-address-book'></i></div><span class='label'>Klasse #{tr_klasse(klasse)}</span></a>"
                    end
                    io.puts "</div>"
                    io.puts "</li>"
                end
            elsif x == :techteam
                io.puts "<li class='nav-item text-nowrap'>"
                io.puts "<a class='nav-link nav-icon' href='/techpost'><div class='icon'><i class='fa fa-laptop'></i></div>Technikamt</a>"
            elsif x == :aula
                io.puts "<li class='nav-item dropdown'>"
                io.puts "<a class='nav-link nav-icon dropdown-toggle' href='#' id='navbarDropdownAula' role='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>"
                io.puts "<div class='icon'><i class='fa fa-music'></i></div>Aulatechnik"
                io.puts "</a>"
                io.puts "<div class='dropdown-menu dropdown-menu-right' aria-labelledby='navbarDropdownAula'>"
                io.puts "<a class='dropdown-item nav-icon' href='/aula_light'><div class='icon'><i class='fa fa-lightbulb-o'></i></div><span class='label'>Licht</span></a>"
                io.puts "<a class='dropdown-item nav-icon' style='display: none;' href='/aula_sound'><div class='icon'><i class='fa fa-music'></i></div><span class='label'>Ton</span></a>"
                io.puts "<a class='dropdown-item nav-icon' href='/aula_progress'><div class='icon'><i class='fa fa-bars'></i></div><span class='label'>Ablauf</span></a>"
                io.puts "</div>"
                io.puts "</li>"
            elsif x == :messages
                io.puts "<li class='nav-item text-nowrap'>"
                if new_messages_count > 0
                    io.puts "<a class='nav-link nav-icon' href='/messages'><div class='icon'><i class='fa fa-comment'></i></div>Nachrichten<span class='new-messages-indicator' style='background-color: #{primary_color}'><span>#{new_messages_count_s}</span></span></a>"
                else
                    io.puts "<a class='nav-link nav-icon' href='/messages'><div class='icon'><i class='fa fa-comment'></i></div>Nachrichten</a>"
                end
                io.puts "</li>"
            elsif x == :tresor
                io.puts "<li class='nav-item text-nowrap'>"
                io.puts "<a class='nav-link nav-icon' href='/tresor'><div class='icon'><i class='fa fa-database'></i></div>Datentresor</a>"
                io.puts "</li>"
            else
                io.puts "<li class='nav-item text-nowrap'>"
                io.puts "<a class='nav-link nav-icon' href='#{x[0]}' #{x[3]}><div class='icon'><i class='#{x[2]}'></i></div>#{x[1]}</a>"
                io.puts "</li>"
            end
        end
        io.puts "</ul>"
        io.puts "</div>"
        io.string
    end
end

#parse_paths_and_values(paths, values) ⇒ Object



293
294
295
296
297
298
299
# File 'src/ruby/include/zeugnisse.rb', line 293

def parse_paths_and_values(paths, values)
    result = {}
    recurse_arrays(paths, values) do |path, value|
        result[path] = value
    end
    result
end

#parse_request_data(options = {}) ⇒ Object



1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
# File 'src/ruby/main.rb', line 1360

def parse_request_data(options = {})
    options[:max_body_length] ||= 512
    options[:max_string_length] ||= 512
    options[:required_keys] ||= []
    options[:optional_keys] ||= []
    options[:max_value_lengths] ||= {}
    data_str = request.body.read(options[:max_body_length]).to_s
    if @session_user
        unless ['/api/send_message', '/api/update_message', '/api/submit_poll_run'].include?(request.path)
            begin
                ip_short = request.ip.to_s.split('.').map { |x| sprintf('%02x', x.to_i) }.join('')
                STDERR.puts sprintf("%s [%s] [%s] %s %s", DateTime.now.strftime('%Y-%m-%d %H:%M:%S'), ip_short, @session_user[:nc_login], request.path, data_str)
            rescue
            end
        end
    end
#         debug data_str
    @latest_request_body = data_str.dup
    begin
        assert(data_str.is_a? String)
        assert(data_str.size < options[:max_body_length], 'too_much_data')
        data = JSON::parse(data_str)
        @latest_request_body_parsed = data.dup
        result = {}
        options[:required_keys].each do |key|
            assert(data.include?(key.to_s))
            test_request_parameter(data, key, options)
            result[key.to_sym] = data[key.to_s]
        end
        options[:optional_keys].each do |key|
            if data.include?(key.to_s)
                test_request_parameter(data, key, options)
                result[key.to_sym] = data[key.to_s]
            end
        end
        result
    rescue
        debug "Request was:"
        debug data_str
        raise
    end
end

#pick_random_color_scheme(go_wild = false) ⇒ Object



2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
# File 'src/ruby/main.rb', line 2259

def pick_random_color_scheme(go_wild = false)
    today = Date.today.strftime('%Y-%m-%d')
    return 'la2c6e80d60aea2c6e80' if ZEUGNISKONFERENZEN.include?(today)
    @@default_color_scheme ||= {}
    jd = (Date.today + 1).jd
    return @@default_color_scheme[jd] if @@default_color_scheme[jd]
    unless go_wild
        srand(DEVELOPMENT ? (Time.now.to_f * 1000).to_i : jd)
    end
    which = nil
    style = nil
    while true do
        which = @@color_scheme_colors.sample
        style = [0].sample
        break unless which[4] == 'd' || which[1] == '#ff0040'
    end
    color_scheme = "#{which[4]}#{which[0, 3].join('').gsub('#', '')}#{style}"
    @@default_color_scheme[jd] = color_scheme unless DEVELOPMENT
    return color_scheme
end

#poll_run_results_to_html(poll, poll_run, responses, target = :web, only_this_email = nil) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'src/ruby/include/poll.rb', line 202

def poll_run_results_to_html(poll, poll_run, responses, target = :web, only_this_email = nil)
    StringIO.open do |io|
        unless only_this_email
            io.puts "<h3>Umfrage: #{poll[:title]}</h3>"
            time_range = "am #{Date.parse(poll_run[:start_date]).strftime('%d.%m.%Y')} von #{poll_run[:start_time]} Uhr bis #{poll_run[:end_time]} Uhr"
            if poll_run[:start_date] != poll_run[:end_date]
                time_range = "vom #{Date.parse(poll_run[:start_date]).strftime('%d.%m.%Y')} (#{poll_run[:start_time]} Uhr) bis zum #{Date.parse(poll_run[:end_date]).strftime('%d.%m.%Y')} (#{poll_run[:end_time]} Uhr)"
            end
            io.puts "<p>Diese #{poll_run[:anonymous] ? 'anonyme' : 'personengebundene'} Umfrage wurde von #{poll[:organizer].sub('Herr ', 'Herrn ')} mit #{poll_run[:participant_count]} Teilnehmern #{time_range} durchgeführt.</p>"
            io.puts "<div class='alert alert-info'>"
            io.puts "Von #{poll_run[:participant_count]} Teilnehmern haben #{responses.size} die Umfrage beantwortet (#{(responses.size * 100 / poll_run[:participant_count]).to_i}%)."
            unless poll_run[:anonymous]
                missing_responses_from = (Set.new(poll_run[:participants].keys) - Set.new(responses.map { |x| x[:email]})).map { |x| poll_run[:participants][x] }.sort
                io.puts "Es fehlen Antworten von: <em>#{missing_responses_from.join(', ')}</em>."
            end
            io.puts "</div>"
            poll_run[:items].each_with_index do |item, item_index|
                item = item.transform_keys(&:to_sym)
                if item[:type] == 'paragraph'
                    io.puts "<p><strong>#{item[:title]}</strong></p>" unless (item[:title] || '').strip.empty?
                    io.puts "<p>#{item[:text]}</p>" unless (item[:text] || '').strip.empty?
                elsif item[:type] == 'radio' || item[:type] == 'checkbox'
                    io.puts "<p>"
                    io.puts "<strong>#{item[:title]}</strong>"
                    if item[:type] == 'checkbox'
                        io.puts " <em>(Mehrfachnennungen möglich)</em>"
                    end
                    io.puts "</p>"
                    histogram = {}
                    participants_for_answer = {}
                    (0...item[:answers].size).each { |x| histogram[x] = 0 }
                    responses.each do |entry|
                        response = entry[:response]
                        if item[:type] == 'radio'
                            value = response[item_index.to_s]
                            unless value.nil?
                                unless histogram[value]
                                    STDERR.puts "Error evaluating poll: unknown value #{value} for #{item.to_json}!"
                                    next
                                end
                                histogram[value] += 1
                                participants_for_answer[value] ||= []
                                participants_for_answer[value] << entry[:email]
                            end
                        else
                            (response[item_index.to_s] || []).each do |value|
                                unless histogram[value]
                                    STDERR.puts "Error evaluating poll: unknown value #{value} for #{item.to_json}!"
                                    next
                                end
                                histogram[value] += 1
                                participants_for_answer[value] ||= []
                                participants_for_answer[value] << entry[:email]
                            end
                        end
                    end
                    sum = histogram.values.sum
                    sum = 1 if sum == 0
                    io.puts "<table class='table'>"
                    io.puts "<tbody>"
                    (0...item[:answers].size).each do |answer_index| 
                        v = histogram[answer_index]
                        io.puts "<tr class='pb-0'><td>#{item[:answers][answer_index]}</td><td style='text-align: right;'>#{v == 0 ? '&ndash;' : v}</td></tr>"
                        io.puts "<tr class='noborder pdf-space-below'><td colspan='2'>"
                        io.puts "<div class='progress'>"
                        io.puts "<div class='progress-bar progress-bar-striped bg-info' role='progressbar' style='width: #{(v * 100.0 / sum).round}%' aria-valuenow='50' aria-valuemin='0' aria-valuemax='100'><span>#{(v * 100.0 / sum).round}%</span></div>"
                        io.puts "</div>"
                        unless poll_run[:anonymous]
                            if participants_for_answer[answer_index]
                                io.puts "<em>#{(participants_for_answer[answer_index] || []).map { |x| poll_run[:participants][x]}.join(', ')}</em>"
                            else
                                io.puts "<em>&ndash;</em>"
                            end
                        end
                        io.puts "</td></tr>"
                    end
                    io.puts "</tbody>"
                    io.puts "</table>"
                elsif item[:type] == 'textarea'
                    io.puts "<p>"
                    io.puts "<strong>#{item[:title]}</strong>"
                    io.puts "</p>"
                    first_response = true
                    responses.each do |entry|
                        response = (entry[:response][item_index.to_s] || '').strip
                        unless response.empty?
                            io.puts "<hr />" unless first_response
                            if poll_run[:anonymous]
                                io.puts "<p>#{response}</p>"
                            else
                                io.puts "<p><em>#{poll_run[:participants][entry[:email]]}</em>: #{response}</p>"
                            end
                            first_response = false
                        end
                    end
                    
                end
            end
        end
        unless poll_run[:anonymous]
            responses.each do |entry|
                if only_this_email
                    next unless entry[:email] == only_this_email
                else
                    io.puts "<div class='page-break'></div>"
                end
                io.puts "<h3>Einzelauswertung: #{poll_run[:participants][entry[:email]]}</h3>"
                poll_run[:items].each_with_index do |item, item_index|
                    item = item.transform_keys(&:to_sym)
                    if item[:type] == 'paragraph'
                        io.puts "<p><strong>#{item[:title]}</strong></p>" unless (item[:title] || '').strip.empty?
                        io.puts "<p>#{item[:text]}</p>" unless (item[:text] || '').strip.empty?
                    elsif item[:type] == 'radio'
                        io.puts "<p>"
                        io.puts "<strong>#{item[:title]}</strong>"
                        io.puts "</p>"
                        answer = entry[:response][item_index.to_s]
                        unless answer.nil?
                            io.puts "<p>#{item[:answers][answer]}</p>"
                        end
                    elsif item[:type] == 'radio' || item[:type] == 'checkbox'
                        io.puts "<p>"
                        io.puts "<strong>#{item[:title]}</strong>"
                        io.puts " <em>(Mehrfachnennungen möglich)</em>"
                        io.puts "</p>"
                        unless entry[:response][item_index.to_s].nil?
                            io.puts "<p>"
                            io.puts entry[:response][item_index.to_s].reject { |x| x.nil? }.map { |answer| item[:answers][answer]}.join(', ')
                            io.puts "</p>"
                        end
                    elsif item[:type] == 'textarea'
                        io.puts "<p>"
                        io.puts "<strong>#{item[:title]}</strong>"
                        io.puts "</p>"
                        response = (entry[:response][item_index.to_s] || '').strip
                        io.puts "<p>#{response}</p>" unless response.empty?
                    end
                end
            end
        end
        
#             cm = {}
#             citems = (0...poll_run[:items].size).select do |item_index|
#                 item = poll_run[:items][item_index].transform_keys(&:to_sym)
#                 ['radio', 'checkbox'].include?(item[:type])
#             end.map { |x| x.to_s }
#             citems.each do |a|
#                 citems.each do |b|
#                     next if a == b
#                     total = 0
#                     matches = 0
#                     responses.each do |response|
#                         # now we're looking at responses by one person to questions a and b
#                         va = response[:response][a]
#                         vb = response[:response][b]
#                         va = [va] unless va.is_a? Array
#                         vb = [vb] unless vb.is_a? Array
#                         va.each do |za|
#                             key = "#{a}/#{za}"
#                             cm[key] ||= 0
#                             cm[key] += 1
#                             vb.each do |zb|
#                                 key = "#{a}/#{za}-#{b}/#{zb}"
#                                 cm[key] ||= 0
#                                 cm[key] += 1
#                             end
#                         end
#                     end
#                 end
#             end
#             cm_final = {}
#             cm.keys.each do |k|
#                 next unless k.include?('-')
#                 match = cm[k]
#                 total = cm[k.split('-').first]
#                 cm_final[k] = match * 100.0 / total
#             end
#             
#             use_keys = cm_final.keys.select do |x|
#                 cm_final[x] >= 30.0
#             end.sort do |a, b|
#                 cm_final[b] <=> cm_final[a]
#             end
#             unless use_keys.empty?
#                 io.puts "<div class='page-break'></div>"
#                 io.puts "<h3>Korrelationen</h3>"
#                 io.puts "<table class='table'>"
#                 use_keys.each do |k|
#                     k2 = k.split('-').map { |x| x.split('/') }.flatten
#                     qa = poll_run[:items][k2[0].to_i]['title']
#                     aa = poll_run[:items][k2[0].to_i]['answers'][k2[1].to_i]
#                     qb = poll_run[:items][k2[2].to_i]['title']
#                     ab = poll_run[:items][k2[2].to_i]['answers'][k2[3].to_i]
#                     io.puts "<tr><td style='vertical-align: top;'>#{sprintf('%3d%%', cm_final[k])}</td><td><strong>#{qa}</strong><br />#{aa}<br /><strong>#{qb}</strong><br />#{ab}</td></tr>"
#                 end
#                 io.puts "</table>"
#             end
        
        io.string
    end
end


579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'src/ruby/include/user.rb', line 579

def print_ad_hoc_2fa_panel()
    return '' unless admin_2fa_hotline_logged_in?
    require_admin_2fa_hotline!
    ts = Time.now.to_i
    neo4j_query(<<~END_OF_QUERY, {:ts => ts})
        MATCH (ahr:AdHocTwoFaRequest)-[:BELONGS_TO]->(s:Session)-[:BELONGS_TO]->(u:User)
        WHERE $ts > ahr.ts_expire
        DETACH DELETE ahr;
    END_OF_QUERY
    users = neo4j_query(<<~END_OF_QUERY).map { |x| x['u'] }
        MATCH (ahr:AdHocTwoFaRequest)-[:BELONGS_TO]->(s:Session)-[:BELONGS_TO]->(u:User)
        RETURN u;
    END_OF_QUERY
    return '' if users.empty?
    StringIO.open do |io|
        io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
        io.puts "<div class='hint'>"
        io.puts "<p><b>Datentresor-Hotline</b></p>"
        io.puts "<hr />"
        users.each do |user|
            io.puts "<button class='bu_open_ad_hoc_2fa_request button btn btn-success' data-email='#{user[:email]}' data-name='#{@@user_info[user[:email]][:display_name]}'><i class='fa fa-phone'></i>&nbsp;&nbsp;#{@@user_info[user[:email]][:display_name_official]}&nbsp;&nbsp;<i class='fa fa-angle-double-right'></i></button>"
        end
        # io.puts "<div><span style='font-size: 200%; opacity: 0.7; float: left; margin-right: 8px;'><i class='fa fa-book'></i></span>#{n_to_s[@@bib_summoned_books[email].size] || 'Mehrere'} Bücher, die du ausgeliehen hast, #{@@bib_summoned_books[email].size == 1 ? 'wird' : 'werden'} dringend in der Bibliothek benötigt. Bitte bring #{@@bib_summoned_books[email].size == 1 ? 'es' : 'sie'} zurück und lege #{@@bib_summoned_books[email].size == 1 ? 'es' : 'sie'} ins <a target='_blank' href='https://rundgang.gymnasiumsteglitz.de/#g114'>Rückgaberegal</a> vor der Bibliothek.</div>"
        io.puts "</div>"
        io.puts "</div>"
        io.string
    end
end


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'src/ruby/include/admin.rb', line 18

def print_admin_dashboard()
    require_admin!
    temp = neo4j_query(<<~END_OF_QUERY).map { |x| {:session => x['s'], :email => x['u.email'] } }
        MATCH (s:Session)-[:BELONGS_TO]->(u:User)
        RETURN s, u.email
    END_OF_QUERY
    all_sessions = {}
    temp.each do |s|
        all_sessions[s[:email]] ||= []
        all_sessions[s[:email]] << s[:session]
    end
    all_homeschooling_users = Main.get_all_homeschooling_users()
    users_with_telephone_number = Set.new(neo4j_query("MATCH (u:User) WHERE u.telephone_number IS NOT NULL RETURN u.email").map { |x| x['u.email']})
    users_with_otp = Set.new(neo4j_query("MATCH (u:User) WHERE u.otp_token IS NOT NULL RETURN u.email").map { |x| x['u.email']})
    twofa_status = {}
    (users_with_telephone_number | users_with_otp).each do |email|
        methods = []
        methods << "<i class='fa fa-mobile'></i>&nbsp;&nbsp;SMS" if users_with_telephone_number.include?(email)
        methods << "<i class='fa fa-qrcode'></i>&nbsp;&nbsp;OTP" if users_with_otp.include?(email)
        twofa_status[email] = methods.join(' / ')
    end
    StringIO.open do |io|
        bolt_connections = neo4j_query("CALL dbms.listConnections();").size
        io.puts "<span style='float: right;'>SMS Gateway: #{Main.sms_gateway_ready? ? 'online' : 'offline'} / Aktive Bolt-Verbindungen: #{bolt_connections} / <a href='/schema'>Schema</a></span>"
        io.puts "<a class='btn btn-secondary' href='#teachers'>Lehrerinnen und Lehrer</a>"
        io.puts "<a class='btn btn-secondary' href='#sus'>Schülerinnen und Schüler</a>"
        io.puts "<a class='btn btn-secondary' href='#website'>Website</a>"
        io.puts "<a class='btn btn-secondary' href='#tablets'>Tablets</a>"
        io.puts "<a class='btn btn-secondary' href='#monitor'>Monitor</a>"
        io.puts "<a class='btn btn-secondary' href='#bibliothek'>Bibliothek</a>"
        io.puts "<a class='btn btn-secondary' href='/sus_ohne_kurse'>SuS ohne Kurse</a>"
        io.puts "<a class='btn btn-secondary' href='/api/all_sus_logo_didact'>LDC: Alle SuS</a>"
        io.puts "<a class='btn btn-secondary' href='/api/all_lul_logo_didact'>LDC: Alle Lehrkräfte</a>"
        io.puts "<a class='btn btn-secondary' href='/api/all_sus_untis'>Untis: Alle SuS</a>"
        io.puts "<a class='btn btn-secondary' href='/api/all_kurse_untis'>Untis: Alle Kurse</a>"
        io.puts "<hr />"
        io.puts "<h3 id='teachers'>Lehrerinnen und Lehrer</h3>"
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        # io.puts "<th></th>"
        io.puts "<th>Kürzel</th>"
        io.puts "<th>Name</th>"
        io.puts "<th>Vorname</th>"
        io.puts "<th>E-Mail-Adresse</th>"
        io.puts "<th>Stundenplan</th>"
        io.puts "<th>Anmelden</th>"
        io.puts "<th>2FA</th>"
        io.puts "<th>Sessions</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        @@lehrer_order.each do |email|
            io.puts "<tr class='user_row'>"
            user = @@user_info[email]
            # io.puts "<td>#{user_icon(email, 'avatar-md')}</td>"
            io.puts "<td>#{user[:shorthand]}</td>"
            io.puts "<td>#{user[:last_name]}</td>"
            io.puts "<td>#{user[:first_name]}</td>"
            if USE_MOCK_NAMES
                io.puts "<td>#{user[:first_name].downcase}.#{user[:last_name].downcase}@#{SCHUL_MAIL_DOMAIN}</td>"
            else
                io.print "<td>"
                print_email_field(io, user[:email])
                io.puts "</td>"
            end
            io.puts "<td><a class='btn btn-xs btn-secondary' href='/timetable/#{user[:id]}'><i class='fa fa-calendar'></i>&nbsp;&nbsp;Stundenplan</a></td>"
            io.puts "<td><button class='btn btn-warning btn-xs btn-impersonate' data-impersonate-email='#{user[:email]}'><i class='fa fa-id-badge'></i>&nbsp;&nbsp;Anmelden</button></td>"
            io.puts "<td>#{twofa_status[email]}</td>"
            if all_sessions.include?(email)
                io.puts "<td><button class='btn-sessions btn btn-xs btn-secondary' data-sessions-id='#{@@user_info[email][:id]}'>#{all_sessions[email].size} Session#{all_sessions[email].size == 1 ? '' : 's'}</button></td>"
            else
                io.puts "<td></td>"
            end
            io.puts "</tr>"
            (all_sessions[email] || []).each do |s|
                scrambled_sid = Digest::SHA2.hexdigest(SESSION_SCRAMBLER + s[:sid]).to_i(16).to_s(36)[0, 16]
                io.puts "<tr class='session-row sessions-#{@@user_info[email][:id]}' style='display: none;'>"
                io.puts "<td colspan='4'></td>"
                io.puts "<td colspan='2'>"
                io.puts "#{s[:user_agent] || '(unbekanntes Gerät)'}"
                io.puts "</td>"
                io.puts "<td>"
                io.puts "<button class='btn btn-xs btn-danger btn-purge-session' data-email='#{email}' data-scrambled-sid='#{scrambled_sid}'>Abmelden</button>"
                io.puts "</td>"
                io.puts "</tr>"
            end
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<h3 id='sus'>Schülerinnen und Schüler</h3>"
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        # io.puts "<th></th>"
        io.puts "<th>Name</th>"
        io.puts "<th>Vorname</th>"
        io.puts "<th>E-Mail-Adresse</th>"
        io.puts "<th>Stundenplan</th>"
        io.puts "<th>Anmelden</th>"
        # io.puts "<th>Homeschooling</th>"
        io.puts "<th>2FA</th>"
        io.puts "<th>Sessions</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        @@klassen_order.each do |klasse|
            io.puts "<tr>"
            io.puts "<th colspan='7'>Klasse #{tr_klasse(klasse)}</th>"
            io.puts "</tr>"
            (@@schueler_for_klasse[klasse] || []).each do |email|
                io.puts "<tr class='user_row'>"
                user = @@user_info[email]
                # io.puts "<td>#{user_icon(email, 'avatar-md')}</td>"
                io.puts "<td>#{user[:last_name]}</td>"
                io.puts "<td>#{user[:first_name]}</td>"
                io.print "<td>"
                print_email_field(io, user[:email])
                io.puts "</td>"
                io.puts "<td><a class='btn btn-xs btn-secondary' href='/timetable/#{user[:id]}'><i class='fa fa-calendar'></i>&nbsp;&nbsp;Stundenplan</a></td>"
                io.puts "<td><button class='btn btn-warning btn-xs btn-impersonate' data-impersonate-email='#{user[:email]}'><i class='fa fa-id-badge'></i>&nbsp;&nbsp;Anmelden</button></td>"
                # if all_homeschooling_users.include?(email)
                #     io.puts "<td><button class='btn btn-info btn-xs btn-toggle-homeschooling' data-email='#{user[:email]}'><i class='fa fa-home'></i>&nbsp;&nbsp;zu Hause</button></td>"
                # else
                #     io.puts "<td><button class='btn btn-secondary btn-xs btn-toggle-homeschooling' data-email='#{user[:email]}'><i class='fa fa-building'></i>&nbsp;&nbsp;Präsenz</button></td>"
                # end
                io.puts "<td>#{twofa_status[email]}</td>"
                if all_sessions.include?(email)
                    io.puts "<td><button class='btn-sessions btn btn-xs btn-secondary' data-sessions-id='#{@@user_info[email][:id]}'>#{all_sessions[email].size} Session#{all_sessions[email].size == 1 ? '' : 's'}</button></td>"
                else
                    io.puts "<td></td>"
                end
                io.puts "</tr>"
                (all_sessions[email] || []).each do |s|
                    scrambled_sid = Digest::SHA2.hexdigest(SESSION_SCRAMBLER + s[:sid]).to_i(16).to_s(36)[0, 16]
                    io.puts "<tr class='session-row sessions-#{@@user_info[email][:id]}' style='display: none;'>"
                    io.puts "<td colspan='3'></td>"
                    io.puts "<td colspan='2'>"
                    io.puts "#{s[:user_agent] || '(unbekanntes Gerät)'}"
                    io.puts "</td>"
                    io.puts "<td>"
                    io.puts "<button class='btn btn-xs btn-danger btn-purge-session' data-email='#{email}' data-scrambled-sid='#{scrambled_sid}'>Abmelden</button>"
                    io.puts "</td>"
                    io.puts "</tr>"
                end
            end
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<hr />"
        io.puts "<h3 id='website'>Website</h3>"
        io.puts "<button class='btn btn-secondary bu-refresh-staging'><i id='refresh-icon-staging' class='fa fa-refresh'></i>&nbsp;&nbsp;Vorschau-Seite aktualisieren</button>"
        io.puts "<button class='btn btn-success bu-refresh-live'><i id='refresh-icon-live' class='fa fa-refresh'></i>&nbsp;&nbsp;Live-Seite aktualisieren</button>"
        io.puts "<hr />"
        io.puts "<h3 id='tablets'>Tablets</h3>"
        io.puts "<hr />"
        io.puts "<p>Mit einem Klick auf diesen Button können Sie dieses Gerät dauerhaft als Lehrer-Tablet anmelden.</p>"
        io.puts "<button class='btn btn-success bu_login_teacher_tablet'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Lehrer-Tablet-Modus aktivieren</button>"
        io.puts "<hr />"
        io.puts "<p>Bitte wählen Sie ein order mehrere Kürzel, um dieses Gerät dauerhaft als Kurs-Tablet anzumelden.</p>"
        @@shorthands.keys.sort.each do |shorthand|
            io.puts "<button class='btn-teacher-for-kurs-tablet-login btn btn-xs btn-outline-secondary' data-shorthand='#{shorthand}'>#{shorthand}</button>"
        end
        io.puts "<br /><br >"
        io.puts "<button class='btn btn-success bu_login_kurs_tablet' disabled><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Kurs-Tablet-Modus aktivieren</button>"
        io.puts "<hr />"
        io.puts "<p>Bitte wählen Sie ein Tablet, um dieses Gerät dauerhaft als dieses Tablet anzumelden.</p>"
        @@tablets.keys.each do |id|
            tablet = @@tablets[id]
            io.puts "<button class='btn-tablet-login btn btn-xs btn-outline-secondary' data-id='#{id}' style='background-color: #{tablet[:bg_color]}; color: #{tablet[:fg_color]};'>#{id}</button>"
        end
        io.puts "<hr />"
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Typ</th>"
        io.puts "<th>Gerät</th>"
        io.puts "<th>Abmelden</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        get_sessions_for_user("lehrer.tablet@#{SCHUL_MAIL_DOMAIN}").each do |session|
            io.puts "<tr>"
            io.puts "<td>Lehrer-Tablet</td>"
            io.puts "<td>#{session[:user_agent]}</td>"
            io.puts "<td><button class='btn btn-xs btn-danger btn-purge-session' data-email='lehrer.tablet@#{SCHUL_MAIL_DOMAIN}' data-scrambled-sid='#{session[:scrambled_sid]}'>Abmelden</button></td>"
            io.puts "</tr>"
        end
        get_sessions_for_user("kurs.tablet@#{SCHUL_MAIL_DOMAIN}").each do |session|
            io.puts "<tr>"
            io.puts "<td>Kurs-Tablet (#{(session[:shorthands] || []).sort.join(', ')})</td>"
            io.puts "<td>#{session[:user_agent]}</td>"
            io.puts "<td><button class='btn btn-xs btn-danger btn-purge-session' data-email='kurs.tablet@#{SCHUL_MAIL_DOMAIN}' data-scrambled-sid='#{session[:scrambled_sid]}'>Abmelden</button></td>"
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<h3 id='monitor'>Monitor</h3>"
        io.puts "<button class='btn btn-success bu-login-as-monitor'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als Flur-Monitor anmelden</button>"
        io.puts "<button class='btn btn-success bu-login-as-monitor-sek'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als Sek-Monitor anmelden</button>"
        io.puts "<button class='btn btn-success bu-login-as-monitor-lz'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als LZ-Monitor anmelden</button>"
        io.puts "<div style='margin-top: 4px;'>"
        io.puts "<button class='btn btn-sm bu-allow-zeugniskonferenzen-monitor-flur'>Flur-Monitor: Zeugniskonferenzen</button>"
        io.puts "<button class='btn btn-sm bu-allow-zeugniskonferenzen-monitor-sek'>Sek-Monitor: Zeugniskonferenzen</button>"
        io.puts "<button class='btn btn-sm bu-allow-zeugniskonferenzen-monitor-lz'>LZ-Monitor: Zeugniskonferenzen</button>"
        io.puts "</div>"
        io.puts "<hr />"
        io.puts "<h3 id='bibliothek'>Bibliothek</h3>"
        io.puts "<button class='btn btn-success bu-login-as-bib-mobile'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als Bibliotheks-Handy anmelden</button>"
        io.puts "<button class='btn btn-success bu-login-as-bib-station'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als Bibliotheks-Station anmelden</button>"
        io.puts "<button class='btn btn-success bu-login-as-bib-station-with-printer'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Als Bibliotheks-Station mit Labeldrucker anmelden</button>"
        io.puts "<hr />"
        io.string
    end
end


2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
# File 'src/ruby/main.rb', line 2300

def print_advent_calendar_css
    return '' unless user_logged_in?
    permutation = [18,2,7,5,8,23,21,15,10,14,11,20,4,9,17,0,19,3,1,12,6,22,16,13]
    xo = [1,2,3,4,0.5,1.5,2.5,3.5,4.5,0,1,2,3,4,5,0.5,1.5,2.5,3.5,4.5,1,2,3,4]
    yo = [0,0,0,0,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,4,4,4,4]
    images = advent_calendar_images
    StringIO.open do |io|
        (0..23).each do |k|
            i = permutation[k]
            image = images[i]
            image = nil if i > advents_calendar_date_today() - 1
            io.puts ".door.door#{i} {"
            io.puts "    left: #{xo[k] * 17.5 + 0.5}vh;"
            io.puts "    top: #{yo[k] * 17.5 + 0.1}vh;"
            io.puts "    width: 17.5vh;"
            io.puts "    height: 17.5vh;"
            io.puts "}"
            io.puts ".door.door#{i} .flip-card-front {"
            io.puts "    background-image: url(/images/advent-calendar/doors/tiles-#{i}.png);"
            io.puts "    background-size: cover;"
            io.puts "}"
            io.puts ".door.door#{i} .flip-card-back {"
            io.puts "    background-image: url(#{images[i]});"
            io.puts "    background-size: cover;"
            io.puts "}"
        end
        io.puts "@media (orientation: portrait) {"
            (0..23).each do |k|
                i = permutation[k]
                io.puts ".door.door#{i} {"
                io.puts "    left: #{yo[k] * 18.277 + 0.1044}vw;"
                io.puts "    top: #{xo[k] * 18.277 + 0.5222}vw;"
                io.puts "    width: 18.277vw;"
                io.puts "    height: 18.277vw;"
                io.puts "}"
            end
        io.puts "}"
        io.string
    end
end


2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
# File 'src/ruby/main.rb', line 2341

def print_advent_calendar_doors
    return '' unless user_logged_in?
    doors = get_open_doors_for_user()
    StringIO.open do |io|
        (0..23).each do |i|
            io.puts "<div data-door='#{i}' class='door door#{i} #{((doors >> i) & 1) > 0 ? 'open' : ''}'>"
            io.puts "<div class='flip-card-inner'>"
            io.puts "<div class='flip-card-front'></div>"
            io.puts "<div class='flip-card-back'></div>"
            io.puts "</div>"
            io.puts "</div>"
        end
        io.string
    end
end


2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
# File 'src/ruby/main.rb', line 2392

def print_adventskalender_sidepanel()
    require_user!
    StringIO.open do |io|
        io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
        io.puts "<div class='hint'>"
        io.puts "<div>Adventskalender</div>"
        io.puts "<hr />"
        io.puts "<button style='white-space: nowrap;' class='float-right btn btn-success bu-launch-adventskalender'>Adventskalender öffnen&nbsp;<i class='fa fa-angle-double-right'></i></button>"
        io.puts "<div style='clear: both;'></div>"
        io.puts "</div>"
        io.puts "</div>"
        io.string
    end
end


344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'src/ruby/include/admin.rb', line 344

def print_all_kurse_untis
    require_admin!
    StringIO.open do |io|
        count = 0
        @@klassen_order.each do |klasse|
            next unless ['11', '12'].include?(klasse)
            @@schueler_for_klasse[klasse].each do |email|
                @@kurse_for_schueler[email].each do |lesson_key|
                    count += 1
                    user = @@user_info[email]
                    parts = []
                    parts << "#{user[:last_name]}#{user[:first_name]}".gsub(' ', '').gsub('-', '').gsub(',', '')
                    parts << ""
                    parts << @@original_fach_for_lesson_key[lesson_key] || lesson_key
                    parts << ""
                    parts << "#{klasse}"
                    parts << ""
                    parts << ""
                    parts << ""
                    parts << ""
                    parts << ""
                    parts << @@original_fach_for_lesson_key[lesson_key] || lesson_key
                    parts << ""
                    parts << "1"
                    io.puts parts.map { |x| '"' + x + '"'}.join("\t")
                end
            end
        end
        io.string
    end
end


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'src/ruby/include/admin.rb', line 401

def print_all_lul_logo_didact
    require_admin!
    StringIO.open do |io|
        @@lehrer_order.sort do |a, b|
            au = @@user_info[a]
            bu = @@user_info[b]
            au[:last_name].downcase <=> bu[:last_name].downcase
        end.each do |email|
            user = @@user_info[email]
            shorthand = user[:shorthand]
            shorthand = 'Mand' if shorthand == 'Man'
            io.puts "#{user[:last_name]};#{user[:first_name].strip.empty? ? user[:last_name] : user[:first_name]};#{shorthand}"
        end
        path = '/data/lehrer/extra-ldc-accounts.csv'
        if File.exist?(path)
            File.open(path) do |f|
                f.each_line do |line|
                    io.puts line
                end
            end
        end
        io.string
    end
end


381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'src/ruby/include/admin.rb', line 381

def print_all_sus_logo_didact
    require_admin!
    StringIO.open do |io|
        @@klassen_order.each do |klasse|
            @@schueler_for_klasse[klasse].each do |email|
                user = @@user_info[email]
                next if user[:geburtstag].nil?
                geburtstag = "#{user[:geburtstag][8, 2]}.#{user[:geburtstag][5, 2]}.#{user[:geburtstag][0, 4]}"
                io.puts "#{user[:last_name]};#{user[:first_name]};#{user[:klasse]};#{user[:geschlecht]};#{geburtstag}"
            end
        end
        io.string
    end
end


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'src/ruby/include/admin.rb', line 307

def print_all_sus_untis
    require_admin!
    StringIO.open do |io|
        count = 0
        @@klassen_order.each do |klasse|
            @@schueler_for_klasse[klasse].each do |email|
                count += 1
                user = @@user_info[email]
                parts = []
                parts << "#{user[:last_name]}#{user[:first_name]}".gsub(' ', '').gsub('-', '').gsub(',', '')
                parts << user[:last_name]
                parts << ""
                parts << ""
                parts << ""
                parts << ""
                parts << user[:geschlecht].upcase
                parts << user[:first_name]
                parts << "#{count}"
                parts << "#{user[:klasse]}"
                parts << "#{count}"
                parts << ""
                parts << "#{user[:geburtstag][0, 4]}#{user[:geburtstag][5, 2]}#{user[:geburtstag][8, 2]}"
                parts << ""
                # ["\"Mustermann\"", "\"Mustermann\"", "", "", "", "", "\"W\"", "\"Max\"", "\"1\"", "\"5a\"", "\"1\"", "", "\"19670101\"", "", "\r"]
                # io.puts "#{user[:last_name]}\t#{user[:first_name]}\t#{user[:klasse]}\t#{user[:geschlecht]}\t#{geburtstag}"
                io.puts parts.map { |x| '"' + x + '"'}.join("\t")
            end
        end
        io.string
    end
end


292
293
294
295
296
297
298
299
300
# File 'src/ruby/include/admin.rb', line 292

def print_all_users
    require_admin!
    StringIO.open do |io|
        @@user_info.keys.sort.each do |email|
            io.puts "#{email} #{@@user_info[email][:nc_login]} #{@@user_info[email][:display_name]}"
        end
        io.string
    end
end


675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'src/ruby/include/admin.rb', line 675

def print_all_users_informatik_biber
    require_admin!
    StringIO.open do |io|
        @@klassen_order.each do |klasse|
            @@schueler_for_klasse[klasse].each do |email|
                user = @@user_info[email]
                biber_user_id = email
                biber_password = user[:biber_password]
                io.puts "#{Main.tr_klasse(user[:klasse])};#{user[:klasse].to_i};#{user[:first_name]};#{user[:last_name]};#{biber_user_id};#{biber_password};#{user[:geschlecht] == 'm' ? 'male' : 'female'}"
            end
        end
        @@klassen_order.each do |klasse|
            io.puts
            io.puts "Informatik-Biber Klasse #{Main.tr_klasse(klasse)}"
            io.puts '-' * "Informatik-Biber Klasse #{Main.tr_klasse(klasse)}".size
            io.puts
            io.puts "Anmeldung mit schulischer E-Mail-Adresse und 4-stelligem Passwort"
            io.puts
            @@schueler_for_klasse[klasse].each do |email|
                user = @@user_info[email]
                biber_user_id = email
                biber_password = user[:biber_password]
                io.puts "#{biber_password} #{user[:email]}"
            end
        end
        io.string
    end
end


2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
# File 'src/ruby/main.rb', line 2357

def print_current_monitor_advent_calendar_images()
    images = advent_calendar_images
    today = advents_calendar_date_today()
    StringIO.open do |io|
        d = advents_calendar_date_today() - 4
        d = 0 if d < 0
        d = 19 if d > 19
        (0..4).each do |x|
            i = d + x
            url = "/images/advent-calendar/doors/tiles-#{i}.png"
            if i <= today - 1
                url = images[i]
            end
            io.puts "<img src='#{url}' />"
        end
        io.string
    end
end


879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
# File 'src/ruby/include/poll.rb', line 879

def print_current_polls()
    require_user!
    today = Date.today.strftime('%Y-%m-%d')
    now = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
    email = @session_user[:email]
    entries = neo4j_query(<<~END_OF_QUERY, :email => email, :today => today).map { |x| {:poll_run => x['pr'], :poll_title => x['p.title'], :organizer => x['a.email'], :hidden => x['hidden'] } }
        MATCH (u:User {email: $email})-[rt:IS_PARTICIPANT]->(pr:PollRun)-[:RUNS]->(p:Poll)-[:ORGANIZED_BY]->(a:User)
        WHERE COALESCE(rt.deleted, false) = false
        AND COALESCE(pr.deleted, false) = false
        AND COALESCE(p.deleted, false) = false
        AND $today >= pr.start_date
        AND $today <= pr.end_date
        RETURN pr, p.title, a.email, COALESCE(rt.hide, FALSE) AS hidden
        ORDER BY pr.end_date, pr.end_time;
    END_OF_QUERY
    entries.select! do |entry|
        pr = entry[:poll_run]
        now >= "#{pr[:start_date]}T#{pr[:start_time]}:00" && now <= "#{pr[:end_date]}T#{pr[:end_time]}:00"
    end
    return '' if entries.empty?
    hidden_entries = entries.select { |x| x[:hidden] }
    StringIO.open do |io|
        io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
        unless hidden_entries.empty?
            io.puts "<div class='hint hint_poll_hidden_indicator'>"
            io.puts "Ausgeblendete Umfragen: #{hidden_entries.map { |x| x[:poll_title] }.join(', ')} <a id='show_hidden_polls' href='#'>(anzeigen)</a>"
            io.puts "</div>"
        end
        entries.each.with_index do |entry, _|
            io.puts "<div class='hint hint_poll' style='#{entry[:hidden] ? 'display: none;': ''}'>"
            poll_title = entry[:poll_title]
            poll_run = entry[:poll_run]
            organizer = entry[:organizer]
            io.puts "<div style='float: left; width: 36px; height: 36px; margin-right: 15px; position: relative; top: 5px; left: 4px;'>"
            io.puts user_icon(organizer, 'avatar-fill')
            io.puts "</div>"
            io.puts "<div>#{@@user_info[organizer][:display_name_official]} hat #{teacher_logged_in? ? 'Sie' : 'dich'} zu einer Umfrage eingeladen: <strong>#{poll_title}</strong>. #{teacher_logged_in? ? 'Sie können' : 'Du kannst'} bis zum #{Date.parse(poll_run[:end_date]).strftime('%d.%m.%Y')} um #{poll_run[:end_time]} Uhr teilnehmen (die Umfrage <span class='moment-countdown' data-target-timestamp='#{poll_run[:end_date]}T#{poll_run[:end_time]}:00' data-before-label='läuft noch' data-after-label='ist vorbei'></span>).</div>"
            io.puts "<hr />"
            io.puts "<button style='white-space: nowrap;' class='float-right btn btn-success bu-launch-poll' data-poll-run-id='#{poll_run[:id]}'>Zur Umfrage&nbsp;<i class='fa fa-angle-double-right'></i></button>"
            io.puts "<div style='clear: both;'></div>"
            io.puts "</div>"
        end
        io.puts "</div>"
        io.string
    end
end


454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'src/ruby/include/admin.rb', line 454

def print_email_accounts()
    require_admin!
    StringIO.open do |io|
        all_marked_known = Set.new
        all_termination_dates = {}
        neo4j_query('MATCH (n:KnownEmailAddress) RETURN n;').each do |row|
            info = row['n']
            email = info[:email]
            if info[:known]
                all_marked_known << email
            end
            if info[:scheduled_termination]
                all_termination_dates[email] = info[:scheduled_termination]
            end
        end

        Set.new(neo4j_query('MATCH (n:KnownEmailAddress {known: true}) RETURN n.email;').map { |x| x['n.email'] })
        email_addresses = @@current_email_addresses
        required_email_addresses = []
        data_for_required_email_address = {}
        @@user_info.each_pair do |email, info|
            next unless email.include?(SMTP_DOMAIN)
            required_email_addresses << email
            if info[:teacher]
                data_for_required_email_address[email] = {
                    :first_name => info[:first_name],
                    :last_name => info[:last_name],
                    :email => email,
                    :password => Main.gen_password_for_email(email)
                }
            else
                data_for_required_email_address[email] = {
                    :first_name => info[:first_name],
                    :last_name => info[:last_name],
                    :email => email,
                    :password => Main.gen_password_for_email(email)
                }
                eltern_email = "eltern.#{email}"
                required_email_addresses << eltern_email
                data_for_required_email_address[eltern_email] = {
                    :first_name => '',
                    :last_name => info[:last_name],
                    :email => eltern_email,
                    :password => Main.gen_password_for_email(eltern_email)
                }
            end
        end
        @@klassen_order.each do |klasse|
            email = "ev.#{klasse}@#{SCHUL_MAIL_DOMAIN}"
            data_for_required_email_address[email] = {
                :first_name => '',
                :last_name => "Elternvertreter:innen #{klasse}",
                :email => email,
                :password => Main.gen_password_for_email(email + Date.today.year.to_s)
            }
        end
        @@mailing_lists.keys.each { |email| required_email_addresses << email }
        @@klassen_order.each do |klasse|
            required_email_addresses << "ev.#{klasse}@#{SCHUL_MAIL_DOMAIN}"
        end

        known_email_association = {}
        email_addresses.each do |email|
            if @@user_info.include?(email)
                if @@user_info[email][:teacher]
                    known_email_association[email] = :teacher
                else
                    known_email_association[email] = :sus
                end
            elsif email[0, 7] == 'eltern.' && @@user_info.include?(email.sub('eltern.', ''))
                known_email_association[email] = :parents
            elsif @@mailing_lists.include?(email)
                known_email_association[email] = :mailing_list
            elsif email[0, 3] == 'ev.' && @@klassen_order.include?(email.split('@').first.sub('ev.', ''))
                known_email_association[email] = :ev
            end
        end
        required_email_addresses = (Set.new(required_email_addresses) - Set.new(email_addresses)).to_a.sort
        unknown_addresses = email_addresses.reject { |email| known_email_association.include?(email) }
        io.puts "<h3>Fehlende Postfächer</h3>"

        # io.puts "<table class='table'>"
        # io.puts "<tr><th>E-Mail-Adresse</th></tr>"
        # required_email_addresses.each do |email|
        #     io.puts "<tr>"
        #     io.puts "<td>#{email}</td>"
        #     io.puts "</tr>"
        # end
        # io.puts "</table>"
        io.puts "<pre>"
        required_email_addresses.each do |email|
            if data_for_required_email_address[email]
                io.puts data_for_required_email_address[email].to_json + ','
            else
                io.puts "// no data for #{email}"
            end
        end
        io.puts "</pre>"

        io.puts "<hr />"

        today_str = Date.today.strftime('%Y-%m-%d')
        [false, true].each do |known|
            if known
                io.puts "<h3>Bekannte Postfächer</h3>"
            else
                io.puts "<h3>Unbekannte / nicht mehr benötigte Postfächer</h3>"
            end
            io.puts "<table class='table'>"
            io.puts "<tr><th>E-Mail-Adresse</th><th></th></tr>"
            unknown_addresses.sort.each do |email|
                next unless all_marked_known.include?(email) == known
                io.puts "<tr>"
                classes = ''
                if all_termination_dates[email]
                    if (today_str <= all_termination_dates[email])
                        classes = 'bg-warning'
                    else
                        classes = 'bg-danger'
                    end
                end
                io.puts "<td class='#{classes}'>"
                io.puts "#{email}"
                if all_termination_dates[email]
                    io.puts "(Löschung zum #{all_termination_dates[email]})"
                end
                io.puts "<td>"
                io.puts "<td>"
                if known
                    io.puts "<button class='btn btn-xs btn-warning bu-mark-unknown-address' data-email='#{email}'>Unbekannt</button>"
                else
                    io.puts "<button class='btn btn-xs btn-success bu-mark-known-address' data-email='#{email}'>Bekannt</button>"
                    unless all_termination_dates[email]
                        io.puts "<button class='btn btn-xs btn-danger bu-mark-for-termination' data-email='#{email}' data-weeks='4'>Löschen in 4 Wochen</button>"
                        io.puts "<button class='btn btn-xs btn-danger bu-mark-for-termination' data-email='#{email}' data-weeks='1'>Löschen in 1 Woche</button>"
                    end
                end
                if all_termination_dates[email]
                    io.puts "<button class='btn btn-xs btn-warning bu-unmark-for-termination' data-email='#{email}'>Nicht löschen</button>"
                end
            io.puts "</td>"
                io.puts "</tr>"
            end
            io.puts "</table>"

            io.puts "<hr />"
        end
        io.string
    end
end


1998
1999
2000
# File 'src/ruby/main.rb', line 1998

def print_email_field(io, email)
    io.puts "<div class='input-group'><input type='text' class='form-control' readonly value='#{email}' style='min-width: 100px;' /><div class='input-group-append'><button class='btn btn-secondary btn-clipboard' data-clipboard-action='copy' title='Eintrag in die Zwischenablage kopieren' data-clipboard-text='#{email}'><i class='fa fa-clipboard'></i></button></div></div>"
end


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'src/ruby/include/gev.rb', line 3

def print_gev_table()
    assert(gev_logged_in?)
    temp = neo4j_query(<<~END_OF_QUERY).map { |x| { :email => x['u.email'], :name => x['u.ev_name'] } }
        MATCH (u:User {ev: true})
        RETURN u.email, u.ev_name;
    END_OF_QUERY
    gev = Set.new()
    name_for_email = {}
    temp.each do |row|
        gev << row[:email]
        name_for_email[row[:email]] = row[:name]
    end
    gev.each do |email|
        if @@user_info[email].nil?
            GEV_USERS.each do |gev_email|
                deliver_mail do
                    to gev_email
                    bcc SMTP_FROM
                    from SMTP_FROM

                    subject "Elternsprecher entfernt"

                    StringIO.open do |io|
                        io.puts "<p>Hallo!</p>"
                        io.puts "<p>Die Eltern von #{email} (#{name_for_email[email]}) wurden als Elternsprecher entfernt, da die Schülerin / der Schüler nicht mehr an der Schule ist.</p>"
                        io.puts "<p>Viele Grüße,<br />#{WEBSITE_MAINTAINER_NAME}</p>"
                        io.string
                    end
                end
            end
            temp = neo4j_query(<<~END_OF_QUERY, {:email => email})
                MATCH (u:User {email: $email})
                REMOVE u.ev
                REMOVE u.ev_name;
            END_OF_QUERY
            gev.delete(email)
        end
    end
    gev = gev.to_a.sort do |a, b|
        (@@user_info[a][:klasse] == @@user_info[b][:klasse]) ?
        (@@user_info[a][:last_name] <=> @@user_info[b][:last_name]) :
        ((KLASSEN_ORDER.index(@@user_info[a][:klasse]) || 0) <=> (KLASSEN_ORDER.index(@@user_info[b][:klasse]) || 0))
    end
    StringIO.open do |io|
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Elternvertreter:innen</th>"
        io.puts "<th>Klasse</th>"
        io.puts "<th>Name</th>"
        io.puts "<th></th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        gev.each do |email|
            io.puts "<tr class='user_row' data-email='#{email}'>"
            user = @@user_info[email]
            io.puts "<td>Eltern von #{user[:display_name]}</td>"
            io.puts "<td>#{tr_klasse(user[:klasse])}</td>"
            io.puts "<td><input type='text' class='form-control ti_name' value='#{name_for_email[email]}'/></td>"
            io.puts "<td><button class='btn btn-xs btn-danger bu-remove-ev'><i class='fa fa-trash'></i>&nbsp;&nbsp;Löschen</button></td>"
        io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
end


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'src/ruby/include/lehrbuchverein.rb', line 46

def print_lehrbuchverein_table()
    assert(can_manage_bib_members_logged_in? || can_manage_bib_payment_logged_in?)
    temp = neo4j_query(<<~END_OF_QUERY).map { |x| { :email => x['u.email'] } }
        MATCH (u:User {lmv_no_pay: true})
        RETURN u.email;
    END_OF_QUERY
    no_pay = Set.new()
    temp.each do |row|
        no_pay << row[:email]
    end
    temp = neo4j_query(<<~END_OF_QUERY, {:jahr => LEHRBUCHVEREIN_JAHR}).map { |x| { :email => x['u.email'] } }
        MATCH (u:User)-[:PAID_FOR]->(j:Lehrbuchvereinsjahr {jahr: $jahr})
        RETURN u.email;
    END_OF_QUERY
    paid = Set.new()
    temp.each do |row|
        paid << row[:email]
    end
    StringIO.open do |io|
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Name</th>"
        io.puts "<th>Vorname</th>"
        io.puts "<th>Klasse</th>"
        if can_manage_bib_members_logged_in?
            io.puts "<th>Bezahlt für #{LEHRBUCHVEREIN_JAHR}/#{(LEHRBUCHVEREIN_JAHR % 100) + 1}</th>"
            io.puts "<th>Zahlungsbefreit</th>"
            io.puts "<th>Selbstzahler</th>"
        end
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        all_schueler = []
        @@klassen_order.each do |klasse|
            (@@schueler_for_klasse[klasse] || []).each do |email|
                all_schueler << email
            end
        end
        # all_schueler.sort! do |a, b|
        #     @@user_info[a][:last_name] == @@user_info[b][:last_name] ?
        #     (@@user_info[a][:first_name] <=> @@user_info[b][:first_name]) :
        #     (@@user_info[a][:last_name] <=> @@user_info[b][:last_name])
        # end
        all_schueler.each do |email|
            unless can_manage_bib_members_logged_in?
                next unless mitglieder.include?(email)
            end
            state = 0
            state += 1 if paid.include?(email)
            state += 2 if no_pay.include?(email)
            # state += 4 if [5, 6].include?(((@@user_info[email] || {})[:klasse] || '').to_i)
            io.puts "<tr class='user_row' data-email='#{email}'>"
            user = @@user_info[email]
            io.puts "<td>#{user[:last_name]}</td>"
            io.puts "<td>#{user[:first_name]}</td>"
            io.puts "<td>#{tr_klasse(user[:klasse])}</td>"
            if can_manage_bib_members_logged_in?
                io.puts "<td>"
                io.puts "<button class='btn btn-xs #{((state >> 0) & 1) == 1 ? 'btn-success' : 'btn-outline-secondary'} bu_toggle_paid'>bezahlt für #{LEHRBUCHVEREIN_JAHR}/#{(LEHRBUCHVEREIN_JAHR % 100) + 1}</button>"
                io.puts "</td>"
                io.puts "<td>"
                io.puts "<button class='btn btn-xs #{(((state >> 1) & 1) == 1) || (((state >> 2) & 1) == 1) ? 'btn-primary' : 'btn-outline-secondary'} bu_toggle_no_pay' #{state & 4 == 4 ? 'disabled' : ''}>zahlungsbefreit</button>"
                io.puts "</td>"
                io.puts "<td>"
                io.puts "<button class='btn btn-xs #{state == 0 ? 'btn-danger' : 'btn-outline-secondary'} bu_no_book_for_you disabled'>Selbstzahler</button>"
                io.puts "</td>"
            end
        io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
end


2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
# File 'src/ruby/main.rb', line 2006

def print_lehrerzimmer_panel()
    require_user!
    return '' unless teacher_logged_in?
    return '' if teacher_tablet_logged_in?
    StringIO.open do |io|
        io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
        io.puts "<div class='hint lehrerzimmer-panel'>"
        io.puts "<div class='hide-sm'>"
        io.puts "<div style='padding-top: 7px;'>Momentan im Jitsi-Lehrerzimmer:&nbsp;"
        rooms = current_jitsi_rooms()
        nobody_here = true
        users = []
        if rooms
            rooms.each do |room|
                if room['roomName'] == 'lehrerzimmer'
                    room['participants'].sort do |a, b|
                        a['displayName'].downcase.sub('herr', '').sub('frau', '').sub('dr.', '').strip <=> b['displayName'].downcase.sub('herr', '').sub('frau', '').sub('dr.', '').strip
                    end.each do |participant|
                        email = participant['email']
                        users << email
                        if @@user_info[email] && @@user_info[email][:teacher]
                            io.puts "<span class='btn btn-xs ttc'>#{@@user_info[email][:shorthand]}</span>"
                            nobody_here = false
                        end
                    end
                end
            end
        end
        if nobody_here
            io.puts "<em>niemand</em>"
        end
        io.puts "</div>"
        io.puts "<hr />"
        io.puts "</div>"
        io.puts "<div class='hide-non-sm'>"
        users.each do |email|
            if @@user_info[email] && @@user_info[email][:teacher]
#                     io.puts "<span class='btn btn-xs ttc'>#{@@user_info[email][:shorthand]}</span>"
                io.puts "<div style='margin-right: 5px; display: inline-block; position: relative; top: 5px; background-image: url(#{NEXTCLOUD_URL}/index.php/avatar/#{@@user_info[email][:nc_login]}/128), url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mO88h8AAq0B1REmZuEAAAAASUVORK5CYII=);' class='avatar-md'></div>"
            end
        end
        io.puts "</div>"
        io.puts "<a href='/jitsi/Lehrerzimmer' target='_blank' style='white-space: nowrap;' class='float-right btn btn-success'><i class='fa fa-microphone'></i>&nbsp;&nbsp;Lehrerzimmer&nbsp;<i class='fa fa-angle-double-right'></i></a>"
        io.puts "<div style='clear: both;'></div>"
        io.puts "</div>"
        io.puts "</div>"
        io.string
    end
end


240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'src/ruby/include/admin.rb', line 240

def print_lesson_keys_history
    require_admin!
    StringIO.open do |io|
        start_dates = @@lessons[:timetables].keys.sort
        data = {}
        start_dates.each do |start_date|
            @@lessons[:timetables][start_date].each_pair do |lesson_key, info|
                info[:stunden].each_pair do |dow, info2|
                    info2.each_pair do |stunde, info3|
                        klassen = info3[:klassen]
                        lehrer = info3[:lehrer]
                        fach = @@lessons[:lesson_keys][lesson_key][:fach]
                        klassen.each do |klasse|
                            data[klasse] ||= {}
                            data[klasse][fach] ||= {}
                            data[klasse][fach][start_date] ||= {}
                            lehrer.each do |l|
                                data[klasse][fach][start_date][l] = true
                            end
                        end
                    end
                end
            end
        end
        KLASSEN_ORDER.each do |klasse|
            next if klasse.to_i > 10
            io.puts "<h3>Klasse #{tr_klasse(klasse)}</h3>"
            io.puts "<table class='table table-condensed table-striped table-sm'>"
            io.puts "<thead>"
            io.puts "<tr>"
            io.puts "<th>ab</th>"
            data[klasse].keys.sort.each do |fach|
                io.puts "<th>#{fach}</th>"
            end
            io.puts "</tr>"
            io.puts "</thead>"
            io.puts "<tbody>"
            start_dates.each do |start_date|
                io.puts "<tr>"
                io.puts "<td>#{start_date}</td>"
                data[klasse].keys.sort.each do |fach|
                    io.puts "<td>#{(data[klasse][fach][start_date] || {'-' => true}).keys.sort.join(', ')}</td>"
                end
                io.puts "</tr>"
            end
            io.puts "</tbody>"
            io.puts "</table>"
        end
        io.string
    end
end


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'src/ruby/include/stats.rb', line 91

def ()
    stats = ()
    klassen_stats = {}
    @@klassen_order.each do |klasse|
        klassen_stats[klasse] = 100 * stats[klasse][:count][LOGIN_STATS_D.last].to_f / stats[klasse][:total]
        if stats[klasse][:count][LOGIN_STATS_D.last] == stats[klasse][:total]
            neo4j_query(<<~END_OF_QUERY, :klasse => klasse, :timestamp => Time.now.to_i)
                MERGE (n:KlasseKomplett {klasse: $klasse})
                ON CREATE SET n.timestamp = $timestamp
            END_OF_QUERY
        end
    end
    klassen_ranking = neo4j_query(<<~END_OF_QUERY).map { |x| x['n.klasse'] }
        MERGE (n:KlasseKomplett)
        RETURN n.klasse
        ORDER BY n.timestamp ASC;
    END_OF_QUERY
    now = Time.now.to_i
    StringIO.open do |io|
        io.puts "<p style='text-align: center;'>"
        io.puts "<em>Die ersten Klassen sind komplett im Dashboard angemeldet.<br />Herzlichen Glückwunsch an die Klassen #{join_with_sep(klassen_ranking.map { |x| '<b>' + (tr_klasse(x) || '') + '</b>' }, ', ', ' und ')}!</em>"
        io.puts "</p>"
        klassen_stats.keys.sort do |a, b|
            va = sprintf('%020d%020d', 1000 - (klassen_ranking.index(a) || 1000), klassen_stats[a] * 1000)
            vb = sprintf('%020d%020d', 1000 - (klassen_ranking.index(b) || 1000), klassen_stats[b] * 1000)
            vb <=> va
        end.each.with_index do |klasse, index|
            place = "#{index + 1}."
            percent = klassen_stats[klasse]
            bgcol = get_gradient(['#cc0000', '#f4951b', '#ffe617', '#80bc42'], percent / 100.0)
            c = ''
            star_span = ''
            if stats[klasse][:count][LOGIN_STATS_D.last] == stats[klasse][:total]
                c = 'complete'
                star_span = "<i class='fa fa-star'></i>"
            else
                place = ''
            end
            io.puts "<span class='ranking #{c}' style='background-color: #{bgcol};'>#{star_span}<span class='klasse'>#{tr_klasse(klasse)}</span><span class='percent'>#{percent.to_i}%</span>"
            io.puts "<span class='place'>#{place}</span>" unless place.empty?
            io.puts "</span>"
        end
        io.string
    end
end


641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# File 'src/ruby/include/directory.rb', line 641

def print_mailing_list(io, list_email)
    return unless @@mailing_lists.include?(list_email)
    io.puts "<tr class='user_row'>"
    info = @@mailing_lists[list_email]
    io.puts "<td class='list_email_label'>#{info[:label]}</td>"
    io.puts "<td>"
    print_email_field(io, list_email)
    io.puts "</td>"
    io.puts "<td style='text-align: right;'><button data-list-email='#{list_email}' class='btn btn-warning btn-sm bu-toggle-adresses'>#{info[:recipients].size} Adressen&nbsp;&nbsp;<i class='fa fa-chevron-down'></i></button></td>"
    io.puts "</tr>"
    io.puts "<tbody style='display: none;' class='list_email_emails' data-list-email='#{list_email}'>"
    info[:recipients].sort do |a, b|
        an = ((@@user_info[a.sub(/^eltern\./, '')] || {})[:display_name] || '').downcase
        bn = ((@@user_info[b.sub(/^eltern\./, '')] || {})[:display_name] || '').downcase
        an <=> bn
    end.each do |email|
        name = (@@user_info[email] || {})[:display_name] || ''
        if email[0, 7] == 'eltern.'
            name = (@@user_info[email.sub('eltern.', '')] || {})[:display_name] || ''
            name = "Eltern von #{name}"
        end
        io.puts "<tr class='user_row'>"
        io.puts "<td>#{name}</td>"
        io.puts "<td colspan='2'>"
        print_email_field(io, email)
        io.puts "</td>"
        io.puts "</tr>"
    end
    io.puts "</tbody>"
end


672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'src/ruby/include/directory.rb', line 672

def print_mailing_lists()
    StringIO.open do |io|
        io.puts "<table class='table table-condensed narrow' style='width: unset; min-width: 100%;'>"
        remaining_mailing_lists = Set.new(@@mailing_lists.keys)
        @@klassen_order.each do |klasse|
            io.puts "<tr><th colspan='3'>Klasse #{tr_klasse(klasse)}</th></tr>"
            ["klasse.#{klasse}@#{SCHUL_MAIL_DOMAIN}",
             "eltern.#{klasse}@#{SCHUL_MAIL_DOMAIN}",
             "lehrer.#{klasse}@#{SCHUL_MAIL_DOMAIN}"].each do |list_email|
                print_mailing_list(io, list_email)
                remaining_mailing_lists.delete(list_email)
            end
            if ['11', '12'].include?(klasse)
                ['gr', 'it'].each do |group_af|
                    ['', '.eltern'].each do |extra|
                        list_email = "antikenfahrt.#{group_af}#{extra}.#{klasse}@#{SCHUL_MAIL_DOMAIN}"
                        if @@mailing_lists[list_email]
                            print_mailing_list(io, list_email)
                            remaining_mailing_lists.delete(list_email)
                        end
                    end
                end
            end
        end
        io.puts "<tr><th colspan='3'>Klassenleiter-Teams</th></tr>"
        [5, 6, 7, 8, 9, 10].each do |klasse|
            list_email = "team.#{klasse}@mail.gymnasiumsteglitz.de"
            print_mailing_list(io, list_email)
            remaining_mailing_lists.delete(list_email)
        end
        print_mailing_list(io, "kl@#{SCHUL_MAIL_DOMAIN}")
        remaining_mailing_lists.delete("kl@#{SCHUL_MAIL_DOMAIN}")
        io.puts "<tr><th colspan='3'>Gesamte Schule</th></tr>"
        ["sus@#{SCHUL_MAIL_DOMAIN}",
         "eltern@#{SCHUL_MAIL_DOMAIN}",
         "lehrer@#{SCHUL_MAIL_DOMAIN}"].each do |list_email|
            print_mailing_list(io, list_email)
            remaining_mailing_lists.delete(list_email)
        end
        unless remaining_mailing_lists.empty?
            io.puts "<tr><th colspan='3'>Weitere E-Mail-Verteiler</th></tr>"
            remaining_mailing_lists.to_a.sort.each do |list_email|
                print_mailing_list(io, list_email)
            end
        end
        io.puts "</table>"
        io.string
    end
end


722
723
724
725
726
727
728
729
730
731
732
733
# File 'src/ruby/include/directory.rb', line 722

def print_mailing_lists_allowed_senders()
    StringIO.open do |io|
        io.puts "<table class='table table-condensed narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<tr>"
        io.puts "<th>Name</th>"
        io.puts "<th>E-Mail-Adresse</th>"
        io.puts "<th>Berechtigung</th>"
        io.puts "</tr>"
        io.puts "</table>"
        io.string
    end
end


718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
# File 'src/ruby/include/cypher.rb', line 718

def print_maze_defs()
    room_heights = [
        [1, 1, 1],
        [1, 2, 2],
        [1, 3, 2],
        [2, 5, 4],
        [1, 4, 3],
        [1, 3, 2],
        [2, 5, 3],
        [1, 4, 2],
        [2, 3, 2],
        [1, 2, 1],
    ]
    StringIO.open do |io|
        font = JSON.parse(File.read('maze-font-processed.json'))
        cypher_content()
        word = @cypher_next_password.upcase.gsub(/[^A-Z]/, '')
        bitmap = []
        depth = 7
        width = 1
        height = 1
        z0 = 1
        z1 = 3
        tz = 2
        x0 = nil
        x1 = nil
        y0 = nil
        y1 = nil
        sections = []
        tunnel_length = 16
        [:test, :for_real].each do |mode|
            ox = 0
            oy = 0
            if mode == :for_real
                x0 -= 1
                y0 -= 1
                x1 += 1
                y1 += 1
                width = x1 - x0 + 1
                height = y1 - y0 + 1
                (0...depth).each do |z|
                    level = []
                    (0...height).each do |y|
                        level << '1' * width
                    end
                    bitmap << level
                end
            end
            (0...word.size).each do |i0|
                z0 = room_heights[i0][0]
                z1 = room_heights[i0][1]
                tz = room_heights[i0][2]
    
                i1 = i0 + 1
                c0 = word[i0]
                c1 = word[i1]
                font[c0]['cells'].each do |pair|
                    x = pair[0] + ox
                    y = pair[1] + oy
                    if mode == :test
                        x0 ||= x
                        x0 = x if x < x0
                        x1 ||= x
                        x1 = x if x > x1
                        y0 ||= y
                        y0 = y if y < y0
                        y1 ||= y
                        y1 = y if y > y1
                    else
                        (z0..z1).each do |z|
                            bitmap[z][y - y0][x - x0] = '0'
                            if z == tz
                                if i0 == 0
                                    if x - ox == font[c0]['entry'][0] && y - oy == font[c0]['entry'][1]
                                        bitmap[z][y - y0][x - x0] = 'A'
                                    end
                                    if x - ox - 1 == font[c0]['entry'][0] && y - oy == font[c0]['entry'][1]
                                        bitmap[z][y - y0][x - x0] = 'B'
                                    end
                                end
                                if x - ox == font[c0]['exit'][0] && y - oy == font[c0]['exit'][1]
                                    if i0 > 0
                                        sections << x - x0 - tunnel_length + 2 if mode == :for_real
                                    end
                                    sections << x - x0 + 1 if mode == :for_real
                                end
                            end
                        end
                    end
                end
                if i1 < word.size
                    (1...tunnel_length).each do |dx|
                        x = font[c0]['exit'][0] + dx + ox
                        y = font[c0]['exit'][1] + oy
                        if mode == :for_real
                            bitmap[tz][y - y0][x - x0] = '0'
                        end
                    end
                    ox += font[c0]['exit'][0] - font[c1]['entry'][0] + tunnel_length
                    oy += font[c0]['exit'][1] - font[c1]['entry'][1]
                end
            end
        end
        sections << bitmap[0][0].size
        io.puts '<script type="text/plain" id="level">'
        bitmap.each do |level|
            level.each do |row|
                row.each_char do |c|
                    if c == '0'
                        io.print ((0...13).to_a.sample + 'a'.ord).chr
                    elsif c == '1'
                        io.print ((0...13).to_a.sample + 13 + 'a'.ord).chr
                    else
                        io.print c
                    end
                end
                io.puts
            end
            io.puts
        end
        io.puts "</script>"
        io.puts '<script type="text/plain" id="level_sectors">'
        io.puts sections.to_json
        io.puts "</script>"
        io.string
    end
end


2002
2003
2004
# File 'src/ruby/main.rb', line 2002

def print_password_field(io, password)
    io.puts "<div class='input-group'><input type='password' class='form-control' readonly value='#{password}' style='min-width: 50px;' /><div class='input-group-append'><button class='btn btn-secondary btn-clipboard' data-clipboard-action='copy' title='Eintrag in die Zwischenablage kopieren' data-clipboard-text='#{password}'><i class='fa fa-clipboard'></i></button></div></div>"
end


186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'src/ruby/include/public_event.rb', line 186

def print_public_event_table()
    self.class.refresh_public_event_config()
    ts = Time.now.strftime("%Y-%m-%dT%H:%M")
    StringIO.open do |io|
        @@public_event_config.each.with_index do |event, event_index|
            if event_index > 0
                io.puts "<hr />"
            end
            io.puts "<h3>#{event[:title]}</h3>"
            if event[:description]
                io.puts event[:description]
            end
            sign_ups = get_sign_ups_for_public_event(event[:key])
            if event[:auto_rows]
                io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;' data-event-key='#{event[:key]}'>"
                io.puts "<div style='display: none;' class='event-title'>#{event[:title]}</div>"
                io.puts "<table class='table table-narrow narrow th-middle'>"
                colspan = 1
                io.puts "<colgroup>"
                io.puts "<col style='width: 180px;'/>"
                colspan.times do
                    io.puts "<col style='width: auto;'/>"
                end
                io.puts "</colgroup>"
                io.puts "<tbody>"
                if event[:headings]
                    io.puts "<tr>"
                    io.puts "<th>#{event[:headings][0]}</th>"
                    io.puts "<th colspan='#{colspan}'>#{event[:headings][1]}</th>"
                    io.puts "</tr>"
                end
                event[:rows].each do |row|
                    printed_row = false
                    row[:entries].each do |entry|
                        text = (event[:booking_text] || '').dup
                        while true
                            index = text.index('{')
                            break if index.nil?
                            length = 1
                            balance = 1
                            while index + length < text.size && balance > 0
                                c = text[index + length]
                                balance -= 1 if c == '}'
                                balance += 1 if c == '{'
                                length += 1
                            end
                            code = text[index + 1, length - 2]
                            begin
                                text[index, length] = eval(code).to_s || ''
                            rescue
                                debug "Error while evaluating for #{(@session_user || {})[:email]}:"
                                debug code
                                raise
                            end
                        end
                        if entry[:deadline]
                            next if ts > entry[:deadline]
                        end
                        booked_out = false
                        if (sign_ups[entry[:key]] || []).size >= (entry[:capacity] || 0)
                            booked_out = true
                        end
                        unless printed_row
                            io.puts "<tr>"
                            io.puts "<th>#{row[:description]}</th>"
                            io.puts "<td>"
                            printed_row = true
                        end
                        io.puts "<button data-event-key='#{event[:key]}' data-key='#{entry[:key]}' style='width: 5.3em;' class='btn #{booked_out ? 'btn-outline-secondary' : 'btn-info'} bu-book-public-event' #{booked_out ? 'disabled': ''}>#{entry[:description]}</button><div style='display: none;' class='booking-text'>#{text}</div>"
                    end
                    if printed_row
                        io.puts "</td>"
                        io.puts "</tr>"
                    end
                end
                io.puts "</tbody>"
                io.puts "</table>"
                io.puts "</div>"
            else
                io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;' data-event-key='#{event[:key]}'>"
                io.puts "<div style='display: none;' class='event-title'>#{event[:title]}</div>"
                io.puts "<table class='table table-narrow narrow th-middle'>"
                colspan = event[:rows].map { |x| x[:entries].size }.max
                io.puts "<colgroup>"
                io.puts "<col style='width: 180px;'/>"
                colspan.times do
                    io.puts "<col style='width: auto;'/>"
                end
                io.puts "</colgroup>"
                io.puts "<tbody>"
                if event[:headings]
                    io.puts "<tr>"
                    io.puts "<th>#{event[:headings][0]}</th>"
                    io.puts "<th colspan='#{colspan}'>#{event[:headings][1]}</th>"
                    io.puts "</tr>"
                end
                event[:rows].each do |row|
                    io.puts "<tr>"
                    io.puts "<th>#{row[:description]}</th>"
                    row[:entries].each do |entry|
                        text = (event[:booking_text] || '').dup
                        while true
                            index = text.index('{')
                            break if index.nil?
                            length = 1
                            balance = 1
                            while index + length < text.size && balance > 0
                                c = text[index + length]
                                balance -= 1 if c == '}'
                                balance += 1 if c == '{'
                                length += 1
                            end
                            code = text[index + 1, length - 2]
                            begin
                                text[index, length] = eval(code).to_s || ''
                            rescue
                                debug "Error while evaluating for #{(@session_user || {})[:email]}:"
                                debug code
                                raise
                            end
                        end
                        booked_out = false
                        if (sign_ups[entry[:key]] || []).size >= (entry[:capacity] || 0)
                            booked_out = true
                        end
                        io.puts "<td><button data-event-key='#{event[:key]}' data-key='#{entry[:key]}' class='btn #{booked_out ? 'btn-outline-secondary' : 'btn-info'} bu-book-public-event' #{booked_out ? 'disabled': ''}>#{entry[:description]}</button><div style='display: none;' class='booking-text'>#{text}</div></td>"
                    end
                    io.puts "</tr>"
                end
                io.puts "</tbody>"
                io.puts "</table>"
                io.puts "</div>"
            end
        end
        io.string
    end
end


400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'src/ruby/include/salzh.rb', line 400

def print_salzh_panel
    require_user!
    unless teacher_logged_in?
        salzh_status = Main.get_salzh_status_for_emails(@session_user[:email])[@session_user[:email]]
        return '' if salzh_status[:status].nil?
        StringIO.open do |io|
            end_date = salzh_status[:status_end_date]
            if salzh_status[:status] == :salzh
                p = Date.parse(end_date) + 1
                while [0, 6].include?(p.wday) || @@holiday_dates.include?(p.strftime("%Y-%m-%d"))
                    p += 1
                end
                io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
                io.puts "<div class='hint'>"
                io.puts "<p><strong>Unterricht im saLzH</strong></p>"
                io.puts "<p>Du bist <strong>bis zum #{Date.parse(end_date).strftime('%d.%m.')}</strong> für das schulisch angeleite Lernen zu Hause (saLzH) eingetragen. Bitte schau regelmäßig in deinem Stunden&shy;plan nach, ob du Aufgaben in der Nextcloud oder im Lernraum bekommst oder ob Stunden per Jitsi durch&shy;geführt werden. <strong>Ab dem #{p.strftime('%d.%m.')}</strong> erwarten wir dich wieder in der Schule.</p>"
                io.puts "</div>"
                io.puts "</div>"
            elsif salzh_status[:status] == :contact_person
                io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
                io.puts "<div class='hint'>"
                io.puts "<p><strong>Kontaktperson</strong></p>"
                io.puts "<p>Du bist <strong>bis zum #{Date.parse(end_date).strftime('%d.%m.')}</strong> als Kontakt&shy;person markiert. Das heißt, dass du weiterhin in die Schule kommen darfst, aber einige Regeln beachten musst. Falls du freiwillig zu Hause bleiben möchtest, müssen deine Eltern dem Sekretariat <a href='mailto:sekretariat@gymnasiumsteglitz.de'>per E-Mail Bescheid geben</a>. Die folgenden Regeln gelten für dich:</p>"
                io.puts "<hr />"
                io.puts "<ul style='padding-left: 1.5em;'>"
                io.puts "<li>tägliche Testung vor Beginn der Schultages (ein Test vom Vortag, z. B. aus einem Schnell&shy;test&shy;zentrum, kann nicht akzeptiert werden)</li>"
                if ['11', '12'].include?(@session_user[:klasse])
                    io.puts "<li>du bekommst von deiner Tutorin / deinem Tutor am Freitag für jeden Tag des Wochenendes, an du noch Kontakt&shy;person bist, einen Schnelltest mit nach Hause</li>"
                else
                    io.puts "<li>du bekommst von deiner Klassen&shy;leitung (#{@@klassenleiter[@session_user[:klasse]].map { |shorthand| @@user_info[@@shorthands[shorthand]][:display_last_name] }.join(' oder ')}) am Freitag für jeden Tag des Wochenendes, an du noch Kontakt&shy;person bist, einen Schnelltest mit nach Hause</li>"
                end
                io.puts "<li>falls du Symptome (Husten, Fieber, Kopfschmerzen, …) zeigst, darfst du das Schulhaus nicht mehr betreten</li>"
                io.puts "<li>du darfst nicht mehr am gemeinsamen Essen in der Mensa teilnehmen</li>"
                io.puts "<li>während des Sport&shy;unter&shy;richts (Umkleide, Sport in der Halle) musst du durch&shy;gehend eine Maske tragen – ist dies aufgrund der körperlichen Betätigung nicht möglich, nimmst du nicht am Sportunterricht teil</li>"
                io.puts "</ul>"
                io.puts "</div>"
                io.puts "</div>"
            elsif salzh_status[:status] == :hotspot_klasse
                io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
                io.puts "<div class='hint'>"
                io.puts "<p><strong>Klasse mit erhöhtem Infektionsaufkommen</strong></p>"
                io.puts "<p>Da in deiner Klasse momentan ein erhöhtes Infektionsgeschehen herrscht, wirst du <strong>bis zum #{Date.parse(end_date).strftime('%d.%m.')}</strong> täglich getestet.</p>"
                io.puts "</div>"
                io.puts "</div>"
            end
            io.string
        end
    else
        entries = get_current_salzh_status_for_logged_in_teacher()
        return '' if entries.empty?
        hide_explanations = neo4j_query_expect_one(<<~END_OF_QUERY, {:email => @session_user[:email]})['hide']
            MATCH (u:User {email: $email})
            RETURN COALESCE(u.hide_salzh_panel_explanation, false) AS hide;
        END_OF_QUERY
        StringIO.open do |io|
            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
            io.puts "<div class='hint'>"
            # io.puts "<p><strong><div style='display: inline-block; padding: 4px; margin: -4px; border-radius: 4px' class='bg-warning'>SuS im saLzH</div></strong></p>"
            contact_person_count = 0
            salzh_count = 0
            all_klassen = {}
            entry_for_email = {}
            email_for_klasse_and_status = {}
            entries.each do |x| 
                entry_for_email[x[:email]] = x
                if x[:status] == :contact_person
                    contact_person_count += 1
                elsif x[:status] == :salzh
                    salzh_count += 1
                end
                all_klassen[x[:klasse]] ||= []
                all_klassen[x[:klasse]] << x[:email]
                email_for_klasse_and_status[x[:klasse]] ||= {}
                email_for_klasse_and_status[x[:klasse]][x[:status]] ||= []
                email_for_klasse_and_status[x[:klasse]][x[:status]] << x[:email]
            end

            spans = []
            if contact_person_count > 0
                spans << "#{contact_person_count}&nbsp;SuS, die Kontaktpersonen sind"
            end
            if salzh_count > 0
                spans << "#{salzh_count}&nbsp;SuS im saLzH"
            end
            io.puts "<p>"
            io.puts "Sie haben momentan #{spans.map { |x| '<strong>' + x + '</strong>'}.join(' und ')}."
            io.puts "</p>"
            io.puts "<div style='margin: 0 -10px 0 -10px; overflow-x: clip;'><table class='table table-sm narrow' style='width: 100%;'>"
            io.puts "<tr><th>Klasse</th><th>Status</th></tr>"
            KLASSEN_ORDER.each do |klasse|
                next unless all_klassen.include?(klasse)
                first_row = true
                all_klassen[klasse].each do |email|
                    next unless [:salzh, :contact_person].include?(entry_for_email[email][:status])
                    if first_row
                        io.puts "<tbody>"
                        io.puts "<tr class='klasse-click-row' data-klasse='#{klasse}'><td>Klasse #{tr_klasse(klasse)}</td>"
                        io.puts "<td>"
                        [:contact_person, :salzh].each do |status|
                            if (email_for_klasse_and_status[klasse][status] || []).size > 0
                                io.puts "<span class='salzh-badge salzh-badge-big bg-#{status == :contact_person ? 'warning': 'danger'}'><span>#{(email_for_klasse_and_status[klasse][status] || []).size}</span></span>"
                            end
                        end
                        io.puts "</td></tr>"
                        io.puts "</tbody>"
                        io.puts "<tbody style='display: none;'>"
                    end
                    first_row = false
                    badge = "<span style='position: relative; top: -1px;' class='salzh-badge salzh-badge-big bg-#{SALZH_MODE_COLORS[entry_for_email[email][:status]]}'><i class='fa #{SALZH_MODE_ICONS[entry_for_email[email][:status]]}'></i></span>"
                    io.puts "<tr><td colspan='2'>#{badge}#{@@user_info[email][:display_name]}</td></tr>"
                end
                unless first_row
                    io.puts "</tbody>"
                end
            end
            io.puts "</table></div>"
            io.puts "<hr />"
            io.puts "<p style='cursor: pointer;' onclick=\"$('#salzh_explanation').slideDown();\">"
            io.puts "<strong>Was bedeutet das?</strong>"
            io.puts "</p>"
            io.puts "<div id='salzh_explanation' style='display: #{hide_explanations ? 'none': 'block'};'>"
            if salzh_count > 0
                io.puts "<hr />"
                io.puts "<p>"
                io.puts "<strong><span class='bg-danger' style='padding: 0.2em 0.5em; font-weight: bold; border-radius: 0.25em; color: #fff;'>saLzH:</span></strong> Es handelt sich um SuS, die aus unter&shy;schied&shy;lichen Gründen im saLzH sind (z. B. positiv getestet / als Kontaktperson nach Rückmeldung der Eltern bestätigt freiwillig im saLzH / Aussetzung der Präsenz&shy;pflicht). Bitte ermöglichen Sie diesen SuS eine Teilnahme am Unterricht."
                io.puts "</p>"
            end
            if contact_person_count > 0
                io.puts "<hr />"
                io.puts "<p>"
                io.puts "<strong><span class='bg-warning' style='padding: 0.2em 0.5em; font-weight: bold; border-radius: 0.25em;'>Kontaktpersonen:</span></strong> Diese SuS wurden als Kontaktperson identifiziert, besuchen aber trotzdem weiterhin die Schule. Für sie gilt:"
                io.puts "</p>"
                io.puts "<ul style='padding-left: 1.5em;'>"
                io.puts "<li>tägliche Testung vor Beginn der Schultages (ein Test vom Vortag, z. B. aus einem Schnelltestzentrum, kann bei diesen SuS nicht akzeptiert werden)</li>"
                io.puts "<li>durch die Klassenleitung wird am Freitag für jeden Tag des Wochenendes, der in diesen Status fällt, ein Schnelltest mitgegeben</li>"
                io.puts "<li>zeigen diese Kinder Symptome (Husten, Fieber, Kopfschmerzen, …), dürfen sie das Schulhaus nicht mehr betreten</li>"
                io.puts "<li>am gemeinsamen Essen in der Mensa darf nicht mehr teilgenommen werden</li>"
                io.puts "<li>während des Sportunterrichts (Umkleide, Sport in der Halle) muss durchgehend eine Maske getragen werden – ist dies aufgrund der körperlichen Betätigung nicht möglich, nehmen diese Kinder nicht am Sportunterricht teil</li>"
                io.puts "</ul>"
            end
            io.puts "<button class='btn btn-xs btn-outline-secondary' id='bu_minimize_salzh_explanation'>Diese Information minimieren</button>"
            io.puts "</div>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
end


910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
# File 'src/ruby/include/salzh.rb', line 910

def print_salzh_protocol_table(week_delta = 0)
    holiday_dates = Set.new()
    @@ferien_feiertage.each do |entry|
        temp0 = Date.parse(entry[:from])
        temp1 = Date.parse(entry[:to])
        while temp0 <= temp1
            holiday_dates << temp0.strftime('%Y-%m-%d')
            temp0 += 1
        end
    end
    StringIO.open do |io|
        data = {}
        Dir['/internal/salzh_protocol/*.txt'].sort.each do |path|
            datum = File.basename(path).sub('.txt', '')
            d = DateTime.parse(datum)

            # skip weekends
            next if [0, 6].include?(d.wday)

            # skip holidays
            next if holiday_dates.include?(datum)

            emails = File.read(path).split("\n").map { |x| x.strip }.reject { |x| x.empty? || x[0] == '#' }
            emails.each do |email|
                data[email] ||= {}
                data[email][datum] = true
            end
        end
        now = DateTime.now
        now -= week_delta * 7
        cw = now.strftime('%-V').to_i
        d0 = now
        while d0.wday != 1
            d0 -= 1
        end
        d1 = d0 + 4
        io.puts "<table class='table table-striped table-sm narrow'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th rowspan='2'>Klasse</th>"
        io.puts "<th rowspan='2'>Name</th>"
        io.puts "<th colspan='5'>KW #{cw} (#{d0.strftime('%d.%m.')} &ndash; #{d1.strftime('%d.%m.')})</th>"
        io.puts "<th rowspan='2'>Gesamt</th>"
        io.puts "</tr>"
        io.puts "<tr>"
        io.puts "<th>Mo</th>"
        io.puts "<th>Di</th>"
        io.puts "<th>Mi</th>"
        io.puts "<th>Do</th>"
        io.puts "<th>Fr</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        KLASSEN_ORDER.each.with_index do |klasse, index|
            iterate_directory(klasse) do |email, i|
                next unless data[email]
                io.puts "<tr>"
                io.puts "<td>#{klasse}</td>"
                io.puts "<td>#{@@user_info[email][:display_name]}</td>"
                (0...5).each do |i|
                    flag = data[email][(d0 + i).strftime('%Y-%m-%d')]
                    io.print "<td>"
                    if flag 
                        io.puts "<i class='fa fa-home'></i>"
                    else
                        io.puts "&ndash;"
                    end
                    io.puts "</td>"
                end
                io.puts "<td>#{data[email].size}</td>"
                io.puts "</tr>"
            end
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.string
    end
end


853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
# File 'src/ruby/include/salzh.rb', line 853

def print_salzh_protocol_table_overview(week_delta = 0)
    holiday_dates = Set.new()
    @@ferien_feiertage.each do |entry|
        temp0 = Date.parse(entry[:from])
        temp1 = Date.parse(entry[:to])
        while temp0 <= temp1
            holiday_dates << temp0.strftime('%Y-%m-%d')
            temp0 += 1
        end
    end
    StringIO.open do |io|
        data = {}
        week = Set.new()
        now = DateTime.now
        now -= week_delta * 7
        cw = now.strftime('%-V').to_i
        d0 = now
        while d0.wday != 1
            d0 -= 1
        end
        d1 = d0 + 4
        Dir['/internal/salzh_protocol/*.txt'].sort.each do |path|
            datum = File.basename(path).sub('.txt', '')
            d = DateTime.parse(datum)

            # skip weekends
            next if [0, 6].include?(d.wday)

            # skip holidays
            next if holiday_dates.include?(datum)

            # skip if not current week
            next if datum < d0.strftime('%Y-%m-%d') || datum > d1.strftime('%Y-%m-%d')

            emails = File.read(path).split("\n").map { |x| x.strip }.reject { |x| x.empty? || x[0] == '#' }
            emails.each do |email|
                data[datum] ||= Set.new()
                data[datum] << email
                week << email
            end
        end
        io.puts "<table class='table table-striped table-sm narrow'>"
        io.puts "<tbody>"
        io.puts "<tr>"
        io.puts "<th>KW #{cw}</th><th>#{d0.strftime('%d.%m.')} &ndash; #{d1.strftime('%d.%m.')}</th><td>#{week.size} SuS</td>"
        io.puts "</tr>"
        (0...5).each do |i|
            io.puts "<tr>"
            io.puts "<th>#{%w(Mo Di Mi Do Fr)[i]}</th><th>#{(d0 + i).strftime('%d.%m.')}</th><td>#{(data[(d0 + i).strftime('%Y-%m-%d')] || Set.new()).size} SuS</td>"
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.string
    end
end


1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
# File 'src/ruby/include/salzh.rb', line 1065

def print_self_test_panel()
    require_user!
    return '' unless teacher_logged_in?
    return '' if teacher_tablet_logged_in?
    return '' if EXCLUDE_FROM_SELF_TEST_REPORT.include?(@session_user[:shorthand])
    today = Date.today.strftime('%Y-%m-%d')
    StringIO.open do |io|
        io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
        io.puts "<div class='hint'>"
        days = self_tests_this_week()
        label = 'noch nicht'
        days_label = ''
        if days.size > 0
            label = "bereits #{days.size}×"
            days_label = " (am #{join_with_sep(days.map{ |x| x[:label] }, ', ', ' und ')})"
        end
        io.puts "Sie haben sich in dieser Woche <strong>#{label}</strong> getestet#{days_label}."
        io.puts "<hr />"
        io.puts "<button #{days.map { |x| x[:datum] }.include?(today) ? 'disabled' : ''} class='bu-add-self-test btn btn-success btn-sm float-right'><i class='fa fa-pencil'></i>&nbsp;&nbsp;Testung protokollieren…</button><div style='clear: both;'></div>"
        io.puts "</div>"
        io.puts "</div>"
        io.string
    end
end


2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
# File 'src/ruby/main.rb', line 2209

def print_semi_public_links()
    require_user!
    # return '' unless teacher_logged_in?
    return '' if teacher_tablet_logged_in?
    StringIO.open do |io|
        io.puts "<h2 style='margin-bottom: 30px; margin-top: 30px;'>Schulinterne Links</h2>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<tr><th>Website</th><th>Name</th><th>Passwort</th></tr>"
        SEMI_PUBLIC_LINKS.each do |link|
            next unless link[:condition].call(self)
            io.puts "<tr>"
            io.puts "<td><a href='#{link[:url]}' target='_blank'>#{link[:title]}</a></td>"
            io.puts "<td>"
            print_email_field(io, link[:user])
            io.puts "</td>"
            io.puts "<td>"
            print_password_field(io, link[:password])
            io.puts "</td>"
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<hr />"
        io.string
    end
end


519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'src/ruby/include/login.rb', line 519

def print_sessions()
    require_user!
    StringIO.open do |io|
        io.puts "<table class='table table-condensed table-striped table-narrow'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Gültig bis</th>"
        io.puts "<th>Zuletzt verwendet</th>"
        io.puts "<th>Gerät</th>"
        io.puts "<th>Art</th>"
        io.puts "<th>Abmelden</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        sessions = get_current_user_sessions()

        sessions.each do |s|
            io.puts "<tr>"
            d = s[:expires] ? Time.parse(s[:expires]).strftime('%d.%m.%Y') : '&ndash;'
            io.puts "<td>#{d}</td>"
            d = s[:last_access] ? Time.parse(s[:last_access]).strftime('%d.%m.%Y') : '&ndash;'
            io.puts "<td>#{d}</td>"
            io.puts "<td style='text-overflow: ellipsis;'>#{(s[:sid] == @used_session[:sid]) ? '<i class=\'text-success fa fa-check\'></i>&nbsp;&nbsp;' : ''}#{s[:user_agent] || 'unbekanntes Gerät'}#{(s[:sid] == @used_session[:sid]) ? '<div style=\'font-size: 85%; margin-top: -5px;\'>(dieses Gerät)</div>' : ''}</td>"
            io.puts "<td>#{LOGIN_METHODS[s[:method].to_sym] || '&ndash;'}</td>"
            io.puts "<td><button class='btn btn-danger btn-xs btn-purge-session' data-purge-session='#{s[:scrambled_sid]}'><i class='fa fa-sign-out'></i>&nbsp;&nbsp;Gerät abmelden</button></td>"
            io.puts "</tr>"
        end
        if sessions.size > 1
            io.puts "<tr>"
            io.puts "<td colspan='5'><button class='float-right btn btn-danger btn-xs btn-purge-session' data-purge-session='_all'><i class='fa fa-sign-out'></i>&nbsp;&nbsp;Alle Geräte abmelden</button></td>"            
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.string
    end
end


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'src/ruby/include/stats.rb', line 49

def print_stats()
    require_admin!
     = ()
    StringIO.open do |io|
        io.puts "<table class='table table-narrow'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Gruppe</th>"
        io.puts "<th>jemals</th>"
        io.puts "<th>letzte 4 Wochen</th>"
        io.puts "<th>letzte Woche</th>"
        io.puts "<th>heute</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        ([:sus, :lehrer] + @@klassen_order).each do |key|
            label = nil
            if key == :sus
                label = 'Schülerinnen und Schüler'
            elsif key == :lehrer
                label = 'Lehrerinnen und Lehrer'
            else
                label = "Klasse #{tr_klasse(key)}" 
            end
            io.puts "<tr>"
            io.puts "<td>#{label}</td>"
            LOGIN_STATS_D.reverse.each do |d|
                io.puts "<td>"
                data = [key]
                percent = data[:total] == 0 ? 0 : ((data[:count][d] || 0) * 100 / data[:total]).to_i
                bgcol = get_gradient(['#cc0000', '#f4951b', '#ffe617', '#80bc42'], percent / 100.0)
                io.puts "<span style='background-color: #{bgcol}; padding: 4px 8px; margin: 0; border-radius: 3px;'>#{percent}%</span>"
                io.puts "</td>"
            end
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.string
    end
end


491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'src/ruby/include/lesson.rb', line 491

def print_stream_restriction_table(klasse)
    lesson_keys = (@@lessons_for_shorthand[@session_user[:shorthand]] || []).select do |lesson_key|
        lesson_info = @@lessons[:lesson_keys][lesson_key]
        (lesson_info[:klassen] || []).include?(klasse)
    end
    return '' if lesson_keys.empty?
    StringIO.open do |io|
        io.puts "<hr />"
        io.puts "<div class='alert alert-warning'>"
        io.puts "Falls Sie einschränken möchten, welche Kinder in Ihrem Unterricht am Streaming teilnehmen dürfen, können Sie dies hier tun. Standardmäßig ist der Stream, falls Sie ihn aktivieren, für alle Kinder aktiviert. Sie können zwei Einschränkungen vornehmen: a) nur für Kinder, die planmäßig gerade nicht in der Schule sind + Kinder, die dauerhaft zu Hause sind (»nicht für Wechselgruppe in Präsenz«) oder b) nur für Kinder, die dauerhaft zu Hause, also oben als »zu Hause« markiert sind (»nur für Dauer-saLzH«)."
        io.puts "</div>"
        io.puts "<div class='table-responsive'>"
        io.puts "<table class='table stream-restriction-table'>"
        io.puts "<tr>"
        io.puts "<th style='text-align: left;'>Fach</th>"
        io.puts "<th>Montag</th>"
        io.puts "<th>Dienstag</th>"
        io.puts "<th>Mittwoch</th>"
        io.puts "<th>Donnerstag</th>"
        io.puts "<th>Freitag</th>"
        io.puts "</tr>"
        lesson_keys.each do |lesson_key|
            io.puts "<tr>"
            restrictions = self.class.get_stream_restriction_for_lesson_key(lesson_key)
            io.puts "<td style='text-align: left;'>"
            lesson_info = @@lessons[:lesson_keys][lesson_key]
            io.puts "#{lesson_info[:pretty_folder_name]}"
            io.puts "</td>"
            restrictions.each.with_index do |r, i|
                btn_style = 'btn-primary'
                btn_label = 'für alle'
                if r == 1
                    btn_style = 'btn-info'
                    btn_label = 'nur für Dauer-saLzH'
                elsif r == 2
                    btn_style = 'btn-warning'
                    btn_label = 'nicht für Wechselgruppe in Präsenz'
                end
                io.puts "<td>"
                io.puts "<button data-lesson-key='#{lesson_key}' data-day='#{i}' class='bu-toggle-stream-restriction btn #{btn_style}'>#{btn_label}</button>"
                io.puts "</td>"
            end
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
end


543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'src/ruby/include/user.rb', line 543

def print_summoned_books_panel()
    require_user!
    email = @session_user[:email]
    self.class.refresh_bib_data()
    result = ''
    if @@bib_summoned_books[email]
        n_to_s = {1 => 'Eines der', 2 => 'Zwei', 3 => 'Drei', 4 => 'Vier', 5 => 'Fünf'}
        result += StringIO.open do |io|
            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
            io.puts "<div class='hint'>"
            io.puts "<div><span style='font-size: 200%; float: left; margin-right: 8px;'>📚</span>#{n_to_s[@@bib_summoned_books[email].size] || 'Mehrere'} Bücher, die du ausgeliehen hast, #{@@bib_summoned_books[email].size == 1 ? 'wird' : 'werden'} dringend in der Bibliothek benötigt. Bitte bring #{@@bib_summoned_books[email].size == 1 ? 'es' : 'sie'} zurück und lege #{@@bib_summoned_books[email].size == 1 ? 'es' : 'sie'} ins <a target='_blank' href='https://rundgang.gymnasiumsteglitz.de/#g114'>Rückgaberegal</a> vor der Bibliothek.</div>"
            io.puts "<hr />"
            io.puts "<a href='/bibliothek' style='white-space: nowrap;' class='float-right btn btn-sm btn-success'>Zu deinen Büchern&nbsp;<i class='fa fa-angle-double-right'></i></a>"
            io.puts "<div style='clear: both;'></div>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
    # if @@bib_unconfirmed_books[email] && (!teacher_logged_in?)
    #     n_to_s = {1 => 'Eines', 2 => 'Zwei', 3 => 'Drei', 4 => 'Vier', 5 => 'Fünf'}
    #     result += StringIO.open do |io|
    #         io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
    #         io.puts "<div class='hint'>"
    #         io.puts "<div><span style='font-size: 200%; float: left; margin-right: 8px;'>🙁</span>#{n_to_s[@@bib_unconfirmed_books[email].size] || 'Mehrere'} deiner ent&shy;lieh&shy;enen Bücher #{@@bib_unconfirmed_books[email].size == 1 ? 'wurde' : 'wurden'} von dir noch nicht bestätigt. <strong>Bitte scanne #{@@bib_unconfirmed_books[email].size == 1 ? 'das Buch' : 'die Bücher'} jetzt ein.</strong></div>"
    #         io.puts "<hr />"
    #         io.puts "<a href='/bib_confirm' style='white-space: nowrap;' class='float-right btn btn-sm btn-success'><i class='fa fa-barcode'></i>&nbsp;&nbsp;Bücher bestätigen</a>"
    #         io.puts "<div style='clear: both;'></div>"
    #         io.puts "</div>"
    #         io.puts "</div>"
    #         io.string
    #     end
    # end
    result
end


509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'src/ruby/include/techpost.rb', line 509

def ()
    require_user_who_can_manage_tablets!
    StringIO.open do |io|
        io.puts "<p>Mit einem Klick auf diesen Button kannst du dieses Gerät dauerhaft als Lehrer-Tablet anmelden.</p>"
        io.puts "<button class='btn btn-primary bu_login_teacher_tablet'><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Lehrer-Tablet-Modus aktivieren</button>"
        io.puts "<hr />"
        io.puts "<p>Bitte wähle ein order mehrere Kürzel, um dieses Gerät dauerhaft als Kurs-Tablet anzumelden.</p>"
        @@shorthands.keys.sort.each do |shorthand|
            io.puts "<button class='btn-teacher-for-kurs-tablet-login btn btn-xs btn-outline-secondary' data-shorthand='#{shorthand}'>#{shorthand}</button>"
        end
        io.puts "<br /><br >"
        io.puts "<button class='btn btn-primary bu_login_kurs_tablet' disabled><i class='fa fa-sign-in'></i>&nbsp;&nbsp;Kurs-Tablet-Modus aktivieren</button>"
        io.puts "<hr />"
        io.puts "<p>Bitte wähle ein Tablet, um dieses Gerät dauerhaft als dieses Tablet anzumelden.</p>"
        @@tablets.keys.each do |id|
            tablet = @@tablets[id]
            io.puts "<button class='btn-tablet-login btn btn-xs btn-outline-secondary' data-id='#{id}' style='background-color: #{tablet[:bg_color]}; color: #{tablet[:fg_color]};'>#{id}</button>"
        end
        io.puts "<hr />"
        io.puts "<div style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped narrow' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Typ</th>"
        io.puts "<th>Gerät</th>"
        io.puts "<th>Abmelden</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        get_sessions_for_user("lehrer.tablet@#{SCHUL_MAIL_DOMAIN}").each do |session|
            io.puts "<tr>"
            io.puts "<td>Lehrer-Tablet</td>"
            io.puts "<td>#{session[:user_agent]}</td>"
            io.puts "<td><button class='btn btn-xs btn-danger btn-purge-session' data-email='lehrer.tablet@#{SCHUL_MAIL_DOMAIN}' data-scrambled-sid='#{session[:scrambled_sid]}'>Abmelden</button></td>"
            io.puts "</tr>"
        end
        get_sessions_for_user("kurs.tablet@#{SCHUL_MAIL_DOMAIN}").each do |session|
            io.puts "<tr>"
            io.puts "<td>Kurs-Tablet (#{(session[:shorthands] || []).sort.join(', ')})</td>"
            io.puts "<td>#{session[:user_agent]}</td>"
            io.puts "<td><button class='btn btn-xs btn-danger btn-purge-session' data-email='kurs.tablet@#{SCHUL_MAIL_DOMAIN}' data-scrambled-sid='#{session[:scrambled_sid]}'>Abmelden</button></td>"
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
end


455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'src/ruby/include/techpost.rb', line 455

def print_techpost_superuser()
    require_user_who_can_manage_tablets!
    problems = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email]).map { |x| {:problem => x['v'], :email => x['u.email'], :femail => x['f.email']} }
        MATCH (v:TechProblem)-[:BELONGS_TO]->(u:User)
        OPTIONAL MATCH (v:TechProblem)-[:WILL_BE_FIXED_BY]->(f:User)
        RETURN v, u.email, f.email;
    END_OF_QUERY
    StringIO.open do |io|
        io.puts "<h3>User, die Zugriff auf diese Seite haben</h3>"
        io.puts "<div class='alert alert-danger'><code>"
        # for tech_admin in TECHNIKTEAM + CAN_MANAGE_TABLETS_USERS + ADMIN_USERS do
        #     display_name = @@user_info[tech_admin][:display_name]
        #     nc_login = @@user_info[tech_admin][:nc_login]
        #     io.puts "<img src='#{NEXTCLOUD_URL}/index.php/avatar/#{nc_login}/256' class='icon avatar-md'>&nbsp;#{display_name}"
        # end
        io.puts "</code><div class='text-muted'>Diese Funktion steht zurzeit, aufgrund eines technischen Fehlers, nicht zur Verfügung. Bitte nutzen Sie die Liste unten.</div><code>"
        io.puts "</code></div>"
        io.puts "<div class='alert alert-info'><code>#{(TECHNIKTEAM + CAN_MANAGE_TABLETS_USERS + ADMIN_USERS).uniq.sort}</code></div>"
        io.puts "<br><h3>User, die Probleme melden können (Alle oben genannten plus:)</h3>"
        io.puts "<div class='alert alert-warning'>"
        # io.puts "<div class='text-muted'>Klicke auf einen Nutzer, um ihm die Berechtigungen für das Technikamt zu entziehen.</div>"
        io.puts "<table class='table narrow table-striped' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr><td>User</td><td>Bearbeiten</td></tr>"
        io.puts "</thead><tbody>"
        for technikamt in get_technikamt do
            display_name = @@user_info[technikamt][:display_name]
             = @@user_info[technikamt][:nc_login]
            klasse = @@user_info[technikamt][:klasse]
            io.puts "<tr><td><code><img src='#{NEXTCLOUD_URL}/index.php/avatar/#{}/256' class='icon avatar-md'>&nbsp;#{display_name} (#{klasse})</code></td><td><button class='btn btn-xs btn-danger bu-edit-techpost' data-email='#{technikamt}'><i class='fa fa-trash'></i>&nbsp;&nbsp;Rechte entziehen</button>&nbsp;<button class='btn btn-xs btn-success bu-send-single-welcome-mail' data-email='#{technikamt}'><i class='fa fa-envelope'></i>&nbsp;&nbsp;Willkommens-E-Mail versenden</button></td></tr>"
        end
        io.puts "</table></tbody>"
        # io.puts "</code><div class='text-muted'>Diese Funktion steht zurzeit, aufgrund eines technischen Fehlers, nicht zur Verfügung. Bitte nutzen Sie die Liste unten.</div><code>"
        io.puts "</code></div>"
        io.puts "<div class='alert alert-warning'><code>#{get_technikamt}</code></div>"
        unless problems == []
            io.puts "<br><h3>Aktuelle Probleme im json-Format</h3>"
            for problem in problems do
                io.puts "<div class='alert alert-success'><code>#{problem.to_json}</code></div>"
            end
        end
        io.puts "<br><h3>Super Funktionen</h3>"
        io.puts "<div class='alert alert-info'>"
        io.puts "<button class='bu-clear-all btn btn-danger'><i class='fa fa-trash'></i>&nbsp;&nbsp;Alle Probleme löschen</button>"
        io.puts "<button class='bu-send-welcome-mail btn btn-warning'><i class='fa fa-envelope'></i>&nbsp;&nbsp;Willkommens-E-Mail versenden</button>"
        io.puts "</div>"
        io.puts "<div class='alert alert-info'>"
        io.puts "<div class='form-group'><input id='ti_recipients' class='form-control' placeholder='User suchen…' /><div class='recipient-input-dropdown' style='display: none;'></div></input></div>"
        io.puts "<div class='form-group row'><label for='ti_email' class='col-sm-1 col-form-label'>Name:</label><div class='col-sm-3'><input type='text' readonly class='form-control' id='ti_email' placeholder=''></div><div id='publish_message_btn_container'><button disabled id='bu_send_message' class='btn btn-outline-secondary'><i class='fa fa-plus'></i>&nbsp;&nbsp;<span>Hinzufügen</span></button>&nbsp;&nbsp;<button disabled id='bu_delete_message' class='btn btn-outline-secondary'><i class='fa fa-times'></i>&nbsp;&nbsp;<span>Entfernen</span></button></div></div>"
        io.puts "Hinweis: Die Änderungen werden erst nach dem Neuladen der Seite sichtbar.</div>"
        io.string
    end
end


2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
# File 'src/ruby/main.rb', line 2193

def print_test_klassen_chooser(active = nil)
    StringIO.open do |io|
        io.puts "<div style='margin-bottom: 15px;'>"
        klassen_for_session_user.each do |klasse|
            next if ['11', '12'].include?(klasse)
            io.puts "<a data-klasse='#{klasse}' class='btn btn-sm ttc #{klasse == active ? 'active': ''}'>#{tr_klasse(klasse)}</a>"
        end
        if user_who_can_manage_news_logged_in?
            io.puts "<hr />"
            io.puts "<a href='/manage_test_calendar' style='width: 9em;' class='btn btn-sm ttc'>Kalender verwalten</a>"
        end
        io.puts "</div>"
        io.string
    end
end


2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
# File 'src/ruby/main.rb', line 2056

def print_timetable_chooser()
    # if can_see_all_timetables_logged_in?
    #     StringIO.open do |io|
    #         io.puts "<div style='margin-bottom: 15px;'>"
    #         unless teacher_tablet_logged_in?
    #             @@klassen_order.each do |klasse|
    #                 id = @@klassen_id[klasse]
    #                 io.puts "<a data-klasse='#{klasse}' data-id='#{id}' onclick=\"window.location.href = '/timetable/#{id}' + window.location.hash;\" class='btn btn-sm ttc'>#{tr_klasse(klasse)}</a>"
    #             end
    #             io.puts '<hr />'
    #         end
    #         @@lehrer_order.each do |email|
    #             id = @@user_info[email][:id]
    #             next unless @@user_info[email][:can_log_in]
    #             io.puts "<a data-id='#{id}' onclick=\"window.location.href = '/timetable/#{id}' + window.location.hash;\" class='btn btn-sm ttc'>#{@@user_info[email][:shorthand]}</a>"
    #         end
    #         io.puts '<hr />'
    #         ROOM_ORDER.each do |room|
    #             id = room
    #             # next unless @@user_info[email][:can_log_in]
    #             io.puts "<a data-id='#{id}' onclick=\"window.location.href = '/timetable/#{id}' + window.location.hash;\" class='btn btn-sm ttc'>#{room}</a>"
    #         end
    #         io.puts "</div>"
    #         io.string
    #     end
    if kurs_tablet_logged_in?
        StringIO.open do |io|
            io.puts "<div style='margin-bottom: 15px;'>"
            @@lehrer_order.each do |email|
                next unless @session_user[:shorthands].include?(@@user_info[email][:shorthand])
                id = @@user_info[email][:id]
                next unless @@user_info[email][:can_log_in]
                io.puts "<a data-id='#{id}' onclick=\"window.location.href = '/timetable/#{id}' + window.location.hash;\" class='btn btn-sm ttc'>#{@@user_info[email][:shorthand]}</a>"
            end
            io.puts "</div>"
            io.string
        end
    elsif klassenraum_logged_in?
        StringIO.open do |io|
            io.puts "<div style='margin-bottom: 15px;'>"
            @@klassen_order.each do |klasse|
                id = @@klassen_id[klasse]
                io.puts "<a data-klasse='#{klasse}' data-id='#{id}' onclick=\"window.location.href = '/timetable/#{id}' + window.location.hash;\" class='btn btn-sm ttc'>#{tr_klasse(klasse)}</a>"
            end
            io.puts "</div>"
            io.string
        end
    elsif teacher_logged_in?
        StringIO.open do |io|
            io.puts "<div style='margin-bottom: 15px;'>"
            hidden_something = false
            temp = StringIO.open do |tio|
                @@lehrer_order.each do |email|
                    next if teacher_tablet_logged_in? && @@user_info[email][:shorthand][0] == '_'
                    id = @@user_info[email][:id]
                    next unless @@user_info[email][:can_log_in]
                    next unless can_see_all_timetables_logged_in? || email == @session_user[:email]
                    hide = (email != @session_user[:email])
                    hide = false if teacher_tablet_logged_in?
                    hidden_something = true if hide
                    style = hide ? 'display: none;' : ''
                    tio.puts "<a data-id='#{id}' onclick=\"load_timetable('#{id}'); window.selected_shorthand = '#{@@user_info[email][:shorthand]}'; \" class='btn btn-sm ttc ttc-teacher' style='#{style}'>#{@@user_info[email][:shorthand]}</a>"
                end
                tio.string
            end
            if hidden_something
                io.puts "<button class='btn btn-xs ttc bu-show-alle-teacher pull-right' style='margin-left: 0.5em; width: unset; padding: 0.25rem 0.5rem; display: inline-block;' onclick=\"$('.ttc-teacher').show(); $('.bu-show-alle-teacher').hide();\">Alle Lehrkräfte</button>"
            end
            io.puts temp
            unless teacher_tablet_logged_in?
                io.puts '<hr />'

                hidden_something = false
                all_hidden = @@klassen_order.all? do |klasse|
                    !((@@klassen_for_shorthand[@session_user[:shorthand]] || Set.new()).include?(klasse))
                end
                temp = StringIO.open do |tio|
                    @@klassen_order.each do |klasse|
                        hide = !((@@klassen_for_shorthand[@session_user[:shorthand]] || Set.new()).include?(klasse))
                        hide = false if all_hidden
                        hidden_something = true if hide
                        style = hide ? 'display: none;' : ''
                        id = @@klassen_id[klasse]
                        tio.puts "<a data-klasse='#{klasse}' data-id='#{id}' onclick=\"load_timetable('#{id}');\" class='btn btn-sm ttc ttc-klasse' style='#{style}'>#{tr_klasse(klasse)}</a>"
                    end
                    tio.string
                end
                if hidden_something
                    io.puts "<button class='btn btn-xs ttc bu-show-alle-klassen pull-right' style='margin-left: 0.5em; width: unset; padding: 0.25rem 0.5rem; display: inline-block;' onclick=\"$('.ttc-klasse').show(); $('.bu-show-alle-klassen').hide();\">Alle Klassen</button>"
                end
                io.puts temp

                io.puts '<hr />'

                hidden_something = false
                all_hidden = ROOM_ORDER.all? do |room|
                    !((@@rooms_for_shorthand[@session_user[:shorthand]] || Set.new()).include?(room))
                end
                temp = StringIO.open do |tio|
                    ROOM_ORDER.each do |room|
                        hide = !((@@rooms_for_shorthand[@session_user[:shorthand]] || Set.new()).include?(room))
                        hide = false if all_hidden
                        hidden_something = true if hide
                        style = hide ? 'display: none;' : ''
                        id = @@room_ids[room]
                        tio.puts "<a data-id='#{id}' onclick=\"load_timetable('#{id}');\" class='btn btn-sm ttc ttc-room' style='#{style}'>#{room}</a>"
                    end
                    tio.string
                end
                if hidden_something
                    io.puts "<button class='btn btn-xs ttc bu-show-alle-rooms pull-right' style='margin-left: 0.5em; width: unset; padding: 0.25rem 0.5rem; display: inline-block;' onclick=\"$('.ttc-room').show(); $('.bu-show-alle-rooms').hide();\">Alle Räume</button>"
                end
                io.puts temp
            end


            io.puts "</div>"
            io.string
        end
    elsif technikteam_logged_in?
        StringIO.open do |io|
            io.puts "<div style='margin-bottom: 15px;'>"
            temp = StringIO.open do |tio|
                ROOM_ORDER.each do |room|
                    id = @@room_ids[room]
                    tio.puts "<a data-id='#{id}' onclick=\"load_timetable('#{id}');\" class='btn btn-sm ttc ttc-room'>#{room}</a>"
                end
                tio.string
            end
            io.puts temp
            
            io.puts "</div>"
            io.string
        end
    end
end


608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
# File 'src/ruby/include/user.rb', line 608

def print_tresor_countdown_panel()
    return '' unless teacher_logged_in?
    deadline = '2023-06-26T09:00:00'
    if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline
        return StringIO.open do |io|
            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
            io.puts "<div class='hint'>"
            io.puts "<p><b>Noteneingabe im Datentresor</b></p>"
            io.puts "<hr />"
            io.puts "<p>Die Noteneingabe im Datentresor schließt am Montag um 9:00 Uhr.</p>"
            io.puts "<div id='tresor_countdown_here' style='display: none;' data-deadline='#{Time.parse(deadline).to_i}'>"
            io.puts "</div>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
    deadline = '2023-06-28T09:00:00'
    if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline
        return StringIO.open do |io|
            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
            io.puts "<div class='hint'>"
            io.puts "<p><b>Markierung von SuS in den Listen für die Zeugniskonferenzen</b></p>"
            io.puts "<hr />"
            io.puts "<p>Klassenleitungen: Bitte markieren Sie SuS, die Sie in den Zeug&shy;nis&shy;kon&shy;feren&shy;zen besprechen möchten, bis Mittwoch um 9:00 Uhr. Hinweis: Alle SuS mit einer Note ab 4– sind schon auto&shy;matisch markiert.</p>"
            io.puts "<div id='tresor_countdown_here' style='display: none;' data-deadline='#{Time.parse(deadline).to_i}'>"
            io.puts "</div>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
    deadline = '2023-07-05T12:00:00'
    if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline
        return StringIO.open do |io|
            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
            io.puts "<div class='hint'>"
            io.puts "<p><b>Eintragung der Noten für das Arbeits- und Sozialverhalten</b></p>"
            io.puts "<hr />"
            io.puts "<p>Die Möglichkeit für Eintragungen der Noten für das Arbeits- und Sozialverhalten endet am Mittwoch um 12:00 Uhr. Bitte tragen Sie bis dahin fehlende Noten ein, damit die Klassenleitungen bis zu den Zeugniskonferenzen die Listen drucken können.</p>"
            io.puts "<div id='tresor_countdown_here' style='display: none;' data-deadline='#{Time.parse(deadline).to_i}'>"
            io.puts "</div>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
    return ''
end

#public_events_tableObject



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'src/ruby/include/public_event.rb', line 106

def public_events_table()
    require_user_who_can_manage_news!
    self.class.refresh_public_event_config()
    StringIO.open do |io|
        description_for_key_and_key = {}
        @@public_event_config.each.with_index do |event, event_index|
            if event_index > 0
                io.puts "<hr />"
            end
            io.puts "<h3>#{event[:title]}</h3>"
            # if event[:description]
            #     io.puts event[:description]
            # end
            sign_ups = get_sign_ups_for_public_event(event[:key])
            io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;' data-event-key='#{event[:key]}'>"
            io.puts "<div style='display: none;' class='event-title'>#{event[:title]}</div>"
            io.puts "<table style='table-layout: fixed;' class='table table-narrow narrow'>"
            colspan = event[:rows].map { |x| x[:entries].size }.max
            io.puts "<colgroup>"
            io.puts "<col style='width: 180px;'/>"
            colspan.times do
                io.puts "<col style='min-width: 200px;'/>"
            end
            io.puts "</colgroup>"
            if event[:headings]
                io.puts "<thead>"
                io.puts "<tr>"
                io.puts "<th>#{event[:headings][0]}</th>"
                io.puts "<th colspan='#{colspan}'>#{event[:headings][1]}</th>"
                io.puts "</tr>"
                io.puts "</thead>"
            end
            io.puts "<tbody>"
            event[:rows].each do |row|
                io.puts "<tr>"
                io.puts "<th>#{row[:description]}</th>"
                row[:entries].each do |entry|
                    description_for_key_and_key[event[:key]] ||= {}
                    description_for_key_and_key[event[:key]][entry[:key]] ||= entry[:description]
                    capacity = entry[:capacity] || 0
                    booked_count = (sign_ups[entry[:key]] || []).size
                    percent = 0
                    if capacity != 0
                        percent = booked_count * 100 / capacity
                    end
                    io.puts "<td><div style='display: flex; justify-content: space-between;'><div>#{entry[:description]}</div><div>(#{booked_count}/#{capacity})</div></div><div class='progress'><div class='progress-bar progress-bar-striped' role='progressbar' style='width: #{percent}%;'>#{(percent).to_i}%</div></div></td>"
                end
                io.puts "</tr>"
            end
            io.puts "</tbody>"
            io.puts "</table>"
            io.puts "</div>"
        end
        @@public_event_config.each.with_index do |event, event_index|
            sign_ups = get_sign_ups_for_public_event(event[:key])
            next if sign_ups.empty?
            io.puts "<hr />"
            io.puts "<h4>#{event[:title]}</h4>"
            io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;' data-event-key='#{event[:key]}'>"
            io.puts "<table style='table-layout: fixed;' class='table table-narrow narrow'>"
            io.puts "<tr><th>E-Mail</th><th>Name</th><th>Track</th><th></th></tr>"
            io.puts "<tbody>"
            sign_ups.keys.sort.each do |key|
                sign_ups[key].each do |entry|
                    io.puts "<tr>"
                    io.puts "<td>#{entry[:email]}</td>"
                    io.puts "<td>#{entry[:name]}</td>"
                    io.puts "<td>#{description_for_key_and_key[event[:key]][key]}</td>"
                    io.puts "<td><button class='btn btn-xs btn-danger bu-delete-signup' data-tag='#{entry[:tag]}'><i class='fa fa-trash'></i>&nbsp;&nbsp;Löschen</button></td>"
                    io.puts "</tr>"
                end
            end
            io.puts "</tbody>"
            io.puts "</table>"
            io.puts "</div>"
        end
        io.string
    end
end

#purge_missing_sessions(current_sid = nil, remove_other = false) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'src/ruby/include/login.rb', line 301

def purge_missing_sessions(current_sid = nil, remove_other = false)
    sid = request.cookies['sid']
    existing_sids = []
    unless remove_other
        if (sid.is_a? String) && (sid =~ /^[0-9A-Za-z,]+$/)
            sids = sid.split(',')
            sids.each do |sid|
                if sid =~ /^[0-9A-Za-z]+$/
                    results = neo4j_query(<<~END_OF_QUERY, :sid => sid).map { |x| x['sid'] }
                        MATCH (s:Session {sid: $sid})-[:BELONGS_TO]->(u:User)
                        RETURN s.sid AS sid;
                    END_OF_QUERY
                    existing_sids << sid unless results.empty?
                end
            end
        end
        existing_sids.uniq!
    end
    if current_sid
        # insert current SID if it's not there yet (new sessions ID)
        unless existing_sids.include?(current_sid)
            existing_sids.unshift(current_sid)
        end
        # move current SID to front
        existing_sids -= [current_sid]
        existing_sids.unshift(current_sid)
    end
    new_cookie_value = existing_sids.join(',')
    if new_cookie_value.empty? && request.cookies['sid']
        response.delete_cookie('sid')
    end
    if (request.cookies['sid'] || '') != new_cookie_value
        response.set_cookie('sid',
                            :value => new_cookie_value,
                            :expires => Time.new + COOKIE_EXPIRY_TIME,
                            :path => '/',
                            :httponly => true,
                            :secure => DEVELOPMENT ? false : true)
    end
end

#purge_stale_second_factorsObject



763
764
765
766
767
768
769
# File 'src/ruby/include/login.rb', line 763

def purge_stale_second_factors
    neo4j_query(<<~END_OF_QUERY, :ts_now => Time.now.to_i)
        MATCH (sf:SecondFactor)
        WHERE sf.ts_expire < $ts_now
        DETACH DELETE sf;
    END_OF_QUERY
end

#recurse_arrays(path_array, value_array, prefix = [], index_prefix = [], &block) ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'src/ruby/include/zeugnisse.rb', line 279

def recurse_arrays(path_array, value_array, prefix = [], index_prefix = [], &block)
    if path_array.empty?
        yield prefix.join('/'), value_array
        return
    end
    path_entry = path_array[0]
    key = path_entry[0]
    values = path_entry[1]
    values = [values] unless values.is_a? Array
    values.each.with_index do |value, i|
        recurse_arrays(path_array[1, path_array.size - 1], value_array[i], prefix + ["#{key}:#{value}"], index_prefix + [i], &block)
    end
end

#refresh_second_factorObject



788
789
790
791
792
793
794
795
796
# File 'src/ruby/include/login.rb', line 788

def refresh_second_factor
    factors = neo4j_query(<<~END_OF_QUERY, :sid => @used_session[:sid], :ts_expire => Time.now.to_i + tresor_second_factor_ttl())
        MATCH (sf:SecondFactor)-[:BELONGS_TO]->(s:Session {sid: $sid})
        WHERE COALESCE(s.method, 'email') <> sf.method
        SET sf.ts_expire = $ts_expire
        RETURN sf;
    END_OF_QUERY
    return factors.first['sf'][:ts_expire] - Time.now.to_i
end

#require_admin!Object

Assert that an admin is logged in



211
212
213
# File 'src/ruby/include/user.rb', line 211

def require_admin!
    assert(admin_logged_in?)
end

#require_admin_2fa_hotline!Object



219
220
221
# File 'src/ruby/include/user.rb', line 219

def require_admin_2fa_hotline!
    assert(admin_2fa_hotline_logged_in?)
end

#require_device!Object



201
202
203
# File 'src/ruby/include/user.rb', line 201

def require_device!
    assert(!@session_device.nil?)
end

#require_monitor_or_user_who_can_manage_monitors!Object

Assert that an admin is logged in



301
302
303
# File 'src/ruby/include/user.rb', line 301

def require_monitor_or_user_who_can_manage_monitors!
    assert(monitor_logged_in? || user_who_can_manage_monitors_logged_in?)
end

#require_teacher!Object

Assert that a teacher is logged in



224
225
226
# File 'src/ruby/include/user.rb', line 224

def require_teacher!
    assert(teacher_logged_in?)
end

#require_teacher_for_lesson_or_ha_amt_logged_in(lesson_key) ⇒ Object



309
310
311
# File 'src/ruby/include/user.rb', line 309

def require_teacher_for_lesson_or_ha_amt_logged_in(lesson_key)
    assert(teacher_for_lesson_or_ha_amt_logged_in?(lesson_key))
end

#require_teacher_or_sv!Object

Assert that a teacher or SV is logged in



296
297
298
# File 'src/ruby/include/user.rb', line 296

def require_teacher_or_sv!
    assert(teacher_or_sv_logged_in?)
end

#require_teacher_or_user_who_can_manage_bib!Object



291
292
293
# File 'src/ruby/include/user.rb', line 291

def require_teacher_or_user_who_can_manage_bib!
    assert(teacher_or_can_manage_bib_logged_in?)
end

#require_teacher_tablet!Object

Assert that a teacher tablet is logged in



229
230
231
# File 'src/ruby/include/user.rb', line 229

def require_teacher_tablet!
    assert(teacher_tablet_logged_in?)
end

#require_technikteam!Object

Assert that a TechnikTeam user is logged in



254
255
256
# File 'src/ruby/include/user.rb', line 254

def require_technikteam!
    assert(technikteam_logged_in?)
end

#require_user!Object

Assert that a user is logged in



206
207
208
# File 'src/ruby/include/user.rb', line 206

def require_user!
    assert(user_logged_in?, 'User is logged in', true)
end

#require_user_who_can_manage_agr_app!Object

Assert that a user who can manage agrapp is logged in



283
284
285
# File 'src/ruby/include/user.rb', line 283

def require_user_who_can_manage_agr_app!
    assert(can_manage_agr_app_logged_in?)
end

#require_user_who_can_manage_antikenfahrt!Object

Assert that a user who can manage Antikenfahrt is logged in



278
279
280
# File 'src/ruby/include/user.rb', line 278

def require_user_who_can_manage_antikenfahrt!
    assert(user_who_can_manage_antikenfahrt_logged_in?)
end

#require_user_who_can_manage_bib!Object



287
288
289
# File 'src/ruby/include/user.rb', line 287

def require_user_who_can_manage_bib!
    assert(can_manage_bib_logged_in?)
end

#require_user_who_can_manage_monitors!Object

Assert that a user who can manage monitors is logged in



249
250
251
# File 'src/ruby/include/user.rb', line 249

def require_user_who_can_manage_monitors!
    assert(user_who_can_manage_monitors_logged_in?)
end

#require_user_who_can_manage_news!Object

Assert that a user who can manage news is logged in



244
245
246
# File 'src/ruby/include/user.rb', line 244

def require_user_who_can_manage_news!
    assert(user_who_can_manage_news_logged_in?)
end

#require_user_who_can_manage_salzh!Object



305
306
307
# File 'src/ruby/include/user.rb', line 305

def require_user_who_can_manage_salzh!
    assert(can_manage_salzh_logged_in?)
end

#require_user_who_can_manage_tablets!Object

Assert that a user who can manage tablets is logged in



269
270
271
# File 'src/ruby/include/user.rb', line 269

def require_user_who_can_manage_tablets!
    assert(user_who_can_manage_tablets_logged_in?)
end

#require_user_who_can_manage_tablets_or_teacher!Object



273
274
275
# File 'src/ruby/include/user.rb', line 273

def require_user_who_can_manage_tablets_or_teacher!
    assert(user_who_can_manage_tablets_or_teacher_logged_in?)
end

#require_user_who_can_report_tech_problems!Object

Assert that a techpost user is logged in



259
260
261
# File 'src/ruby/include/user.rb', line 259

def require_user_who_can_report_tech_problems!
    assert(user_who_can_report_tech_problems_logged_in?)
end

#require_user_who_can_report_tech_problems_or_better!Object

Assert that a techpost user is logged in



264
265
266
# File 'src/ruby/include/user.rb', line 264

def require_user_who_can_report_tech_problems_or_better!
    assert(user_who_can_report_tech_problems_or_better_logged_in?)
end

#require_user_who_can_upload_files!Object

Assert that a user who can upload files is logged in



239
240
241
# File 'src/ruby/include/user.rb', line 239

def require_user_who_can_upload_files!
    assert(user_who_can_upload_files_logged_in?)
end

#require_user_who_can_upload_vplan!Object

Assert that a user who can upload vplan is logged in



234
235
236
# File 'src/ruby/include/user.rb', line 234

def require_user_who_can_upload_vplan!
    assert(user_who_can_upload_vplan_logged_in?)
end

#require_zeugnis_admin!Object



215
216
217
# File 'src/ruby/include/user.rb', line 215

def require_zeugnis_admin!
    assert(zeugnis_admin_logged_in?)
end

#respond(hash = {}) ⇒ Object



1589
1590
1591
# File 'src/ruby/main.rb', line 1589

def respond(hash = {})
    @respond_hash = hash
end

#respond_raw_with_mimetype(content, mimetype) ⇒ Object



1593
1594
1595
1596
# File 'src/ruby/main.rb', line 1593

def respond_raw_with_mimetype(content, mimetype)
    @respond_content = content
    @respond_mimetype = mimetype
end

#respond_raw_with_mimetype_and_filename(content, mimetype, filename) ⇒ Object



1598
1599
1600
1601
1602
# File 'src/ruby/main.rb', line 1598

def respond_raw_with_mimetype_and_filename(content, mimetype, filename)
    @respond_content = content
    @respond_mimetype = mimetype
    @respond_filename = filename
end

#rgb_to_hex(c) ⇒ Object



74
75
76
# File 'src/ruby/include/color.rb', line 74

def rgb_to_hex(c)
    sprintf('#%02x%02x%02x', c[0].to_i, c[1].to_i, c[2].to_i)
end

#rgb_to_hsv(c) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'src/ruby/include/color.rb', line 20

def rgb_to_hsv(c)
    r = c[0] / 255.0
    g = c[1] / 255.0
    b = c[2] / 255.0
    max = [r, g, b].max
    min = [r, g, b].min
    delta = max - min
    v = max * 100

    if (max != 0.0)
        s = delta / max *100
    else
        s = 0.0
    end

    if (s == 0.0)
        h = 0.0
    else
        if (r == max)
            h = (g - b) / delta
        elsif (g == max)
            h = 2 + (b - r) / delta
        elsif (b == max)
            h = 4 + (r - g) / delta
        end

        h *= 60.0

        if (h < 0)
            h += 360.0
        end
    end
    [h, s, v]
end

#rgb_to_html(x) ⇒ Object



101
102
103
# File 'src/ruby/include/color.rb', line 101

def rgb_to_html(x)
    sprintf('#%02x%02x%02x', x[0], x[1], x[2])
end

#room_name_for_event(title, eid) ⇒ Object



82
83
84
# File 'src/ruby/include/jitsi.rb', line 82

def room_name_for_event(title, eid)
    "#{title} (#{eid[0, 8]})"
end

#running_zeugniskonferenzenObject



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'src/ruby/include/monitor.rb', line 151

def running_zeugniskonferenzen
    sha1_and_t0_list = neo4j_query(<<~END_OF_QUERY, {:t1 => Time.now.to_i}).map { |x| [x['m.sha1'], x['m.t0']] }
        MATCH (m:MonitorZeugniskonferenzState)
        WHERE m.t1 IS NULL
        RETURN m.sha1, m.t0;
    END_OF_QUERY
    sha1_list = Set.new(sha1_and_t0_list.map { |x| x[0] })
    t0_for_sha1 = {}
    sha1_and_t0_list.each do |entry|
        t0_for_sha1[entry[0]] = entry[1]
    end
    result = {}
    today = Date.today.strftime('%Y-%m-%d')
    (ZEUGNISKONFERENZEN[today] || []).each do |entry|
        sha1 = Digest::SHA1.hexdigest([today, entry].to_json)[0, 16]
        if sha1_list.include?(sha1)
            result[entry[0]] = t0_for_sha1[sha1]
        end
    end
    result
end

#sanitize_poll_items(items) ⇒ Object



561
562
563
564
565
566
567
568
# File 'src/ruby/include/poll.rb', line 561

def sanitize_poll_items(items)
    items.map do |item|
        if item['type'] == 'radio' || item['type'] == 'checkbox'
            item['answers'].reject! { |x| x.strip.empty? }
        end
        item
    end
end

#schueler_for_lesson(lesson_key) ⇒ Object



620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'src/ruby/include/directory.rb', line 620

def schueler_for_lesson(lesson_key)
    results = (@@schueler_for_lesson[lesson_key] || []).map do |email| 
        i = {}
        [:email, :first_name, :last_name, :display_name, :nc_login, :group2].each do |k| 
            i[k] = @@user_info[email][k]
        end
        i
    end
    temp = neo4j_query(<<~END_OF_QUERY, :email_addresses => (@@schueler_for_lesson[lesson_key] || []))
        MATCH (u:User)
        WHERE u.email IN $email_addresses
        RETURN u.email, COALESCE(u.group2, 'A') AS group2;
    END_OF_QUERY
    temp = Hash[temp.map { |x| [x['u.email'], x['group2']] }]
    results.map! do |x|
        x[:group2] = temp[x[:email]]
        x
    end
    results
end

#second_factor_time_leftObject



771
772
773
774
775
776
777
778
779
780
781
# File 'src/ruby/include/login.rb', line 771

def second_factor_time_left
    require_user!
    purge_stale_second_factors
    factors = neo4j_query(<<~END_OF_QUERY, :sid => @used_session[:sid])
        MATCH (sf:SecondFactor)-[:BELONGS_TO]->(s:Session {sid: $sid})
        WHERE COALESCE(s.method, 'email') <> sf.method
        RETURN sf;
    END_OF_QUERY
    return nil if factors.empty?
    return factors.first['sf'][:ts_expire] - Time.now.to_i
end

#self_tests_this_weekObject



1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
# File 'src/ruby/include/salzh.rb', line 1045

def self_tests_this_week()
    d = Date.today
    while (d.wday != 1)
        d -= 1
    end
    d0 = d.strftime('%Y-%m-%d')
    d1 = (d + 6).strftime('%Y-%m-%d')

    rows = neo4j_query(<<~END_OF_QUERY, {:d0 => d0, :d1 => d1, :email => @session_user[:email]})
        MATCH (u:User {email: $email})-[:SELF_TESTED_ON]->(std:SelfTestDay)
        WHERE std.datum >= $d0 AND std.datum <= $d1
        RETURN std.datum AS datum
        ORDER BY std.datum;
    END_OF_QUERY
    rows.map do |x|
        i = Date.parse(x['datum']) - Date.parse(d0)
        {:datum => x['datum'], :label => %w(Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag)[i]}
    end
end

#send_sms(telephone_number, message) ⇒ Object



43
44
45
46
47
48
49
50
51
52
# File 'src/ruby/include/sms.rb', line 43

def send_sms(telephone_number, message)
    data = {:telephone_number => telephone_number, :message => message}
    debug "Sending SMS: #{data.to_json}"
    @@ws_clients[:authenticated_sms].values.first[:ws].send(data.to_json)
    ds = Date.today.strftime('%Y-%m-%d')
    neo4j_query(<<~END_OF_QUERY, {:ds => ds})
        MERGE (d:SmsDay {ds: $ds})
        SET d.count = COALESCE(d.count, 0) + 1;
    END_OF_QUERY
end

#session_user_has_streaming_button?Boolean

Returns:

  • (Boolean)


1403
1404
1405
1406
1407
1408
1409
1410
# File 'src/ruby/main.rb', line 1403

def session_user_has_streaming_button?
    return false unless PROVIDE_CLASS_STREAM
    return false unless user_logged_in?
    return false if @session_user[:teacher]
    return false if class_stream_link_for_session_user.nil?
    return false unless @session_user[:homeschooling]
    return true
end

#session_user_jump_table_directionObject



511
512
513
514
515
516
517
518
# File 'src/ruby/include/user.rb', line 511

def session_user_jump_table_direction
    require_user!
    method = neo4j_query_expect_one(<<~END_OF_QUERY, :email => @session_user[:email])['method']
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.jump_table_direction, "rows") AS method;
    END_OF_QUERY
    method
end

#session_user_otp_qr_code(reveal = false) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
# File 'src/ruby/include/otp.rb', line 51

def session_user_otp_qr_code(reveal = false)
    require_user!
    otp_token = @session_user[:otp_token]
    return nil if otp_token.nil?
    return '(redacted)' unless reveal
    totp = ROTP::TOTP.new(otp_token, issuer: "Dashboard")
    uri = totp.provisioning_uri(@session_user[:email])
    qrcode = RQRCode::QRCode.new(uri, 7)
    svg = qrcode.as_svg(offset: 0, color: '000', shape_rendering: 'crispEdges',
                        module_size: 4, standalone: true)
    svg.gsub("\n", '')
end

#session_user_otp_token_good_for_tresorObject



64
65
66
67
68
69
# File 'src/ruby/include/otp.rb', line 64

def session_user_otp_token_good_for_tresor()
    require_user!
    otp_token = @session_user[:otp_token]
    return false if otp_token.nil?
    return @session_user[:otp_token_changed] < DateTime.now.strftime('%Y-%m-%d')
end

#session_user_preferred_login_methodObject



490
491
492
493
494
495
496
497
# File 'src/ruby/include/user.rb', line 490

def 
    require_user!
    method = neo4j_query_expect_one(<<~END_OF_QUERY, :email => @session_user[:email])['method']
        MATCH (u:User {email: $email})
        RETURN COALESCE(u.preferred_login_method, "email") AS method;
    END_OF_QUERY
    method
end

#session_user_telephone_numberObject



29
30
31
32
33
34
# File 'src/ruby/include/sms.rb', line 29

def session_user_telephone_number
    require_user!
    telephone_number = @session_user[:telephone_number]
    return nil if telephone_number.nil?
    return '(redacted)'
end

#session_user_telephone_number_good_for_tresorObject



36
37
38
39
40
41
# File 'src/ruby/include/sms.rb', line 36

def session_user_telephone_number_good_for_tresor()
    require_user!
    telephone_number = @session_user[:telephone_number]
    return false if telephone_number.nil?
    return @session_user[:telephone_number_changed] < DateTime.now.strftime('%Y-%m-%d')
end

#shift_hue(c, f = 60) ⇒ Object



85
86
87
88
89
# File 'src/ruby/include/color.rb', line 85

def shift_hue(c, f = 60)
    hsv = rgb_to_hsv(hex_to_rgb(c))
    hsv[0] = (hsv[0] + f) % 360.0
    rgb_to_hex(hsv_to_rgb(hsv))
end

#skytale(s, w) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
# File 'src/ruby/include/cypher.rb', line 171

def skytale(s, w)
    s = s.upcase.gsub(' ', '')
    t = ''
    (0...w).each do |x|
        i = x
        while i < s.size
            t += s[i]
            i += w
        end
    end
    t
end

#sus_logged_in?Boolean

Returns true if a SuS is logged in.

Returns:

  • (Boolean)


106
107
108
# File 'src/ruby/include/user.rb', line 106

def sus_logged_in?
    user_logged_in? && (!(@session_user[:teacher] == true))
end

#tablet_logged_in?Boolean

Returns true if a tablet is logged in.

Returns:

  • (Boolean)


131
132
133
# File 'src/ruby/include/user.rb', line 131

def tablet_logged_in?
    user_logged_in? && @session_user[:is_tablet]
end

#teacher_for_lesson_or_ha_amt_logged_in?(lesson_key) ⇒ Boolean

Returns:

  • (Boolean)


147
148
149
150
151
152
153
# File 'src/ruby/include/user.rb', line 147

def teacher_for_lesson_or_ha_amt_logged_in?(lesson_key)
    if teacher_logged_in?
        return true
    else
        return get_ha_amt_lesson_keys.include?(lesson_key)
    end
end

#teacher_logged_in?Boolean

Returns true if a teacher is logged in.

Returns:

  • (Boolean)


91
92
93
# File 'src/ruby/include/user.rb', line 91

def teacher_logged_in?
    user_logged_in? && (@session_user[:teacher] == true)
end

#teacher_or_can_manage_bib_logged_in?Boolean

Returns:

  • (Boolean)


185
186
187
# File 'src/ruby/include/user.rb', line 185

def teacher_or_can_manage_bib_logged_in?
    teacher_logged_in? || can_manage_bib_logged_in?
end

#teacher_or_sv_logged_in?Boolean

Returns true if a teacher or SV is logged in.

Returns:

  • (Boolean)


63
64
65
# File 'src/ruby/include/user.rb', line 63

def teacher_or_sv_logged_in?
    user_logged_in? && (teacher_logged_in? || @session_user[:sv])
end

#teacher_tablet_logged_in?Boolean

Returns true if a teacher tablet is logged in.

Returns:

  • (Boolean)


111
112
113
# File 'src/ruby/include/user.rb', line 111

def teacher_tablet_logged_in?
    user_logged_in? && @session_user[:is_tablet] && @session_user[:tablet_type] == :teacher
end

#technikteam_logged_in?Boolean

Returns true if a TechnikTeam is logged in.

Returns:

  • (Boolean)


33
34
35
# File 'src/ruby/include/user.rb', line 33

def technikteam_logged_in?
    user_logged_in? && @session_user[:technikteam]
end

#test_request_parameter(data, key, options) ⇒ Object



1352
1353
1354
1355
1356
1357
1358
# File 'src/ruby/main.rb', line 1352

def test_request_parameter(data, key, options)
    type = ((options[:types] || {})[key]) || String
    assert(data[key.to_s].is_a?(type), "#{key.to_s} is a #{type}")
    if type == String
        assert(data[key.to_s].size <= (options[:max_value_lengths][key] || options[:max_string_length]), 'too_much_data')
    end
end

#this_is_a_page_for_devicesObject



320
321
322
323
324
# File 'src/ruby/include/user.rb', line 320

def this_is_a_page_for_devices
    if @session_device.nil?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_adminsObject

Put this on top of a webpage to assert that this page can be opened by admins only



339
340
341
342
343
# File 'src/ruby/include/user.rb', line 339

def this_is_a_page_for_logged_in_admins
    unless admin_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_gevObject



332
333
334
335
336
# File 'src/ruby/include/user.rb', line 332

def this_is_a_page_for_logged_in_gev
    unless gev_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_teachersObject

Put this on top of a webpage to assert that this page can be opened by teachers only



353
354
355
356
357
# File 'src/ruby/include/user.rb', line 353

def this_is_a_page_for_logged_in_teachers
    unless teacher_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_teachers_or_can_manage_bibObject

Put this on top of a webpage to assert that this page can be opened by teachers only or users who can manage the library



360
361
362
363
364
# File 'src/ruby/include/user.rb', line 360

def this_is_a_page_for_logged_in_teachers_or_can_manage_bib
    unless teacher_or_can_manage_bib_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_teachers_or_svObject

Put this on top of a webpage to assert that this page can be opened by teachers or SV only



367
368
369
370
371
# File 'src/ruby/include/user.rb', line 367

def this_is_a_page_for_logged_in_teachers_or_sv
    unless teacher_or_sv_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_teachers_or_sv_or_users_who_can_manage_tabletsObject



373
374
375
376
377
# File 'src/ruby/include/user.rb', line 373

def this_is_a_page_for_logged_in_teachers_or_sv_or_users_who_can_manage_tablets
    unless teacher_or_sv_logged_in? || user_who_can_manage_tablets_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_usersObject

Put this on top of a webpage to assert that this page can be opened by logged in users only



314
315
316
317
318
# File 'src/ruby/include/user.rb', line 314

def this_is_a_page_for_logged_in_users
    unless user_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_users_who_can_manage_salzhObject



326
327
328
329
330
# File 'src/ruby/include/user.rb', line 326

def this_is_a_page_for_logged_in_users_who_can_manage_salzh
    unless can_manage_salzh_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_logged_in_zeugnis_adminsObject

Put this on top of a webpage to assert that this page can be opened by zeugnis admins only



346
347
348
349
350
# File 'src/ruby/include/user.rb', line 346

def this_is_a_page_for_logged_in_zeugnis_admins
    unless zeugnis_admin_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_people_who_can_manage_monitorsObject

Put this on top of a webpage to assert that this page can be opened by users who can manage monitors only



401
402
403
404
405
# File 'src/ruby/include/user.rb', line 401

def this_is_a_page_for_people_who_can_manage_monitors
    unless user_who_can_manage_monitors_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_people_who_can_manage_newsObject

Put this on top of a webpage to assert that this page can be opened by users who can manage news only



394
395
396
397
398
# File 'src/ruby/include/user.rb', line 394

def this_is_a_page_for_people_who_can_manage_news
    unless user_who_can_manage_news_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_people_who_can_upload_filesObject

Put this on top of a webpage to assert that this page can be opened by users who can upload files only



387
388
389
390
391
# File 'src/ruby/include/user.rb', line 387

def this_is_a_page_for_people_who_can_upload_files
    unless user_who_can_upload_files_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_people_who_can_upload_vplanObject

Put this on top of a webpage to assert that this page can be opened by users who can upload vplan only



380
381
382
383
384
# File 'src/ruby/include/user.rb', line 380

def this_is_a_page_for_people_who_can_upload_vplan
    unless user_who_can_upload_vplan_logged_in?
        redirect "#{WEB_ROOT}/", 303
    end
end

#tr_klasse(klasse) ⇒ Object



469
470
471
# File 'src/ruby/main.rb', line 469

def tr_klasse(klasse)
    KLASSEN_TR[klasse] || klasse
end

#tresor_second_factor_ttlObject



699
700
701
702
703
704
705
706
# File 'src/ruby/include/login.rb', line 699

def tresor_second_factor_ttl
    today = Date.today.strftime('%Y-%m-%d')
    if zeugnis_admin_logged_in? && ZEUGNISKONFERENZEN.include?(today)
        TRESOR_SECOND_FACTOR_TTL * 4
    else
        TRESOR_SECOND_FACTOR_TTL
    end
end

#trigger_send_invitesObject



1644
1645
1646
1647
1648
1649
1650
1651
# File 'src/ruby/main.rb', line 1644

def trigger_send_invites()
    begin
        http = Net::HTTP.new('invitation_bot', 8080)
        response = http.request(Net::HTTP::Get.new("/api/send_invites"))
    rescue StandardError => e
        STDERR.puts e
    end
end

#trigger_update(which) ⇒ Object



1626
1627
1628
1629
1630
1631
1632
1633
# File 'src/ruby/main.rb', line 1626

def trigger_update(which)
    begin
        http = Net::HTTP.new('timetable', 8080)
        response = http.request(Net::HTTP::Get.new("/api/update/#{which}"))
    rescue StandardError => e
        STDERR.puts e
    end
end

#trigger_update_imagesObject



1635
1636
1637
1638
1639
1640
1641
1642
# File 'src/ruby/main.rb', line 1635

def trigger_update_images()
    begin
        http = Net::HTTP.new('image_bot', 8080)
        response = http.request(Net::HTTP::Get.new("/api/update_all"))
    rescue StandardError => e
        STDERR.puts e
    end
end

#update_monitorsObject



54
55
56
57
# File 'src/ruby/include/monitor.rb', line 54

def update_monitors
    update_monitors_message()
    update_monitors_vplan()
end

#update_monitors_messageObject



35
36
37
38
39
40
# File 'src/ruby/include/monitor.rb', line 35

def update_monitors_message
    (@@ws_clients[:monitor] || {}).each_pair do |client_id, info|
        ws = info[:ws]
        ws.send({:command => 'update_monitor_messages', :data => get_monitor_messages}.to_json)
    end
end

#update_monitors_vplanObject



42
43
44
45
46
47
48
49
50
51
52
# File 'src/ruby/include/monitor.rb', line 42

def update_monitors_vplan
    (@@ws_clients[:monitor] || {}).each_pair do |client_id, info|
        ws = info[:ws]
        monitor_data = {:klassen => {}, :timestamp => ''}
        monitor_data_path = '/vplan/monitor/monitor.json'
        if File.exist?(monitor_data_path)
            monitor_data = JSON.parse(File.read(monitor_data_path))
        end
        ws.send({:command => 'update_vplan', :data => monitor_data}.to_json)
    end
end

#user_icon(email, c = nil) ⇒ String

Return a <div> with a background image taken from a user's Nextcloud account, with a gray background as a default fallback.

Parameters:

  • email (String)

    the user's email address

  • c (String) (defaults to: nil)

    a CSS class to apply to the div (e. g. avatar-lg)

Returns:

  • (String)

    the HTML string describing the <div>



412
413
414
# File 'src/ruby/include/user.rb', line 412

def user_icon(email, c = nil)
    "<div style='background-image: url(#{NEXTCLOUD_URL}/index.php/avatar/#{@@user_info[email][:nc_login]}/128), url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mO88h8AAq0B1REmZuEAAAAASUVORK5CYII=);' class='#{c}'></div>"
end

#user_is_eligible_for_sms?Boolean

Returns:

  • (Boolean)


19
20
21
22
23
# File 'src/ruby/include/sms.rb', line 19

def user_is_eligible_for_sms?
    return false unless user_logged_in?
    return true if SMS_AUTH_UNLOCKED_FOR.nil?
    return teacher_logged_in? || SMS_AUTH_UNLOCKED_FOR.include?(@session_user[:email])
end

#user_is_eligible_for_tresor?Boolean

Returns:

  • (Boolean)


3
4
5
6
7
8
9
10
11
# File 'src/ruby/include/tresor.rb', line 3

def user_is_eligible_for_tresor?
    return admin_logged_in?
    # return teacher_logged_in?
    # return false unless user_logged_in?
    # return false if DATENTRESOR_UNLOCKED_FOR.nil?
    # return false unless teacher_logged_in?
    # return true if teacher_logged_in? && DEVELOPMENT
    # return teacher_logged_in? && DATENTRESOR_UNLOCKED_FOR.include?(@session_user[:email])
end

#user_logged_in?Boolean

Returns true if a user is logged in.

Returns:

  • (Boolean)


3
4
5
# File 'src/ruby/include/user.rb', line 3

def user_logged_in?
    !@session_user.nil?
end

#user_who_can_manage_ags_logged_in?Boolean

Returns true if a user who can manage AGs is logged in.

Returns:

  • (Boolean)


23
24
25
# File 'src/ruby/include/user.rb', line 23

def user_who_can_manage_ags_logged_in?
    user_logged_in? && @session_user[:can_manage_ags]
end

#user_who_can_manage_antikenfahrt_logged_in?Boolean

Returns true if a user who can manage Antikenfahrt is logged in.

Returns:

  • (Boolean)


58
59
60
# File 'src/ruby/include/user.rb', line 58

def user_who_can_manage_antikenfahrt_logged_in?
    user_logged_in? && @session_user[:can_manage_antikenfahrt]
end

#user_who_can_manage_monitors_logged_in?Boolean

Returns true if a user who can manage monitors is logged in.

Returns:

  • (Boolean)


28
29
30
# File 'src/ruby/include/user.rb', line 28

def user_who_can_manage_monitors_logged_in?
    user_logged_in? && @session_user[:can_manage_monitors]
end

#user_who_can_manage_news_logged_in?Boolean

Returns true if a user who can manage news is logged in.

Returns:

  • (Boolean)


18
19
20
# File 'src/ruby/include/user.rb', line 18

def user_who_can_manage_news_logged_in?
    user_logged_in? && @session_user[:can_manage_news]
end

#user_who_can_manage_tablets_logged_in?Boolean

Returns true if a user who can manage tablets is logged in.

Returns:

  • (Boolean)


43
44
45
# File 'src/ruby/include/user.rb', line 43

def user_who_can_manage_tablets_logged_in?
    user_logged_in? && @session_user[:can_manage_tablets]
end

#user_who_can_manage_tablets_or_sv_or_teacher_logged_in?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'src/ruby/include/user.rb', line 52

def user_who_can_manage_tablets_or_sv_or_teacher_logged_in?
    return teacher_or_sv_logged_in? || user_who_can_manage_tablets_logged_in?
end

#user_who_can_manage_tablets_or_teacher_logged_in?Boolean

Returns true if a user who can manage tablets or teacher is logged in.

Returns:

  • (Boolean)


48
49
50
# File 'src/ruby/include/user.rb', line 48

def user_who_can_manage_tablets_or_teacher_logged_in?
    user_who_can_manage_tablets_logged_in? || teacher_logged_in?
end

#user_who_can_report_tech_problems_logged_in?Boolean

Returns true if a techpost user is logged in.

Returns:

  • (Boolean)


156
157
158
# File 'src/ruby/include/user.rb', line 156

def user_who_can_report_tech_problems_logged_in?
    user_logged_in? && check_has_technikamt(@session_user[:email]) == [{"hasRelation"=>true}]
end

#user_who_can_report_tech_problems_or_better_logged_in?Boolean

Returns true if a techpost or better user is logged in.

Returns:

  • (Boolean)


161
162
163
# File 'src/ruby/include/user.rb', line 161

def user_who_can_report_tech_problems_or_better_logged_in?
    user_logged_in? && (check_has_technikamt(@session_user[:email]) == [{"hasRelation"=>true}] || @session_user[:can_manage_tablets])
end

#user_who_can_upload_files_logged_in?Boolean

Returns true if a user who can upload files is logged in.

Returns:

  • (Boolean)


13
14
15
# File 'src/ruby/include/user.rb', line 13

def user_who_can_upload_files_logged_in?
    user_logged_in? && @session_user[:can_upload_files]
end

#user_who_can_upload_vplan_logged_in?Boolean

Returns true if a user who can upload vplan is logged in.

Returns:

  • (Boolean)


8
9
10
# File 'src/ruby/include/user.rb', line 8

def user_who_can_upload_vplan_logged_in?
    user_logged_in? && @session_user[:can_upload_vplan]
end

#vote_codes_from_token(vote_code, token, _count) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'src/ruby/include/vote.rb', line 73

def vote_codes_from_token(vote_code, token, _count)
    count = ((_count + 20) / 4).to_i * 4
    vote_code = sprintf('%04d', vote_code)
    codes = []
    i = 0
    while codes.size < count do
        v = "#{token}#{i}"
        code = Digest::SHA1.hexdigest(v).to_i(16).to_s(10)[0, 8]
        code.insert(1, vote_code[0])
        code.insert(4, vote_code[1])
        code.insert(7, vote_code[2])
        code.insert(10, vote_code[3])
        codes << code unless codes.include?(code)
        i += 1
    end
    codes
end

#zeugnis_admin_logged_in?Boolean

Returns:

  • (Boolean)


72
73
74
# File 'src/ruby/include/user.rb', line 72

def zeugnis_admin_logged_in?
    user_logged_in? && ZEUGNIS_ADMIN_USERS.include?(@session_user[:email])
end