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/pk5.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/tablet.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/angebote.rb,
src/ruby/include/ext_user.rb,
src/ruby/include/homework.rb,
src/ruby/include/phishing.rb,
src/ruby/include/projekte.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/development.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

.assign_projects(emails, users, projects, projects_for_klassenstufe, total_capacity, votes, _votes_by_email, _votes_by_vote, _votes_by_project, user_info) ⇒ Object



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
# File 'src/ruby/include/projekte.rb', line 744

def self.assign_projects(emails, users, projects,
    projects_for_klassenstufe, total_capacity,
    votes, _votes_by_email,
    _votes_by_vote, _votes_by_project, )
    votes_by_email = Hash[_votes_by_email.map { |a, b| [a, b.dup ] } ]
    votes_by_vote = Hash[_votes_by_vote.map { |a, b| [a, b.dup ] } ]
    votes_by_project = Hash[_votes_by_project.map { |a, b| [a, b.dup ] } ]
    # STDERR.puts "Got #{emails.size} emails"
    # STDERR.puts "Got #{projects.size} projects with a total capacity of #{total_capacity}"
    # STDERR.puts "Total capacity: #{total_capacity}"
    # STDERR.puts "Schueler: #{emails.size}"
    result = {
        :project_for_email => {},
        :error_for_email => {},
        :emails_for_project => Hash[projects.map { |k, v| [k, []] } ],
    }
    # STDERR.puts result.to_yaml
    current_vote = 3
    remaining_emails = Set.new(emails)
    # STEP 1: Assign projects by priority
    loop do
        votes_by_vote[current_vote] ||= Set.new()
        while votes_by_vote[current_vote].empty?
            current_vote -= 1
            if current_vote == 0
                break
            end
        end
        if current_vote == 0
            break
        end
        sha1 = votes_by_vote[current_vote].to_a.sample
        vote = votes[sha1]
        nr = vote[:nr]
        email = vote[:email]
        # STDERR.puts "[#{current_vote} / #{votes_by_vote[current_vote].size} left] #{sha1} => #{vote.to_json}"
        if result[:emails_for_project][nr].size < projects[nr][:capacity]
            # user can be assigned to project
            result[:emails_for_project][nr] << email
            if result[:project_for_email][email]
                raise 'argh'
            end
            remaining_emails.delete(email)
            result[:project_for_email][email] = nr
            result[:error_for_email][email] = users[email][:highest_vote] - current_vote
            # clear all entries of user
            votes_by_email[email].each do |x|
                votes_by_vote[votes[x][:vote]].delete(x)
            end
        end
        votes_by_vote[current_vote].delete(sha1)
    end
    # STDERR.puts "Assigned #{result[:project_for_email].size} of #{emails.size} users."
    # STEP 2: Randomly assign the rest
    remaining_projects = Set.new()
    projects.each_pair do |nr, p|
        if p[:capacity] - result[:emails_for_project][nr].size > 0
            remaining_projects << nr
        end
    end
    while !remaining_emails.empty?
        email = remaining_emails.to_a.sample
        klassenstufe = [email][:klassenstufe] || 7
        pool = projects_for_klassenstufe[klassenstufe] & remaining_projects
        if pool.empty?
            raise 'oops'
        end
        nr = pool.to_a.sample
        remaining_emails.delete(email)
        if result[:project_for_email][email]
            raise 'argh'
        end
        result[:project_for_email][email] = nr
        result[:emails_for_project][nr] << email
        result[:error_for_email][email] = users[email][:highest_vote] || 0
        if result[:emails_for_project][nr].size >= projects[nr][:capacity]
            remaining_projects.delete(nr)
        end
    end
    # STDERR.puts "Assigned #{result[:project_for_email].size} of #{emails.size} users."
    result
end

.check_zeugnisformular(key) ⇒ Object



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
# File 'src/ruby/include/zeugnisse.rb', line 265

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 << '#Schuljahr'
    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 << '#WeitereBemerkungen' if key.include?('sesb')
    if ZEUGNIS_HALBJAHR == '2'
        required_tags << '#Probejahr' if key == '5' || key == '7_sesb'
        required_tags << '#BBR' if key == '9' || key == '9_sesb'
        required_tags << '#MSA' if key == '10' || key == '10_sesb'
    end
    optional_tags = []

    optional_tags << '#Vorname'
    optional_tags << '#Angebote'
    optional_tags << '#Bemerkungen'
    optional_tags << '#BemerkungenAngebote'

    present_tags = Set.new(@@zeugnisse[:formulare][key][:tags])
    missing_tags = Set.new(required_tags) - present_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?
    if present_tags.include?('#Bemerkungen') && present_tags.include?('#Angebote') && !present_tags.include?('#BemerkungenAngebote')
    elsif !present_tags.include?('#Bemerkungen') && !present_tags.include?('#Angebote') && present_tags.include?('#BemerkungenAngebote')
    else
        errors << "Es muss entweder nur #Angebote und #Bemerkungen geben oder nur #BemerkungenAngebote."
    end
    return errors
end

.collect_dataObject



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
1075
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
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
# File 'src/ruby/main.rb', line 517

def self.collect_data
    @@user_info = {}
    @@login_shortcuts = {}
    @@email_for_matrix_login = {}
    @@shorthands = {}
    @@shorthand_order = []
    @@shorthands_for_fach = {}
    @@schueler_for_klasse = {}
    @@faecher = {}
    @@ferien_feiertage = []
    @@tablets = {}
    @@tablet_sets = {}
    @@lehrer_order = []
    @@klassen_order = []
    @@current_email_addresses = []
    @@antikenfahrt_recipients = {}
    @@antikenfahrt_mailing_lists = {}
    @@forschertage_recipients = {}
    @@forschertage_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],
            :geschlecht => record[:geschlecht],
            :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],
            :roles => Set.new([:teacher]),
        }
         = 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])
            next
            # 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] =~ /^\d/ ? record[:klasse].to_i : nil,
            :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],
            :roles => Set.new([:schueler]),
        }
        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

    parser.parse_extra_accounts do |record|
        if @@user_info[record[:email]]
            raise "Extra account already exists: #{record[:email]}!"
        end
        @@user_info[record[:email]] = {
            :teacher => false,
            :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],
            :geschlecht => record[:geschlecht],
            :email => record[:email],
            :can_log_in => true,
            :nc_login => record[:nc_login],
            :initial_nc_password => record[:initial_nc_password],
            :roles => Set.new(),
        }
    end

    # Now we have schueler, teacher, and extra accounts, now assign the roles
    File.open('/data/roles/roles.txt') do |f|
        role = nil
        f.each_line do |line|
            line.strip!
            next if line.empty?
            if line[0, 2] == '>>'
                role = line[2, line.size - 2].strip.downcase.to_sym
                raise "Unknown role: #{role.to_s.upcase} in /data/roles/roles.txt!" unless AVAILABLE_ROLES.include?(role)
            else
                email = line.downcase
                if @@user_info.include?(email)
                    @@user_info[email][:roles] << role
                else
                    debug "Warning: Not assigning role #{role.to_s.upcase} to unknown email address #{email}, skipping..."
                end
            end
        end
    end

    # Add roles from database
    self.get_technikamt_users.each do |email|
        @@user_info[email][:roles] << :technikamt
    end

    @@user_info.keys.each do |email|
        @@user_info[email][:role_transitive_origin] = {}
    end
    # now apply transitive roles
    loop do
        upgraded_someone = false
        ROLE_TRANSITIONS.split("\n").each do |line|
            line.strip!
            next if line[0] == '#' || line.empty?
            parts = line.split("=>")
            role_from = parts[0].strip.to_sym
            roles_to = parts[1].split(/\s+/).map { |x| x.strip }.reject { |x| x.empty? }.map { |x| x.to_sym }
            raise "Unknown role in ROLE_TRANSITIONS: #{role_from}" unless AVAILABLE_ROLES.include?(role_from)
            roles_to.each do |role|
                raise "Unknown role in ROLE_TRANSITIONS: #{role}" unless  AVAILABLE_ROLES.include?(role)
            end
            @@user_info.keys.each do |email|
                if @@user_info[email][:roles].include?(role_from)
                    roles_to.each do |role|
                        unless @@user_info[email][:roles].include?(role)
                            @@user_info[email][:role_transitive_origin][role] = role_from
                            @@user_info[email][:roles] << role
                            upgraded_someone = true
                        end
                    end
                end
            end
        end
        break unless upgraded_someone
    end

    @@users_for_role = {}
    @@user_info.each_pair do |email, info|
        info[:roles].each do |role|
            @@users_for_role[role] ||= Set.new()
            @@users_for_role[role] << 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]
        @@user_info[email][:public_id] = Digest::SHA2.hexdigest(USER_ID_SALT + 'public' + 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
    parser.parse_geschwister(@@user_info)

    @@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 || {}

    # 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, @@shorthands_for_fach = parser.parse_timetable(@@config, lesson_key_tr, @@shorthands)
    @@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, @@shorthands)
    # 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_fach = "#{fach.gsub('/', '-')}"
        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
        pretty_fach_short = "#{pretty_fach}"
        pretty_fach_short.gsub!('Sport Jungen', 'Sport')
        pretty_fach_short.gsub!('Sport Mädchen', 'Sport')
        pretty_fach_short.gsub!('Evangelische Religionslehre', 'Religion')
        pretty_fach_short.gsub!('Katholische Religionslehre', 'Religion')
        pretty_fach_short.gsub!('Politikwissenschaft', 'Politik')
        pretty_fach_short.gsub!('Informationstechnischer Grundkurs', 'ITG')
        pretty_fach_short.gsub!('Politische Bildung', 'Politik')
        pretty_fach_short.gsub!('Gesellschaftswissenschaften', 'Gewi')
        pretty_fach_short.gsub!('Naturwissenschaften', 'Nawi')
        pretty_fach_short.gsub!('Streicher Anfänger', 'AGs')
        pretty_fach_short.gsub!('Basketball', 'AGs')
        pretty_fach_short.gsub!('Schach', 'AGs')
        pretty_fach_short.gsub!('Unterstufenorchester', 'AGs')
        pretty_fach_short.gsub!('AG Garten', 'AGs')
        pretty_fach_short.gsub!('BlblA', 'AGs')
        pretty_fach_short.gsub!('neugriechisch', 'ngr')
        pretty_fach_short.gsub!('(ngr)', '')
        pretty_fach_short.gsub!('Partnersprache', 'PS')
        pretty_fach_short.gsub!('Förderstunde', '')
        pretty_fach_short.strip!

        @@lessons[:lesson_keys][lesson_key][:pretty_fach] = pretty_fach
        @@lessons[:lesson_keys][lesson_key][:pretty_fach_short] = pretty_fach_short
        @@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

    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

    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)

    @@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 = nil
        if user_has_role(email, :teacher)
            lessons = @@lessons_for_shorthand[user[:shorthand]].dup
        elsif user_has_role(email, :schueler)
            lessons = @@lessons_for_klasse[user[:klasse]].dup
        end
        if user_has_role(email, :schueler)
            if ['11', '12'].include?(user[:klasse])
                lessons = (kurse_for_schueler[email] || Set.new()).to_a
            end
        end
        lessons ||= []
        if user_has_role(email, :schueler)
            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!
        if user_has_role(email, :schueler)
            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_forschertage_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



1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
# File 'src/ruby/main.rb', line 1439

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



1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
# File 'src/ruby/main.rb', line 1349

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



1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
# File 'src/ruby/main.rb', line 1397

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',
        '/include/typewriter.js',
        '/include/bootstrap-autocomplete.min.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



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

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



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
# File 'src/ruby/include/zeugnisse.rb', line 77

def self.determine_zeugnislisten()
    # STDERR.puts "ATTENTION determine_zeugnislisten() IS DOING NOTHING RIGHT NOW"
    # return

    @@zeugnisliste_for_klasse = {}
    @@zeugnisliste_for_lehrer = {}
    # @@need_sozialverhalten is a hash of teacher shorthands and klassen
    @@need_sozialverhalten = {}

    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
    need_sv_for_klasse = {}
    ((ANLAGE_SOZIALVERHALTEN[ZEUGNIS_SCHULJAHR] || {})[ZEUGNIS_HALBJAHR] || []).each do |entry|
        if entry == '*'
            ZEUGNIS_KLASSEN_ORDER.each do |klasse|
                need_sv_for_klasse[klasse] = true
            end
        else
            if ZEUGNIS_KLASSEN_ORDER.include?(entry)
                need_sv_for_klasse[entry] = true
            else
                raise "zeugnis config: unknown klasse #{entry}"
            end
        end
    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|
            lesson_keys_for_fach[kurs[:fach]] ||= []
            lesson_keys_for_fach[kurs[:fach]] << kurs[:lesson_key]
            fach = ZEUGNIS_CONSOLIDATE_FACH[kurs[:fach]] || kurs[:fach]
            # next unless FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR].include?(fach) || FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR].include?('$' + 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
                if need_sv_for_klasse[klasse]
                    SOZIALNOTEN_KEYS.each do |item|
                        @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/#{fach}"] = true
                    end
                    @@need_sozialverhalten[shorthand] = true
                    @@need_sozialverhalten[klasse] = true
                end
            end
        end
        # get teachers from delegate entries
        (delegates_for_klasse[klasse] || {}).each_pair do |path, emails|
            fach = path.split('/')[3]
            # next unless FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR].include?(fach) || FAECHER_FOR_ZEUGNIS[ZEUGNIS_SCHULJAHR][ZEUGNIS_HALBJAHR].include?('$' + fach)
            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
                if need_sv_for_klasse[klasse]
                    SOZIALNOTEN_KEYS.each do |item|
                        @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/#{fach}"] = true
                    end
                    @@need_sozialverhalten[shorthand] = true
                    @@need_sozialverhalten[klasse] = 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
            if need_sv_for_klasse[klasse]
                SOZIALNOTEN_KEYS.each do |item|
                    @@zeugnisliste_for_lehrer[shorthand] ||= {}
                    @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/_KL"] = true
                end
                @@need_sozialverhalten[shorthand] = true
                @@need_sozialverhalten[klasse] = 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
                if need_sv_for_klasse[klasse]
                    SOZIALNOTEN_KEYS.each do |item|
                        @@zeugnisliste_for_lehrer[shorthand] ||= {}
                        @@zeugnisliste_for_lehrer[shorthand]["#{klasse}/#{item}/_KL"] = true
                    end
                    @@need_sozialverhalten[shorthand] = true
                    @@need_sozialverhalten[klasse] = 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



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
# File 'src/ruby/credentials.rb', line 354

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



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
427
428
429
430
431
432
433
434
435
436
437
# File 'src/ruby/credentials.rb', line 380

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



439
440
441
442
# File 'src/ruby/credentials.rb', line 439

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

.fix_public_event_configObject



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
# File 'src/ruby/include/public_event.rb', line 350

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",
                            :row_description => row[:description],
                            :capacity => 1,
                        }
                        t += auto_entry[:duration] * 60
                    end
                end
                rows << row
            end
            entry[:rows] = rows
        end
        entry
    end
end

.fix_stundenzeitenObject



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
# File 'src/ruby/credentials.rb', line 220

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



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/main.rb', line 479

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



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

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



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

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 toInteger(e.number), e.title;
    END_OF_QUERY
    results
end

.get_current_ab_weekObject

Returns A or B or nil



349
350
351
# File 'src/ruby/include/directory.rb', line 349

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



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

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



353
354
355
356
357
358
359
360
# File 'src/ruby/include/directory.rb', line 353

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



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'src/ruby/include/directory.rb', line 362

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
270
# 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']
        next if @@tablet_sets[tablet_set_id].nil?
        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



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

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



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

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_technikamt_usersObject



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

def self.get_technikamt_users
    results = $neo4j.neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User)-[:HAS_AMT {amt: 'technikamt'}]->(v:Techpost)
        RETURN u.email;
    END_OF_QUERY
    return results.map { |result| result["u.email"] } || []
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



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
# File 'src/ruby/include/poll.rb', line 791

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



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

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



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
# File 'src/ruby/main.rb', line 1368

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



381
382
383
384
385
386
387
388
389
390
# File 'src/ruby/include/public_event.rb', line 381

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)


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

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_has_role(email, :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



513
514
515
# File 'src/ruby/main.rb', line 513

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

.update_angebote_groupsObject



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'src/ruby/include/directory.rb', line 483

def self.update_angebote_groups()
    @@angebote_mailing_lists = {}
    $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| {:info => x['a'], :recipient => x['u.email'], :owner => x['ou.email'] } }.each do |row|
        MATCH (a:Angebot)-[:DEFINED_BY]->(ou:User)
        WITH a, ou
        OPTIONAL MATCH (u:User)-[r:IS_PART_OF]->(a)
        RETURN a, u.email, ou.email
        ORDER BY a.created DESC, a.id;
    END_OF_QUERY
        ['', 'eltern.'].each do |who|
            list_email = who + remove_accents(row[:info][:name].downcase).split(/[^a-z0-9]+/).map { |x| x.strip }.reject { |x| x.empty? }.join('-') + '@' + MAILING_LIST_DOMAIN
            @@angebote_mailing_lists[list_email] ||= {
                :label => row[:info][:name] + (who.empty? ? '' : ' (Eltern)'),
                :recipients => [],
            }
            @@angebote_mailing_lists[list_email][:recipients] << who + row[:recipient]
        end
    end
end

.update_antikenfahrt_groupsObject



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
427
428
429
430
431
432
433
# File 'src/ruby/include/directory.rb', line 387

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_forschertage_groupsObject



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
# File 'src/ruby/include/directory.rb', line 435

def self.update_forschertage_groups()
    results = $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| {:email => x['email'], :group_ft => x['group_ft'] }}
        MATCH (u:User)
        RETURN u.email AS email, COALESCE(u.group_ft, '') AS group_ft;
    END_OF_QUERY
    groups = {}
     = @@user_info
    results.each do |row|
        next unless ['nawi', 'gewi', 'musik', 'medien'].include?(row[:group_ft])
         = [row[:email]]
        next unless 
        next unless [:teacher] == false
        next unless ['5', '6'].include?([:klasse][0])
        groups[[:klasse][0]] ||= {}
        groups[[:klasse][0]][row[:group_ft]] ||= []
        groups[[:klasse][0]][row[:group_ft]] << row[:email]
    end
    @@forschertage_recipients = {
        :recipients => {},
        :groups => []
    }
    @@forschertage_mailing_lists = {}
    ['5', '6'].each do |klasse|
        ['nawi', 'gewi', 'musik', 'medien'].each do |group_ft|
            next if ((groups[klasse] || {})[group_ft] || []).empty?
            @@forschertage_recipients[:groups] << "/ft/#{klasse}/#{group_ft}/sus"
            @@forschertage_recipients[:recipients]["/ft/#{klasse}/#{group_ft}/sus"] = {
                :label => "Forschertage #{GROUP_FT_ICONS[group_ft]} – SuS #{klasse}",
                :entries => groups[klasse][group_ft]
            }
            @@forschertage_recipients[:groups] << "/af/#{klasse}/#{group_ft}/eltern"
            @@forschertage_recipients[:recipients]["/af/#{klasse}/#{group_ft}/eltern"] = {
                :label => "Forschertage #{GROUP_FT_ICONS[group_ft]} – Eltern #{klasse} (extern)",
                :external => true,
                :entries => groups[klasse][group_ft].map { |x| 'eltern.' + x }
            }
            @@forschertage_mailing_lists["forschertage.#{group_ft}.#{klasse}@#{MAILING_LIST_DOMAIN}"] = {
                :label => "Forschertage #{GROUP_FT_ICONS[group_ft]} – SuS Klassenstufe #{klasse}",
                :recipients => groups[klasse][group_ft]
            }
            @@forschertage_mailing_lists["forschertage.#{group_ft}.eltern.#{klasse}@#{MAILING_LIST_DOMAIN}"] = {
                :label => "Forschertage #{GROUP_FT_ICONS[group_ft]} – Eltern Klassenstufe #{klasse}",
                :recipients => groups[klasse][group_ft].map { |x| 'eltern.' + x }
            }
        end
    end
end

.update_mailing_listsObject



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
1213
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
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
# File 'src/ruby/main.rb', line 1187

def self.update_mailing_lists()
    self.update_antikenfahrt_groups()
    self.update_forschertage_groups()
    self.update_angebote_groups()
    self.update_projekttage_groups()
    @@mailing_lists = {}
    @@angebote_mailing_lists.each_pair do |k, v|
        @@mailing_lists[k] = v
    end
    all_kl = Set.new()
    @@klassen_order.each do |klasse|
        klasse = klasse.downcase
        next unless @@schueler_for_klasse.include?(klasse)
        @@mailing_lists["klasse.#{klasse}@#{MAILING_LIST_DOMAIN}"] = {
            :label => "SuS der Klasse #{tr_klasse(klasse)}",
            :recipients => @@schueler_for_klasse[klasse]
        }
        @@mailing_lists["eltern.#{klasse}@#{MAILING_LIST_DOMAIN}"] = {
            :label => "Eltern der Klasse #{tr_klasse(klasse)}",
            :recipients => @@schueler_for_klasse[klasse].map do |email|
                "eltern.#{email}"
            end
        }
        @@mailing_lists["lehrer.#{klasse}@#{MAILING_LIST_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}@#{MAILING_LIST_DOMAIN}"] ||= {
                    :label => "Klassenleiterteam der Klassenstufe #{klasse.to_i}",
                    :recipients => []
                }
                @@klassenleiter[klasse].each do |shorthand|
                    if @@shorthands[shorthand]
                        @@mailing_lists["team.#{klasse.to_i}@#{MAILING_LIST_DOMAIN}"][:recipients] << @@shorthands[shorthand]
                        all_kl << @@shorthands[shorthand]
                    end
                end
            end
        end
    end

    @@mailing_lists["lehrer@#{MAILING_LIST_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@#{MAILING_LIST_DOMAIN}"] = {
        :label => "Alle Schülerinnen und Schüler",
        :recipients => @@user_info.keys.select do |email|
            !@@user_info[email][:teacher]
        end
    }
    @@mailing_lists["eltern@#{MAILING_LIST_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@#{MAILING_LIST_DOMAIN}"] = {
        :label => "Alle Elternvertreter:innen",
        :recipients => temp.map { |x| 'eltern.' + x[:email] }
    }
    @@mailing_lists["kl@#{MAILING_LIST_DOMAIN}"] = {
        :label => "Alle Klassenleiter:innen",
        :recipients => all_kl.to_a.sort
    }
    @@shorthands_for_fach.each_pair do |fach, shorthands|
        @@mailing_lists["lehrer.#{fach.downcase}@#{MAILING_LIST_DOMAIN}"] = {
            :label => "Alle Lehrkräfte im Fach #{fach}",
            :recipients => shorthands.map { |x| @@shorthands[x] }
        }
    end
    @@antikenfahrt_mailing_lists.each_pair do |k, v|
        @@mailing_lists[k] = v
    end
    @@forschertage_mailing_lists.each_pair do |k, v|
        @@mailing_lists[k] = v
    end
    @@projekttage_mailing_lists.each_pair do |k, v|
        @@mailing_lists[k] = v
    end
    VERTEILER_TEST_EMAILS.each do |m|
        @@mailing_lists[m] = {
            :label => "Verteiler-Test",
            :recipients => VERTEILER_DEVELOPMENT_EMAILS
        }
    end
    projekte = {}
    $neo4j.neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (u:User)-[:ASSIGNED_TO]->(p:Projekt)
        RETURN u.email, p.nr, p.title;
    END_OF_QUERY
        nr = row['p.nr']
        projekte[nr] ||= {
            :title => row['p.title'],
            :participants => [],
            :organized_by => [],
        }
        projekte[nr][:participants] << row['u.email']
    end
    $neo4j.neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User)
        RETURN u.email, p.nr;
    END_OF_QUERY
        nr = row['p.nr']
        next unless projekte[nr]
        projekte[nr][:organized_by] << row['u.email']
    end
    projekte.each_pair do |nr, info|
        ['', 'eltern.'].each do |prefix|
            email = "#{prefix}projekt-#{nr}@#{MAILING_LIST_DOMAIN}"
            @@mailing_lists[email] = {
                :label => "Alle Teilnehmer:innen im Projekt »#{info[:title]}#{prefix == '' ? '' : ' (Eltern)'}«",
                :recipients => info[:participants].map { |x|  prefix + x },
                :extra_allowed_users => info[:organized_by],
            }
        end
    end
    if File.exist?('/internal/projekttage/votes/assign-result.json')
        assign_results = JSON.parse(File.read('/internal/projekttage/votes/assign-result.json'))
        users_for_error = {}
        assign_results['error_for_email'].each_pair do |email, error|
            users_for_error[error] ||= []
            users_for_error[error] << email
        end
        users_for_error.each_pair do |error, emails|
            emails_sorted = emails.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
            ['', 'eltern.'].each do |prefix|
                email = "#{prefix}projekt-abweichung-#{error}@#{MAILING_LIST_DOMAIN}"
                @@mailing_lists[email] = {
                    :label => "Alle Projektteilnehmer:innen mit Abweichung #{error}#{prefix == '' ? '' : ' (Eltern)'}",
                    :recipients => emails_sorted.map { |x|  prefix + x },
                }
            end
        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

.update_projekttage_groupsObject



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'src/ruby/include/directory.rb', line 503

def self.update_projekttage_groups()
    @@projekttage_mailing_lists = {}
    $neo4j.neo4j_query(<<~END_OF_QUERY).map { |x| {:recipient => x['u.email'] } }.each do |row|
        MATCH (u:User)
        WHERE NOT (u)-[:VOTED_FOR]->(:Projekt)
        RETURN u.email;
    END_OF_QUERY
        email = row[:recipient]
        next unless user_has_role(email, :schueler) && ((@@user_info[email][:klassenstufe] || 7) < 10)
        ['', 'eltern.'].each do |who|
            list_email = who + 'kein.projekt.gewaehlt' + '@' + MAILING_LIST_DOMAIN
            @@projekttage_mailing_lists[list_email] ||= {
                :label => 'Kein Projekt gewählt' + (who.empty? ? '' : ' (Eltern)'),
                :recipients => [],
            }
            @@projekttage_mailing_lists[list_email][:recipients] << who + email
        end
    end
end

.user_has_role(email, role) ⇒ Object



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

def self.user_has_role(email, role)
    assert(AVAILABLE_ROLES.include?(role), "Unknown role: #{role}")
    @@user_info[email] && @@user_info[email][:roles].include?(role)
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)


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

def admin_2fa_hotline_logged_in?
    admin_logged_in? && user_with_role_logged_in?(:datentresor_hotline)
end

#admin_logged_in?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'src/ruby/include/user.rb', line 56

def admin_logged_in?
    user_with_role_logged_in?(:admin)
end

#advent_calendar_imagesObject



2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
# File 'src/ruby/main.rb', line 2570

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



1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
# File 'src/ruby/main.rb', line 1878

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



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# File 'src/ruby/include/login.rb', line 438

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



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

def already_booked_tablet_sets_for_day(datum)
    require_user_with_role!(:can_book_tablets)
    rows = neo4j_query(<<~END_OF_QUERY, { :datum => datum }).map { |x| {:tablet_set_id => x['t.id'], :lesson_key => x['l.key'], :reason =>x['b.reason'], :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],
            :reason => row[:reason],
            :email => row[:email],
            :display_name => (@@user_info[row[:email]] || {})[:display_last_name_dativ] || (@@user_info[row[:email]] || {})[:display_name] || '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
66
67
# File 'src/ruby/include/tablet_set.rb', line 42

def already_booked_tablet_sets_for_timespan(datum, start_time, end_time)
    require_user_with_role!(:can_book_tablets)
    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'], :reason => x['b.reason'], :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, b.reason, u.email;
    END_OF_QUERY
    debug rows
    result = {}
    rows.each do |row|
        result[row[:tablet_set_id]] ||= []
        result[row[:tablet_set_id]] << {
            :lesson_key => row[:lesson_key],
            :reason => row[:reason],
            :email => row[:email],
            :display_name => (@@user_info[row[:email]] || {})[:display_last_name_dativ] || (@@user_info[row[:email]] || {})[:display_name] || 'NN'
        }
    end
    result
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



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
# File 'src/ruby/include/tablet_set.rb', line 93

def book_tablet_set_for_lesson(datum, start_time, end_time, tablet_sets = [], lesson_key, offset)
    require_user_with_role!(:can_book_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.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, reason, tablet_sets) ⇒ Object

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



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
# File 'src/ruby/include/tablet_set.rb', line 158

def book_tablet_set_for_timespan(datum, start_time, end_time, reason, 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,
                :reason => reason
            }
            # 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, reason: $reason})
                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



2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
# File 'src/ruby/main.rb', line 2252

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



46
47
48
# File 'src/ruby/include/lehrbuchverein.rb', line 46

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

#can_manage_agr_app_logged_in?Boolean

def user_who_can_report_tech_problems_or_better_logged_in?

user_logged_in? && (@session_user[:can_manage_tablets] || check_has_technikamt(@session_user[:email]))

end

Returns:

  • (Boolean)


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

def can_manage_agr_app_logged_in?
    user_with_role_logged_in?(:can_manage_agr_app)
end

#can_manage_bib_logged_in?Boolean

Returns:

  • (Boolean)


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

def can_manage_bib_logged_in?
    flag = user_with_role_logged_in?(:can_manage_bib)
    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)


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

def can_manage_bib_members_logged_in?
    user_with_role_logged_in?(:can_manage_bib_members)
end

#can_manage_bib_payment_logged_in?Boolean

Returns:

  • (Boolean)


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

def can_manage_bib_payment_logged_in?
    user_with_role_logged_in?(:can_manage_bib_payment)
end

#can_manage_bib_special_access_logged_in?Boolean

Returns:

  • (Boolean)


158
159
160
# File 'src/ruby/include/user.rb', line 158

def can_manage_bib_special_access_logged_in?
    user_with_role_logged_in?(:can_manage_bib_special_access)
end

#can_manage_salzh_logged_in?Boolean

Returns:

  • (Boolean)


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

def can_manage_salzh_logged_in?
    user_with_role_logged_in?(:can_manage_salzh)
end

#can_see_all_timetables_logged_in?Boolean

Returns:

  • (Boolean)


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

def can_see_all_timetables_logged_in?
    user_with_role_logged_in?(:can_see_all_timetables)
end

#check_has_technikamt(email) ⇒ Object



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

def check_has_technikamt(email)
    rows = 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 false if rows.empty?
    return rows.first['hasRelation']
end


1583
1584
1585
1586
1587
1588
1589
1590
# File 'src/ruby/main.rb', line 1583

def class_stream_link_for_session_user
    require_user!
    if PROVIDE_CLASS_STREAM && schueler_logged_in? && (!['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
147
148
149
# 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_lighter = rgb_to_hex(mix(hex_to_rgb(primary_color), hex_to_rgb('#ffffff'), 0.3))
    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_lighter => primary_color_lighter,
        :primary_color_much_lighter => primary_color_much_lighter,
        :primary_color_darker => primary_color_darker,
        :primary_color_much_darker => primary_color_much_darker,
        :primary_color_dark => rgb_to_hex(mix(hex_to_rgb(primary_color), [0, 0, 0], 0.8)),
        :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



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
# File 'src/ruby/include/cypher.rb', line 274

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 teacher_logged_in?
                possible_names << "#{@session_user[:first_name]} (#{@session_user[:klasse]})"
            end
            possible_names << @session_user[:display_name]
            if schueler_logged_in?
                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



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'src/ruby/include/lehrbuchverein.rb', line 30

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:

  • (Boolean)


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

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

def developer_logged_in?

user_with_role_logged_in?(:developer)

end

Returns:

  • (Boolean)


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

def external_user_logged_in?
    return !(teacher_logged_in? || schueler_logged_in?)
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_with_role_logged_in?(:can_create_events)
    # 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



207
208
209
# File 'src/ruby/include/tablet_set.rb', line 207

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



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
# File 'src/ruby/include/tablet_set.rb', line 217

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|
                debug entry[:reason]
                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#{entry[:reason] != "" ? ': ' : ''} #{entry[:reason]}."
                    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[:reason] != "" ? ': ' : ''} #{entry[:reason]}."
                    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
        unless !@@tablet_sets[x][:is_tablet_set]
            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
        end
        unless !@@tablet_sets[x][:is_tablet_set]
            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
        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 (schueler_logged_in?) && (!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] = user_has_role(use_user[:email], :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



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
# File 'src/ruby/include/poll.rb', line 943

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_angeboteObject



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
# File 'src/ruby/include/angebote.rb', line 3

def get_angebote
    # first, purge all connections to users which no longer exist
    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (a:Angebot)<-[r:IS_PART_OF]->(u:User)
        RETURN DISTINCT u.email;
    END_OF_QUERY
        email = row['u.email']
        unless @@user_info[email]
            neo4j_query(<<~END_OF_QUERY, :email => email)
                MATCH (u:User {email: $email})-[r:IS_PART_OF]->(a:Angebot)
                DELETE r;
            END_OF_QUERY
        end
    end
    angebote = neo4j_query(<<~END_OF_QUERY).map { |x| {:info => x['a'], :recipient => x['u.email'], :owner => x['ou.email'] } }
        MATCH (a:Angebot)-[:DEFINED_BY]->(ou:User)
        WITH a, ou
        OPTIONAL MATCH (u:User)-[r:IS_PART_OF]->(a)
        RETURN a, u.email, ou.email
        ORDER BY a.created DESC, a.id;
    END_OF_QUERY
    temp = {}
    temp_order = []
    angebote.each do |x|
        unless temp[x[:info][:id]]
            temp[x[:info][:id]] = {
                :recipients => [],
                :aid => x[:info][:id],
                :info => x[:info],
                :owner => x[:owner],
            }
            temp_order << x[:info][:id]
        end
        temp[x[:info][:id]][:recipients] << x[:recipient]
    end
    angebote = temp_order.map do |x|
        temp[x]
    end
    angebote.sort! do |a, b|
        a[:info][:name].downcase <=> b[:info][:name].downcase
    end
    angebote.each do |angebot|
        angebot[:recipients].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]) <=> KLASSEN_ORDER.index(@@user_info[b][:klasse]))
        end
    end
    angebote
end

#get_angebote_for_emailObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'src/ruby/include/angebote.rb', line 175

def get_angebote_for_email
    require_teacher!
    angebote = {}
    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (u:User)-[:IS_PART_OF]->(a:Angebot)-[:DEFINED_BY]->(ou:User)
        RETURN u.email, a.name, ou.email;
    END_OF_QUERY
        owner = row['ou.email']
        name = row['a.name']
        email = row['u.email']
        angebote[email] ||= []
        angebote[email] << {
            :owner => @@user_info[owner][:display_name],
            :name => name,
        }
    end
    angebote
end

#get_angebote_for_session_userObject



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

def get_angebote_for_session_user
    require_user!
    angebote = []
    neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]}).each do |row|
        MATCH (u:User {email: $email})-[:IS_PART_OF]->(a:Angebot)-[:DEFINED_BY]->(ou:User)
        RETURN a, ou.email;
    END_OF_QUERY
        owner = row['ou.email']
        angebot = row['a']
        next unless @@user_info[owner]
        angebote << {
            :owner => @@user_info[owner][:display_name],
            :name => angebot[:name],
        }
    end
    angebote.sort! do |a, b|
        a[:name].downcase <=> b[:name].downcase
    end
    angebote
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



653
654
655
656
# File 'src/ruby/include/login.rb', line 653

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

#get_emails_for_foto_pathsObject



2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
# File 'src/ruby/main.rb', line 2943

def get_emails_for_foto_paths
    require_teacher!
    results = {}
    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (u:User)
        WHERE u.foto_path IS NOT NULL
        RETURN u.email AS email, u.foto_path AS path;
    END_OF_QUERY
        results[row['path']] = row['email']
    end
    results
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 teacher_logged_in?
    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_invited_and_accepted_pk5_for_teacherObject



618
619
620
621
622
623
624
625
# File 'src/ruby/include/pk5.rb', line 618

def get_invited_and_accepted_pk5_for_teacher
    return '' unless teacher_logged_in?
    result = get_remaining_pk5_projects_for_teacher()
    invited = result[:invited]
    accepted = result[:accepted]
    left = result[:left]
    "Sie haben bisher #{accepted == 0 ? 'keine' : accepted} Prüfung#{accepted == 1 ? '' : 'en'} angenommen und #{(invited - accepted) == 0 ? 'keine' : (invited - accepted)} ausstehende Anfrage#{(invited - accepted) == 1 ? '' : 'n'}. Sie können insgesamt höchstens fünf Prüfungen annehmen, also nach aktuellem Stand noch #{left} Prüfung#{left == 1 ? '' : 'en'}."
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 && user_has_role(user, :admin))) &&
                (entry['data'] || {})['lesson_jitsi']
    end.sort { |a, b| a['start'] <=> b['start'] }
    now_time = Time.now
    old_timetable_size = timetable.size
    unless (user && user_has_role(user, :admin))
        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_has_role(email, :teacher)
            teacher_count += 1
        elsif user_has_role(email, :schueler)
            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_has_role(email, :teacher)
                [:lehrer][:count][d] ||= 0
                [:lehrer][:count][d] += 1
            elsif user_has_role(email, :schueler)
                [: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_my_pk5(email) ⇒ Object



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
# File 'src/ruby/include/pk5.rb', line 21

def get_my_pk5(email)
    require_user!
    result = nil
    neo4j_query(<<~END_OF_QUERY, {:email => email}).each do |row|
        MATCH (p:Pk5)-[:BELONGS_TO]->(u:User {email: $email})
        WITH p
        MATCH (p)-[:BELONGS_TO]->(ou:User)
        RETURN p, ou.email;
    END_OF_QUERY
        if result.nil?
            result = row['p']
            result[:sus] = []
        end
        result[:sus] << row['ou.email']
    end
    if result.nil?
        {
            :sus => [@@user_info[email][:display_name]],
        }
    else
        if result[:sus]
            result[:sus].sort! do |a, b|
                @@user_info[a][:last_name].downcase <=> @@user_info[b][:last_name].downcase
            end
            result[:sus].map! { |email| @@user_info[email][:display_name_official] }
        end
        if result[:betreuende_lehrkraft]
            result[:betreuende_lehrkraft] = result[:betreuende_lehrkraft]
            result[:betreuende_lehrkraft_display_name] = @@user_info[result[:betreuende_lehrkraft]][:display_name_official]
            result[:betreuende_lehrkraft_is_confirmed] = result[:betreuende_lehrkraft] == result[:betreuende_lehrkraft_confirmed_by]
        end
        result
    end
end

#get_next_cypher_passwordObject



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
# File 'src/ruby/include/cypher.rb', line 205

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_dont_end_on_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



428
429
430
431
432
433
434
# File 'src/ruby/include/user.rb', line 428

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



2561
2562
2563
2564
2565
2566
2567
2568
# File 'src/ruby/main.rb', line 2561

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_user_with_role!(:can_create_polls)
    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_projekteObject



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
# File 'src/ruby/include/projekte.rb', line 47

def get_projekte
    projekte = {}
    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User)
        RETURN p, u.email;
    END_OF_QUERY
        p = row['p']
        projekte[p[:nr]] ||= parse_projekt_node(p)
        projekte[p[:nr]][:organized_by] << row['u.email']
    end

    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (p:Projekt)-[:SUPERVISED_BY]->(u:User)
        RETURN p, u.email;
    END_OF_QUERY
        p = row['p']
        projekte[p[:nr]] ||= parse_projekt_node(p)
        projekte[p[:nr]][:supervised_by] << row['u.email']
    end
    projekte_list = []
    projekte.each_pair do |nr, p|
        p[:organized_by] = p[:organized_by].sort.uniq
        p[:supervised_by] = p[:supervised_by].sort.uniq
        p[:klassen_label] = ''
        if p[:min_klasse] && p[:max_klasse]
            if p[:min_klasse] == p[:max_klasse]
                p[:klassen_label] = "nur #{p[:min_klasse]}."
            else
                p[:klassen_label] = "#{p[:min_klasse]}. – #{p[:max_klasse]}."
            end
        end
        projekte_list << p
    end

    projekte_list.sort! do |a, b|
        (a[:nr].to_i == b[:nr].to_i) ?
        (a[:nr] <=> b[:nr]) :
        (a[:nr].to_i <=> b[:nr].to_i)
    end

    projekte_list
end

#get_recognized_emailsObject



2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
# File 'src/ruby/main.rb', line 2843

def get_recognized_emails()
    result = {}
    neo4j_query(<<~END_OF_QUERY, {:session_email => @session_user[:email]}).each do |x|
        MATCH (u:User {email: $session_email})-[r:RECOGNIZED]->(u2:User)
        RETURN u2.email, r.ts;
    END_OF_QUERY
        result[x['u2.email']] = x['r.ts']
    end
    result
end

#get_remaining_pk5_projects_for_teacherObject



597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'src/ruby/include/pk5.rb', line 597

def get_remaining_pk5_projects_for_teacher
    return {} unless teacher_logged_in?
    invited = 0
    accepted = 0
    neo4j_query(<<~END_OF_QUERY).each do |row|
        MATCH (p:Pk5)-[:BELONGS_TO]->(u:User)
        WITH DISTINCT p
        RETURN p;
    END_OF_QUERY
        p = row['p']
        if p[:betreuende_lehrkraft] == @session_user[:email]
            invited += 1
            if p[:betreuende_lehrkraft] == p[:betreuende_lehrkraft_confirmed_by]
                accepted += 1
            end
        end
    end
    left = 5 - accepted
    {:invited => invited, :accepted => accepted, :left => left}
end

#get_sessions_for_user(email) ⇒ Object



639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'src/ruby/include/login.rb', line 639

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_sitzplan_music_choiceObject



757
758
759
760
761
762
# File 'src/ruby/include/user.rb', line 757

def get_sitzplan_music_choice
    neo4j_query_expect_one(<<~END_OF_QUERY, :email => @session_user[:email])['u.music_choice'] || '01-preis'
        MATCH (u:User {email: $email})
        RETURN u.music_choice;
    END_OF_QUERY
end

#get_technikamt_usersObject



20
21
22
# File 'src/ruby/include/techpost.rb', line 20

def get_technikamt_users
    Main.get_technikamt_users()
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:

  • (Boolean)


88
89
90
# File 'src/ruby/include/user.rb', line 88

def gev_logged_in?
    user_with_role_logged_in?(:admin)
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]
            if schueler_logged_in?
                possible_names << "#{@session_user[:first_name]} (#{@session_user[:klasse]})"
            end
            possible_names << @session_user[:display_name]
            if schueler_logged_in?
                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



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

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



1817
1818
1819
1820
# File 'src/ruby/main.rb', line 1817

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



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'src/ruby/include/directory.rb', line 539

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



475
476
477
# File 'src/ruby/main.rb', line 475

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

#klasse_for_susObject



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

def klasse_for_sus
    assert(user_with_role_logged_in?(:can_manage_tablets) || user_with_role_logged_in?(: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



374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'src/ruby/include/user.rb', line 374

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:

  • (Boolean)


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

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:

  • (Boolean)


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

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:

  • (Boolean)


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

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

#kurs_tablet_logged_in?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'src/ruby/include/user.rb', line 108

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



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'src/ruby/include/user.rb', line 388

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



187
188
189
190
191
192
193
194
195
# File 'src/ruby/include/cypher.rb', line 187

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

#line_for_lang_dont_end_on_lang(lang) ⇒ Object



197
198
199
200
201
202
203
# File 'src/ruby/include/cypher.rb', line 197

def line_for_lang_dont_end_on_lang(lang)
    [
        "Wenn du #{lang} eingibst dann sollte es klappen",
        "#{lang} ist das naechste Passwort",
        "Du solltest es mal mit #{lang} versuchen"
    ].sample
end

#logoutObject



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

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
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
# File 'src/ruby/include/directory.rb', line 2

def mail_addresses_table(klasse)
    assert((teacher_logged_in?) || (@session_user[:klasse] == klasse))
    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>"
        # if teacher_logged_in?
        #     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>"
        # end
        io.puts "<h3>Klasse #{tr_klasse(klasse)}"
        io.puts "</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>" if teacher_logged_in?
        # 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 teacher_logged_in?
            if ['11', '12'].include?(klasse)
                io.puts "<th>Antikenfahrt</th>"
            end
            if ['5', '6'].include?(klasse[0])
                io.puts "<th>Forschertage</th>"
            end
        end
        if teacher_logged_in?
            io.puts "<th>A/B</th>"
            io.puts "<th>Letzter Zugriff</th>"
            io.puts "<th>Eltern-E-Mail-Adresse</th>"
        end
        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, COALESCE(u.group_ft, '') AS group_ft;
        END_OF_QUERY
        last_access = {}
        group2_for_email = {}
        group_af_for_email = {}
        group_ft_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']
            group_ft_for_email[x['u.email']] = x['group_ft']
        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 teacher_logged_in?
                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
            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>"
            if teacher_logged_in?
                io.puts "<td>#{Date.parse(record[:geburtstag]).strftime('%d.%m.%Y')}</td>"
            end
            # 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 teacher_logged_in?
                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
                if ['5', '6'].include?(klasse[0])
                    io.puts "<td><div class='group-ft-button #{admin_logged_in? ? '' : 'disabled'}' data-email='#{email}'>#{GROUP_FT_ICONS[group_ft_for_email[email]] || ''}</div></td>"
                end
            end
            if teacher_logged_in?
                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>"
            end
            io.puts "</tr>"
        end
        if teacher_logged_in?
            io.puts "<tr>"
            io.puts "<td colspan='4'></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='4'></td>"
            io.puts "<td colspan='2'>"
            print_email_field(io, "klasse.#{klasse}@#{MAILING_LIST_DOMAIN}")
            io.puts "</td>"
            io.puts "<td></td>"
            io.puts "<td colspan='3'>"
            print_email_field(io, "eltern.#{klasse}@#{MAILING_LIST_DOMAIN}")
            io.puts "</td>"
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        if teacher_logged_in?
            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>"
        end
        # io.puts print_stream_restriction_table(klasse)
        if teacher_logged_in?
            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>"
        end
        io.puts "<hr style='margin: 3em 0;'/>"
        if teacher_logged_in?
            io.puts "<h3>Stundenpläne der Klasse #{tr_klasse(klasse)} zum Ausdrucken</h3>"
        elsif schueler_logged_in?
            io.puts "<h3>Stundenpläne zum Ausdrucken</h3>"
            io.puts "<p>Den Hintergrund der Stundenpläne kannst über dein Profil verändern, da immer der Hintergrund aus deinem Dashboard genommen wird.</p>"
        end
        if teacher_logged_in?
            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>"
        elsif schueler_logged_in?
            io.puts "<a href='/api/get_single_timetable_pdf' target='_blank' class='btn btn-primary'><i class='fa fa-file-pdf-o'></i>&nbsp;&nbsp;PDF herunterladen</a>"
            io.puts "<a href='/api/get_single_timetable_with_png_addition_pdf' target='_blank' class='btn btn-success'><i class='fa fa-file-pdf-o'></i>&nbsp;&nbsp;PDF herunterladen (mit Symbolen)</a>"
        end
        io.puts "<hr style='margin: 3em 0;'/>"
        io.puts "<h3>Lehrkräfte 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[teacher_logged_in? ? :display_name : :display_name_official] || ''}</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
        if teacher_logged_in?
            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}@#{MAILING_LIST_DOMAIN}")
            io.puts "</td>"
            io.puts "</tr>"
        end
        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)


2492
2493
2494
# File 'src/ruby/main.rb', line 2492

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:

  • (Boolean)


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

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

#my_pk5_history(email) ⇒ Object



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
# File 'src/ruby/include/pk5.rb', line 56

def my_pk5_history(email)
    StringIO.open do |io|
        entries = neo4j_query(<<~END_OF_QUERY, {:email => email}).to_a
            MATCH (eu:User)<-[:BY]-(pc:Pk5Change)-[:TO]->(p:Pk5)-[:BELONGS_TO]->(u:User {email: $email})
            RETURN pc, eu.email
            ORDER BY pc.ts;
        END_OF_QUERY
        if entries.empty?
            io.puts "<em>(keine Einträge)</em>"
        else
            current_date = nil
            entries.each do |entry|
                pc = entry['pc']
                ts = pc[:ts]
                ts_d = Time.at(ts)
                 = "#{WEEKDAYS_LONG[ts_d.wday]}, den #{ts_d.strftime("%d.%m.%Y")}"
                if  != current_date
                    io.puts "<div class='history_date'>#{}</div>"
                    if current_date.nil?
                        io.puts "<div class='history_entry'>Vorgang erstellt durch #{@@user_info[entry['eu.email']][:display_name_official]}</div>"
                    end
                    current_date = 
                end
                if pc[:type] == 'update_value'
                    key = pc[:key].to_sym
                    value = pc[:value]
                    if key == :betreuende_lehrkraft
                        value = (@@user_info[value] || {})[:display_name_official] || value
                    end
                    if value.nil?
                        io.puts "<div class='history_entry'>#{PK5_KEY_LABELS[key]} gelöscht durch #{@@user_info[entry['eu.email']][:display_name_official]}</div>"
                    else
                        io.puts "<div class='history_entry'>#{PK5_KEY_LABELS[key]} geändert auf <strong>»#{value}«</strong> durch #{@@user_info[entry['eu.email']][:display_name_official]}</div>"
                    end
                elsif pc[:type] == 'invite_sus'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[pc[:other_email]][:display_name]}</strong> zur Gruppenprüfung eingeladen durch #{@@user_info[entry['eu.email']][:display_name_official]}</div>"
                elsif pc[:type] == 'uninvite_sus'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[pc[:other_email]][:display_name]}</strong> von der Gruppenprüfung ausgeladen durch #{@@user_info[entry['eu.email']][:display_name_official]}</div>"
                elsif pc[:type] == 'accept_invitation'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[pc[:email]][:display_name]}</strong> hat die Einladung zur Gruppenprüfung angenommen</div>"
                elsif pc[:type] == 'reject_invitation'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[pc[:email]][:display_name]}</strong> hat die Einladung zur Gruppenprüfung abgelehnt</div>"
                elsif pc[:type] == 'accept_betreuung'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[entry['eu.email']][:display_name_official]}</strong> hat die Betreuung der Prüfung angenommen</div>"
                elsif pc[:type] == 'reject_betreuung'
                    io.puts "<div class='history_entry'><strong>#{@@user_info[entry['eu.email']][:display_name_official]}</strong> hat die Betreuung der Prüfung abgelehnt</div>"
                else
                    io.puts pc.to_json
                end
            end
        end
        io.string
    end
end


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
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
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
2055
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
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
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
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
# File 'src/ruby/main.rb', line 1890

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?
            unless external_user_logged_in?
                nav_items << ['/', 'Stundenplan', 'fa fa-calendar']
            end
            if teacher_logged_in?
                nav_items << :kurse
                nav_items << :directory
            end
            if user_with_role_logged_in?(:can_receive_messages)
                nav_items << :messages
            end
            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? || user_with_role_logged_in?(:developer)
                nav_items << :admin
            end
            if (schueler_logged_in? && @session_user[:klasse] == PK5_CURRENT_KLASSE)
                nav_items << :pk5
            end
            if user_who_can_use_aula_logged_in?
                nav_items << :aula
            end
            if user_who_can_report_tech_problems_logged_in?
                unless admin_logged_in?
                    nav_items << :techteam
                end
            end
            if running_phishing_training?
                nav_items << :phishing
            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
        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>"
                    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 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='/tablets'><div class='icon'><i class='fa fa-tablet'></i></div><span class='label'>Tablets</span></a>"
                    io.puts "<a class='dropdown-item nav-icon' href='/bookings'><div class='icon'><i class='fa fa-bookmark'></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 user_with_role_logged_in?(:developer)
                    io.puts "<a class='dropdown-item nav-icon' href='/development'><div class='icon'><i class='fa fa-code'></i></div><span class='label'>Development</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>"
                    printed_something = true
                end
                if admin_logged_in? || sekretariat_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/anmeldungen'><div class='icon'><i class='fa fa-calendar-o'></i></div><span class='label'>Anmeldungen</span></a>"
                    printed_something = true
                end
                io.puts "</div>"
                io.puts "</li>"
            elsif x == :pk5
                io.puts "<li class='nav-item text-nowrap'>"
                io.puts "<a href='/pk5' class='nav-link nav-icon'><div class='icon'><i class='fa fa-file-text-o'></i></div>5. PK</a>"
                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'>"
                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>"
                sessions = all_sessions()
                if sessions.size > 1
                    io.puts "<div class='dropdown-divider'></div>"
                    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>"
                if user_with_role_logged_in?(:can_use_nextcloud)
                    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>"
                end
                if @session_user[:dark]
                    io.puts "<button class='dropdown-item nav-icon nav-light'><div class='icon'><i class='fa fa-sun-o'></i></div><span class='label'>Hell</span></button>"
                else
                    io.puts "<button class='dropdown-item nav-icon nav-dark'><div class='icon'><i class='fa fa-moon'></i></div><span class='label'>Dunkel</span></button>"
                end

                printed_divider = false
                if gev_logged_in?
                    io.puts "<div class='dropdown-divider'></div>" unless printed_divider
                    printed_divider = true
                    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 "<div class='dropdown-divider'></div>" unless printed_divider
                    printed_divider = true
                    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 "<div class='dropdown-divider'></div>" unless printed_divider
                    printed_divider = true
                    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 schueler_logged_in? || teacher_logged_in?
                    io.puts "<div class='dropdown-divider'></div>" unless printed_divider
                    printed_divider = true
                    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
                if user_logged_in?
                    if teacher_logged_in?
                        io.puts "<a class='dropdown-item nav-icon' href='/projekttage_sus'><div class='icon'><i class='fa fa-rocket'></i></div><span class='label'>Projekttage</span></a>"
                    elsif schueler_logged_in?
                        if @session_user[:klasse] == '11'
                            if projekttage_phase() >= 1
                                io.puts "<a class='dropdown-item nav-icon' href='/projekttage_orga'><div class='icon'><i class='fa fa-rocket'></i></div><span class='label'>Projekttage</span></a>"
                            end
                        elsif user_eligible_for_projekt_katalog?
                            if projekttage_phase() >= 2
                                io.puts "<a class='dropdown-item nav-icon' href='/projekttage_sus'><div class='icon'><i class='fa fa-rocket'></i></div><span class='label'>Projekttage</span></a>"
                            end
                        end
                    end
                end
                if teacher_logged_in?
                    io.puts "<a class='dropdown-item nav-icon' href='/pk5_overview'><div class='icon'><i class='fa fa-file-text-o'></i></div><span class='label'>5. PK</span></a>"
                end
                if schueler_logged_in?
                    if @session_user[:klasse].to_i < 11
                        io.puts "<a class='dropdown-item nav-icon' href='/directory/#{@session_user[:klasse]}'><div class='icon'><i class='fa fa-users'></i></div><span class='label'>Meine Klasse</span></a>"
                    end
                end
                if teacher_logged_in? || user_with_role_logged_in?(:can_create_events) || user_with_role_logged_in?(:can_create_polls) || user_with_role_logged_in?(:can_use_mailing_lists)
                    io.puts "<div class='dropdown-divider'></div>"
                    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='/klassenmemory_assign'><div class='icon'><i class='fa fa-user'></i></div><span class='label'>Klassenmemory</span></a>"
                        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
                    if user_with_role_logged_in?(:can_create_events)
                        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>"
                    end
                    if user_with_role_logged_in?(:can_create_polls)
                        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>"
                    end
                    # 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>"
                    if user_with_role_logged_in?(:can_use_mailing_lists)
                        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>"
                    end
                    if teacher_logged_in?
                        io.puts "<a class='dropdown-item nav-icon' href='/angebote'><div class='icon'><i class='fa fa-group'></i></div><span class='label'>AGs und Angebote</span></a>"
                    end
                    io.puts "<a class='dropdown-item nav-icon' href='/groups'><div class='icon'><i class='fa fa-group'></i></div><span class='label'>Meine Gruppen</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
                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
                remaining_klassen = KLASSEN_ORDER.dup
                klassen = @@klassen_for_shorthand[@session_user[:shorthand]] || []
                if user_who_can_manage_antikenfahrt_logged_in?
                    klassen << '11'
                    klassen << '12'
                    klassen.uniq!
                end
                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>"
                    remaining_klassen.delete(klasse)
                end
                unless remaining_klassen.empty?
                    io.puts "<div class='dropdown-divider'></div>"
                    remaining_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
                end
                io.puts "</div>"
                io.puts "</li>"
            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>"
            elsif x == :phishing
                io.puts "<li class='nav-item text-nowrap'>"
                io.puts "<a class='nav-link nav-icon' href='/phishing'><div class='icon'>🦈</div>Phishing Prävention</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

#need_sozialverhaltenObject



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

def need_sozialverhalten()
    # return true if session user has sozialnoten to enter
    return false unless user_logged_in?
    return false unless teacher_logged_in?
    return true if @@need_sozialverhalten[@session_user[:shorthand]]
    return false
end

#parse_paths_and_values(paths, values) ⇒ Object



342
343
344
345
346
347
348
# File 'src/ruby/include/zeugnisse.rb', line 342

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

#parse_projekt_node(p) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'src/ruby/include/projekte.rb', line 30

def parse_projekt_node(p)
    {
        :nr => p[:nr],
        :title => p[:title],
        :description => p[:description],
        :photo => p[:photo],
        :exkursion_hint => p[:exkursion_hint],
        :extra_hint => p[:extra_hint],
        :categories => p[:categories],
        :min_klasse => p[:min_klasse],
        :max_klasse => p[:max_klasse],
        :capacity => p[:capacity],
        :organized_by => [],
        :supervised_by => [],
    }
end

#parse_request_data(options = {}) ⇒ Object



1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
# File 'src/ruby/main.rb', line 1531

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

#pending_pk5_invitations_outgoing(user_email) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'src/ruby/include/pk5.rb', line 424

def pending_pk5_invitations_outgoing(user_email)
    pending_invitations = neo4j_query(<<~END_OF_QUERY, {:email => user_email}).to_a
        MATCH (ou:User {email: $email})<-[:BELONGS_TO]-(p:Pk5)-[r:INVITATION_FOR]->(u:User)
        RETURN u.email;
    END_OF_QUERY
    return '' if pending_invitations.empty?
    StringIO.open do |io|
        io.puts "<hr>"
        pending_invitations.each do |row|
            io.puts "<p>Du hast <strong>#{@@user_info[row['u.email']][:display_name]}</strong> für eine Gruppenprüfung eingeladen.</p>"
            io.puts "<p>"
            io.puts "<button class='btn btn-danger bu-delete-invitation' data-email='#{row['u.email']}'><i class='fa fa-times'></i>&nbsp;&nbsp;Einladung zurücknehmen</button>"
            io.puts "</p>"
        end
        io.puts "<hr>"
        io.string
    end
end

#pick_random_color_scheme(go_wild = false) ⇒ Object



2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
# File 'src/ruby/main.rb', line 2514

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


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
# File 'src/ruby/include/user.rb', line 537

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
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
# 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 mb-1' href='#teachers'>Lehrerinnen und Lehrer</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#sus'>Schülerinnen und Schüler</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#external_users'>Externe Nutzer</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#website'>Website</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#tablets'>Tablets</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#monitor'>Monitor</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='#bibliothek'>Bibliothek</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/sus_ohne_kurse'>SuS ohne Kurse</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/api/all_sus_logo_didact'>LDC: Alle SuS</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/api/all_lul_logo_didact'>LDC: Alle Lehrkräfte</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/api/all_sus_untis'>Untis: Alle SuS</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/api/all_kurse_untis'>Untis: Alle Kurse</a>"
        io.puts "<a class='btn btn-secondary mb-1' href='/api/get_room_timetable_pdf'>Alle Raumpläne (PDF)</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' style='padding-top: 0.4em;' 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='external_users'>Externe Nutzer</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>Anmelden</th>"
        io.puts "<th>2FA</th>"
        io.puts "<th>Sessions</th>"
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        @@user_info.keys.sort.each do |email|
            user = @@user_info[email]
            next if user[:roles].include?(:teacher) || user[:roles].include?(:schueler)
            io.puts "<tr class='user_row'>"
            # io.puts "<td>#{user_icon(email, 'avatar-md')}</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><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 "<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 "<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


2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
# File 'src/ruby/main.rb', line 2581

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


2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
# File 'src/ruby/main.rb', line 2622

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


2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
# File 'src/ruby/main.rb', line 2673

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


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
# File 'src/ruby/include/admin.rb', line 395

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


452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'src/ruby/include/admin.rb', line 452

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


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

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


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
# File 'src/ruby/include/admin.rb', line 358

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


343
344
345
346
347
348
349
350
351
# File 'src/ruby/include/admin.rb', line 343

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


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
# File 'src/ruby/include/admin.rb', line 757

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


2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
# File 'src/ruby/main.rb', line 2638

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


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
# File 'src/ruby/include/poll.rb', line 881

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
        AND pr.visible <> "no"
        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


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
# File 'src/ruby/include/development.rb', line 2

def print_dev_stats()
    require_user_with_role!(:developer)
    dark = neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User {dark: true})
        RETURN COUNT(u) AS userCount;
    END_OF_QUERY

    new_design = neo4j_query(<<~END_OF_QUERY)
        MATCH (u:User {new_design: true})
        RETURN COUNT(u) AS userCount;
    END_OF_QUERY

    StringIO.open do |io|
        io.puts "<h3>User, die Zugriff auf diese Seite haben</h3>"
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
        io.puts "<table class='table narrow table-striped' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr><td>User</td></tr>"
        io.puts "</thead><tbody>"
        for tech_admin in @@users_for_role[:developer].uniq.sort do
            next unless @@user_info[tech_admin]
            display_name = @@user_info[tech_admin][:display_name]
             = @@user_info[tech_admin][:nc_login]
            io.puts "<tr><td><code><img src='#{NEXTCLOUD_URL}/index.php/avatar/#{}/256' class='icon avatar-md'>&nbsp;#{display_name}</code></td></tr>"
        end
        io.puts "</tbody></table>"
        io.puts "</div></div>"
        io.puts "<h3>Statistiken</h3>"
        io.puts "<p>Anzahl der Nutzer, die das neue Design nutzen: #{new_design[0]["userCount"]}</p>"
        io.puts "<p>Anzahl der Nutzer, die den Dark-Mode nutzen: #{dark[0]["userCount"]}</p>"
        io.puts "<h3>Phishing Status</h3>"
        io.puts "<code>"
        io.puts "<p>PHISHING_HINT_START = '#{PHISHING_HINT_START}'<br>PHISHING_HINT_END = '#{PHISHING_HINT_END}'</p>"
        io.puts "<p>PHISHING_START = '#{PHISHING_START}'<br>PHISHING_END = '#{PHISHING_END}'</p>"
        io.puts "<p>PHISHING_POLL_RUN_ID = '#{PHISHING_POLL_RUN_ID}'</p>"
        io.puts "<p>PHISHING_RECEIVING_DATE = '#{PHISHING_RECEIVING_DATE}'</p>"
        io.puts "</code>"
        io.string
    end
end


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
# File 'src/ruby/include/admin.rb', line 536

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


2265
2266
2267
# File 'src/ruby/main.rb', line 2265

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


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
# File 'src/ruby/include/projekte.rb', line 618

def print_free_projekt_spots
    require_user!
    StringIO.open do |io|
        projekt_for_email = {}
        projekte = {}

        neo4j_query(<<~END_OF_QUERY).each do |row|
            MATCH (u:User)-[:ASSIGNED_TO]->(p:Projekt)
            RETURN u.email, p.nr, p;
        END_OF_QUERY
            email = row['u.email']
            projekt_for_email[row['u.email']] = row['p.nr']
            projekte[row['p.nr']] ||= row['p']
        end

        sus_for_projekt = {}
        projekt_for_email.each_pair do |email, nr|
            sus_for_projekt[nr] ||= []
            sus_for_projekt[nr] << email
        end

        io.puts "<h4>Freie Plätze in anderen Projekten</h4>"
        io.puts "<p>Wenn du lieber in ein anderes Projekt wechseln möchtest, schreib einfach eine E-Mail bis <strong>Mittwoch, den 10. Juli um 16:00 Uhr</strong> an <a href='mailto:#{WEBSITE_MAINTAINER_EMAIL}'>#{WEBSITE_MAINTAINER_EMAIL}</a>. Momentan sind noch folgende Plätze frei:</p>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm table-striped' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Projekt</th>"
        io.puts "<th>Klasse</th>"
        io.puts "<th>Freie Plätze</th>"
        io.puts "</tr>"
        projekte.each_pair do |nr, projekt|
            if sus_for_projekt[nr].size < projekt[:capacity]
                io.puts "<tr>"
                io.puts "<td>#{projekt[:title]}</td>"
                if projekt[:min_klasse] == projekt[:max_klasse]
                    io.puts "<td>nur #{tr_klasse(projekt[:min_klasse])}. Klasse</td>"
                else
                    io.puts "<td>#{tr_klasse(projekt[:min_klasse])}. – #{tr_klasse(projekt[:max_klasse])}. Klasse</td>"
                end
                io.puts "<td>#{projekt[:capacity] - sus_for_projekt[nr].size} von #{projekt[:capacity]} frei</td>"
                io.puts "</tr>"
            end
        end
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
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
# 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?
            deliver_mail do
                to @@users_for_role[:gev]
                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
            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


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
# File 'src/ruby/include/lehrbuchverein.rb', line 50

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


2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
# File 'src/ruby/main.rb', line 2273

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


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
# File 'src/ruby/include/admin.rb', line 291

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

post '/api/create_aula_lights' do

require_user_who_can_use_aula!
data = parse_request_data(:required_keys => [:dmx, :number])
neo4j_query(<<~END_OF_QUERY, :dmx => data[:dmx], :desk => data[:desk])
    CREATE (e:AulaLight)
    SET e.dmx = $dmx
END_OF_QUERY
respond(:result => 'lefromage')

end



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'src/ruby/include/aula.rb', line 238

def print_light_structure()
    require_user_who_can_use_aula!
    file_path = "/internal/aula_light/current_config.txt"

    if File.exist?(file_path)
        current_config = File.read(file_path)
    else
        current_config = ""
    end
    
    StringIO.open do |io|
        io.puts "#{current_config}"
        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


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
# File 'src/ruby/include/directory.rb', line 791

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}'>"
    emails = []
    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|
        emails << 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 "<tr class='user_row'>"
    io.puts "<td>Bei Verteiler-Ausfall (bitte in BCC)</td>"
    io.puts "<td colspan='2'>"
    print_email_field(io, emails.join('; '))
    io.puts "</td>"
    io.puts "</tr>"
io.puts "</tbody>"
end


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
# File 'src/ruby/include/directory.rb', line 830

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}@#{MAILING_LIST_DOMAIN}",
             "eltern.#{klasse}@#{MAILING_LIST_DOMAIN}",
             "lehrer.#{klasse}@#{MAILING_LIST_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}@#{MAILING_LIST_DOMAIN}"
                        if @@mailing_lists[list_email]
                            print_mailing_list(io, list_email)
                            remaining_mailing_lists.delete(list_email)
                        end
                    end
                end
            end
            if ['5', '6'].include?(klasse)
                ['nawi', 'gewi', 'musik', 'medien'].each do |group_ft|
                    ['', '.eltern'].each do |extra|
                        list_email = "forschertage.#{group_ft}#{extra}.#{klasse}@#{MAILING_LIST_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}@#{MAILING_LIST_DOMAIN}"
            print_mailing_list(io, list_email)
            remaining_mailing_lists.delete(list_email)
        end
        print_mailing_list(io, "kl@#{MAILING_LIST_DOMAIN}")
        remaining_mailing_lists.delete("kl@#{MAILING_LIST_DOMAIN}")
        io.puts "<tr><th colspan='3'>Gesamte Schule</th></tr>"
        ["sus@#{MAILING_LIST_DOMAIN}",
         "lehrer@#{MAILING_LIST_DOMAIN}",
         "eltern@#{MAILING_LIST_DOMAIN}",
         "ev@#{MAILING_LIST_DOMAIN}",
        ].each do |list_email|
            print_mailing_list(io, list_email)
            remaining_mailing_lists.delete(list_email)
        end
        io.puts "<tr><th colspan='3'>Forschertage</th></tr>"
        [5, 6].each do |klasse|
            ['gewi', 'medien', 'musik', 'nawi'].each do |group_ft|
                ['', '.eltern'].each do |extra|
                    list_email = "forschertage.#{group_ft}#{extra}.#{klasse}@#{MAILING_LIST_DOMAIN}"
                    if @@mailing_lists[list_email]
                        print_mailing_list(io, list_email)
                        remaining_mailing_lists.delete(list_email)
                    end
                end
            end
        end
        unless remaining_mailing_lists.empty?
            io.puts "<tr><th colspan='3'>Weitere E-Mail-Verteiler</th></tr>"
            remaining_mailing_lists.to_a.sort do |a, b|
                ia = @@mailing_lists[a]
                ib = @@mailing_lists[b]
                ia[:label].downcase <=> ib[:label].downcase
            end.each do |list_email|
                print_mailing_list(io, list_email)
            end
        end
        io.puts "</table>"
        io.string
    end
end


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
# File 'src/ruby/include/cypher.rb', line 729

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


2269
2270
2271
# File 'src/ruby/main.rb', line 2269

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


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

def print_pending_pk5_invitations_incoming(user_email)
    pending_invitations = neo4j_query(<<~END_OF_QUERY, {:email => user_email}).to_a
        MATCH (ou:User)<-[:BELONGS_TO]-(p:Pk5)-[r:INVITATION_FOR]->(u:User {email: $email})
        RETURN ou.email, ID(p) AS id;
    END_OF_QUERY
    return '' if pending_invitations.empty?
    StringIO.open do |io|
        io.puts "<hr>"
        invitations = {}
        pending_invitations.each do |row|
            invitations[row['id']] ||= []
            invitations[row['id']] << row['ou.email']
        end
        invitations.values.each do |emails|
            io.puts "<p>Du hast eine Einladung von <strong>#{join_with_sep(emails.map { |x| @@user_info[x][:display_name]}, ', ', ' und ')}</strong> für eine Gruppenprüfung erhalten.</p>"
            io.puts "<p>"
            io.puts "<button class='btn btn-success bu-accept-invitation' data-email='#{emails.first}'><i class='fa fa-check'></i>&nbsp;&nbsp;Einladung annehmen</button>"
            io.puts "<button class='btn btn-danger bu-reject-invitation' data-email='#{emails.first}'><i class='fa fa-times'></i>&nbsp;&nbsp;Einladung ablehnen</button>"
            io.puts "</p>"
        end
        io.puts "<hr>"
        io.string
    end
end


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
# File 'src/ruby/include/phishing.rb', line 144

def print_phishing_groups_table
  nutzerzahlen = {
    maennlich: {
      "5/6" => 0,
      "7/8" => 0,
      "9/10" => 0,
      "11/12" => 0,
      "Lehrkraft" => 0
    },
    weiblich: {
      "5/6" => 0,
      "7/8" => 0,
      "9/10" => 0,
      "11/12" => 0,
      "Lehrkraft" => 0
    }
  }

  @@user_info.each_value do |user|
      if user_has_role(user[:email], :teacher)
          if user[:geschlecht] == 'm'
              nutzerzahlen[:maennlich]["Lehrkraft"] += 1
          elsif user[:geschlecht] == 'w'
              nutzerzahlen[:weiblich]["Lehrkraft"] += 1
          end
      elsif user_has_role(user[:email], :schueler)
          klassenstufe = user[:klassenstufe] || 7
          gruppe = "#{klassenstufe <= 6 ? '5/6' : klassenstufe <= 8 ? '7/8' : klassenstufe <= 10 ? '9/10' : '11/12'}"
          if user[:geschlecht] == 'm'
              nutzerzahlen[:maennlich][gruppe] += 1
          elsif user[:geschlecht] == 'w'
              nutzerzahlen[:weiblich][gruppe] += 1
          end
      end
  end

  current_group = teacher_logged_in? ? "Lehrkraft" : "#{@session_user[:klassenstufe] <= 6 ? '5/6' : @session_user[:klassenstufe] <= 8 ? '7/8' : @session_user[:klassenstufe] <= 10 ? '9/10' : '11/12'}"
  current_gender = @session_user[:geschlecht] == 'm' ? :maennlich : :weiblich

  return StringIO.open do |io|
      io.puts "<p>
      Du warst für die Statistik in folgender der zehn Gruppen: <b>#{current_group}, #{current_gender == :maennlich ? 'männlich' : current_gender}</b>
      <div class='row'>
          <div class='col-md-12'>
              <div style='max-width: 100%; overflow-x: auto;'>
              <table class='table narrow'>
                  <thead>
                      <tr>
                          <th>Geschlecht</th>
                          <th>Klassenstufe</th>
                          <th></th>
                          <th></th>
                          <th></th>
                          <th></th>
                      </tr>
                  </thead>
                  <tbody>
                      <tr>
                          <td>männlich (#{nutzerzahlen[:maennlich].values.sum})</td>
                          <td class='#{'marked' if current_gender == :maennlich && current_group == '5/6'}'>5/6 (#{nutzerzahlen[:maennlich]["5/6"]})</td>
                          <td class='#{'marked' if current_gender == :maennlich && current_group == '7/8'}'>7/8 (#{nutzerzahlen[:maennlich]["7/8"]})</td>
                          <td class='#{'marked' if current_gender == :maennlich && current_group == '9/10'}'>9/10 (#{nutzerzahlen[:maennlich]["9/10"]})</td>
                          <td class='#{'marked' if current_gender == :maennlich && current_group == '11/12'}'>11/12 (#{nutzerzahlen[:maennlich]["11/12"]})</td>
                          <td class='#{'marked' if current_gender == :maennlich && current_group == 'Lehrkraft'}'>Lehrkraft (#{nutzerzahlen[:maennlich]["Lehrkraft"]})</td>
                      </tr>
                      <tr>
                          <td>weiblich (#{nutzerzahlen[:weiblich].values.sum})</td>
                          <td class='#{'marked' if current_gender == :weiblich && current_group == '5/6'}'>5/6 (#{nutzerzahlen[:weiblich]["5/6"]})</td>
                          <td class='#{'marked' if current_gender == :weiblich && current_group == '7/8'}'>7/8 (#{nutzerzahlen[:weiblich]["7/8"]})</td>
                          <td class='#{'marked' if current_gender == :weiblich && current_group == '9/10'}'>9/10 (#{nutzerzahlen[:weiblich]["9/10"]})</td>
                          <td class='#{'marked' if current_gender == :weiblich && current_group == '11/12'}'>11/12 (#{nutzerzahlen[:weiblich]["11/12"]})</td>
                          <td class='#{'marked' if current_gender == :weiblich && current_group == 'Lehrkraft'}'>Lehrkraft (#{nutzerzahlen[:weiblich]["Lehrkraft"]})</td>
                      </tr>
                  </tbody>
              </table>
              </div>
          </div>
      </div>
      </p>"
      io.string
      end
end


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
# File 'src/ruby/include/phishing.rb', line 2

def print_phishing_mail
    require_user!
    us =
    "<div class='text-comment'>
    <div class='from'>
        <h4><mark title='Rechtschreibfehler'>Dein</mark> E-Mail-Adresse wurde für eine Löschung markiert</h4>
        <p>
            <b>Dashboard Gymnasium Steglitz</b> &lt;noreply@<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>&gt;<br>
            <b>Via</b> '<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>'<br>
            <b>An</b> #{@session_user[:email]}<br>
        </p>
        <p>
            <button class='btn btn-outline-primary'>Antworten</button>
            <button class='btn btn-outline-primary'>Allen antworten</button>
            <button class='btn btn-outline-success'>Weiterleiten</button>
            <button class='btn btn-outline-danger'>Löschen</button>
        </p>
    </div>
    <div class='message'>
        <p>Hallo!</p>
        <p>Da Deine E-Mail-Adresse in der Datenbank nicht mehr vorkommt, wurde <mark title='Rechtschreibfehler'>es</mark> für eine <b>Löschung</b> markiert.</p>
        <p><mark title='Drohung' class='threat'>Bitte melde <mark title='Rechtschreibfehler'>dich sich</mark> über diesen <a href='phishing'>Link</a> im Dashboard an, wenn du den Vorgang abbrechen möchtest.</mark></p>
        <p>Der Link ist personalisiert und enthält persönliche Infos, gib diesen Link auf keinen Fall weiter.</p>
        <p>Wenn du alle Schritte richtig gemacht hast leuchtet dieser <mark title='Rechtschreibfehler'>Hacken</mark> grün auf: ☑️</p>
    </div>
    </div>"
    ms1 =
    "<div class='text-comment'>
                <div class='from'>
                    <h4>Deine E-Mail-Adresse wurde für eine Löschung markiert</h4>
                    <p>
                        <b>Dashboard Gymnasium Steglitz</b> &lt;noreply@<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>&gt;<br>
                        <b>Via</b> '<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>'<br>
                        <b>An</b> #{@session_user[:email]}<br>
                    </p>
                    <p>
                        <button class='btn btn-outline-primary'>Antworten</button>
                        <button class='btn btn-outline-primary'>Allen antworten</button>
                        <button class='btn btn-outline-success'>Weiterleiten</button>
                        <button class='btn btn-outline-danger'>Löschen</button>
                    </p>
                </div>
                <div class='message'>
                    <p>Hallo!</p>
                    <p>Da Deine E-Mail-Adresse in der Datenbank nicht mehr vorkommt, wurde sie für eine <b>Löschung</b> markiert.</p>
                    <p><mark title='Drohung' class='threat'>Bitte melde <mark title='Rechtschreibfehler'>dich sich</mark> über diesen <a href='phishing'>Link</a> im Dashboard an, wenn du den Vorgang abbrechen möchtest.</mark></p>
                    <p>Der Link ist personalisiert und enthält persönliche Infos, gib diesen Link auf keinen Fall weiter.</p>
                    <p>Viele Grüße,<br>Dashboard Gymnasium Steglitz</p>
                </div>
                </div>"
    ms2 =
    "<div class='text-comment'>
                <div class='from'>
                    <h4>Deine E-Mail-Adresse wurde für eine Löschung markiert</h4>
                    <p>
                        <b>Dashboard Gymnasium Steglitz</b> &lt;noreply@<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>&gt;<br>
                        <b>Via</b> '<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>'<br>
                        <b>An</b> #{@session_user[:email]}<br>
                    </p>
                    <p>
                        <button class='btn btn-outline-primary'>Antworten</button>
                        <button class='btn btn-outline-primary'>Allen antworten</button>
                        <button class='btn btn-outline-success'>Weiterleiten</button>
                        <button class='btn btn-outline-danger'>Löschen</button>
                    </p>
                </div>
                <div class='message'>
                    <p>Hallo!</p>
                    <p>Da Deine E-Mail-Adresse in der Datenbank nicht mehr vorkommt, wurde sie für eine <b>Löschung</b> markiert.</p>
                    <p><mark title='Drohung' class='threat'>Bitte melde dich über diesen <a href='phishing'>Link</a> im Dashboard an, wenn du den Vorgang abbrechen möchtest.</mark></p>
                    <p>Der Link ist personalisiert und enthält persönliche Infos, gib diesen Link auf keinen Fall weiter.</p>
                    <p>Viele Grüße,<br>Dashboard Gymnasium Steglitz</p>
                </div>
                </div>"
    os =
    "<div class='text-comment'>
                <div class='from'>
                    <h4>Deine E-Mail-Adresse wurde für eine Löschung markiert</h4>
                    <p>
                        <b>Dashboard Gymnasium Steglitz</b> &lt;noreply@<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>&gt;<br>
                        <b>Via</b> '<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>'<br>
                        <b>An</b> #{@session_user[:email]}<br>
                    </p>
                    <p>
                        <button class='btn btn-outline-primary'>Antworten</button>
                        <button class='btn btn-outline-primary'>Allen antworten</button>
                        <button class='btn btn-outline-success'>Weiterleiten</button>
                        <button class='btn btn-outline-danger'>Löschen</button>
                    </p>
                </div>
                <div class='message'>
                    <p>Hallo!</p>
                    <p>Da Deine E-Mail-Adresse in der Datenbank nicht mehr vorkommt, wurde sie für eine <b>Löschung</b> markiert.</p>
                    <p><mark title='Drohung' class='threat'>Bitte melde dich über diesen <a href='phishing'>Link</a> im Dashboard an, wenn du den Vorgang abbrechen möchtest.</mark></p>
                    <p>Der Link ist personalisiert und enthält persönliche Infos, gib diesen Link auf keinen Fall weiter.</p>
                    <p>Viele Grüße,<br>Dashboard Gymnasium Steglitz</p>
                </div>
                </div>"
    teacher =
    "<div class='text-comment'>
    <div class='from'>
        <h4>Ihre E-Mail-Adresse wurde für eine Löschung markiert</h4>
        <p>
            <b>Dashboard Gymnasium Steglitz</b> &lt;noreply@<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>&gt;<br>
            <b>Via</b> '<mark title='Keine offizielle Schul-Domain'>steglitzdashboard.de</mark>'<br>
            <b>An</b> #{@session_user[:email]}<br>
        </p>
        <p>
            <button class='btn btn-outline-primary'>Antworten</button>
            <button class='btn btn-outline-primary'>Allen antworten</button>
            <button class='btn btn-outline-success'>Weiterleiten</button>
            <button class='btn btn-outline-danger'>Löschen</button>
        </p>
    </div>
    <div class='message'>
        <p>Hallo!</p>
        <p>Da Ihre E-Mail-Adresse in der Datenbank nicht mehr vorkommt, wurde sie für eine <b>Löschung</b> markiert.</p>
        <p><mark title='Drohung' class='threat'>Bitte melden Sie sich über diesen <a href='[link]'>Link</a> im Dashboard an, wenn Sie den Vorgang abbrechen möchten.</mark></p>
        <p>Der Link ist personalisiert und enthält persönliche Infos, geben Sie diesen Link auf keinen Fall weiter.</p>
        <p>Viele Grüße<br>Dashboard Gymnasium Steglitz</p>
    </div>
    </div>"

    if running_phishing_training? || user_with_role_logged_in?(:developer)
        return StringIO.open do |io|
            if [5, 6].include?(@session_user[:klassenstufe])
                io.puts us
            elsif [7, 8].include?(@session_user[:klassenstufe])
                io.puts ms1
            elsif [9, 10].include?(@session_user[:klassenstufe])
                io.puts ms2
            elsif [11, 12].include?(@session_user[:klassenstufe])
                io.puts os
            else
                io.puts teacher
            end
            io.string
        end
    end
    return ''
end


683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
# File 'src/ruby/include/user.rb', line 683

def print_phishing_panel()
    if running_phishing_training_hint?
        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>Phishing Prävention</b></p>"
            io.puts "<hr />"
            io.puts "<p>Die Statistiken zu der E-Mail vom #{PHISHING_RECEIVING_DATE} sind nun online.</p>"
            io.puts "<p><a href='/phishing' class='btn btn-primary'>Phishing Prävention&nbsp;<i class='fa fa-angle-double-right'></i></a></p>"
            io.puts "<p>Du kannst auch an unserer Umfrage teilnehmen.</p>"
            io.puts "<p><button class='btn btn-success bu-launch-poll' data-poll-run-id='#{PHISHING_POLL_RUN_ID}'>Zur Umfrage&nbsp;<i class='fa fa-angle-double-right'></i></button></p>"
            io.puts "</div>"
            io.puts "</div>"
            io.string
        end
    end
    return ''
end


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
# File 'src/ruby/include/projekte.rb', line 341

def print_projekt_assigned_sus
    projekt = nil
    neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]}).each do |row|
        MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User {email: $email})
        RETURN p;
    END_OF_QUERY
        projekt = row['p']
    end
    return '' if projekt.nil? || projekt[:min_klasse].nil? || projekt[:max_klasse].nil? || projekt[:capacity].nil?

    sus = []
    neo4j_query(<<~END_OF_QUERY, {:nr => projekt[:nr]}).each do |row|
        MATCH (u:User)-[r:ASSIGNED_TO]->(p:Projekt {nr: $nr})
        RETURN u.email, r;
    END_OF_QUERY
        email = row['u.email']
        next unless @@user_info[email]
        next unless @@user_info[email][:roles].include?(:schueler)
        sus << email
    end
    sus.sort! do |a, b|
        ia = @@user_info[a]
        ib = @@user_info[b]
        ia[:klassenstufe] ||= 7
        ib[:klassenstufe] ||= 7
        (ia[:klassenstufe] == ib[:klassenstufe]) ?
        (ia[:klasse] <=> ib[:klasse]) :
        (ia[:klassenstufe] <=> ib[:klassenstufe])
    end

    StringIO.open do |io|
        io.puts "<p>Die folgenden Schülerinnen und Schüler nehmen an deinem Projekt teil. Unten in der Tabelle findest du E-Mail-Verteiler, die du nutzen kannst, um alle Teilnehmer:innen und / oder deren Eltern zu erreichen. Nutze deine schulische E-Mail-Adresse, um die Verteiler zu verwenden.</p>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Nr.</th>"
        io.puts "<th></th>"
        io.puts "<th>Name</th>"
        io.puts "<th>Klasse</th>"
        io.puts "<th style='width: 30em;'>E-Mail</th>"
        io.puts "</tr>"
        sus.each.with_index do |email, i|
            io.puts "<tr class='user_row'>"
            io.puts "<td>#{i + 1}.</td>"
            io.puts "<td><div class='icon nav_avatar'>#{user_icon(email, 'avatar-md')}</div></td>"
            io.puts "<td>#{@@user_info[email][:display_name]}</td>"
            io.puts "<td>#{tr_klasse(@@user_info[email][:klasse])}</td>"
            io.write "<td>"
            print_email_field(io, email)
            io.write "</td>"
            io.puts "</tr>"
        end
        ['', 'eltern.'].each do |prefix|
            io.puts "<tr class='user_row'>"
            io.puts "<td colspan='4'><em>E-Mail-Verteiler: #{prefix == 'eltern.' ? 'Alle Eltern eurer Teilnehmer:innen' : 'Alle Teilnehmer:innen'}</em></td>"
            io.write "<td>"
            print_email_field(io, "#{prefix}projekt-#{projekt[:nr]}@#{MAILING_LIST_DOMAIN}")
            io.write "</td>"
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"

        io.string
    end
end


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
# File 'src/ruby/include/projekte.rb', line 212

def print_projekt_interesse
    projekt = nil
    neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]}).each do |row|
        MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User {email: $email})
        RETURN p;
    END_OF_QUERY
        projekt = row['p']
    end
    return '' if projekt.nil? || projekt[:min_klasse].nil? || projekt[:max_klasse].nil? || projekt[:capacity].nil?

    votes = {}
    neo4j_query(<<~END_OF_QUERY, {:nr => projekt[:nr]}).each do |row|
        MATCH (u:User)-[r:VOTED_FOR]->(p:Projekt {nr: $nr})
        RETURN u.email, r;
    END_OF_QUERY
        email = row['u.email']
        next unless @@user_info[email]
        next unless @@user_info[email][:roles].include?(:schueler)
        klassenstufe = @@user_info[email][:klassenstufe] || 7
        vote = row['r'][:vote]
        key = "#{klassenstufe}/#{vote}"
        votes[key] ||= 0
        votes[key] += 1
        key = "klassenstufe/#{klassenstufe}"
        votes[key] ||= 0
        votes[key] += 1
        key = "vote/#{vote}"
        votes[key] ||= 0
        votes[key] += 1
        key = "total"
        votes[key] ||= 0
        votes[key] += 1
    end

    StringIO.open do |io|
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Klassenstufe</th>"
        (projekt[:min_klasse]..projekt[:max_klasse]).each do |klasse|
            io.puts "<th class='#{klasse == projekt[:min_klasse] ? 'cbl' : ''}' style='text-align: center;'>#{klasse}.</th>"
        end
        io.puts "<th class='cbl' style='text-align: center;'>Σ</th>"
        io.puts "</tr>"
        ndash = "<span class='text-muted'>&ndash;</span>"
        [3, 2, 1].each do |vote|
            io.puts "<tr>"
            io.puts "<td>#{PROJEKT_VOTE_CODEPOINTS[vote].chr(Encoding::UTF_8)} #{PROJEKT_VOTE_LABELS[vote]}</td>"
            (projekt[:min_klasse]..projekt[:max_klasse]).each do |klasse|
                count = votes["#{klasse}/#{vote}"] || ndash
                io.puts "<td class='#{klasse == projekt[:min_klasse] ? 'cbl' : ''}' style='text-align: center;'>#{count}</td>"
            end
            count = votes["vote/#{vote}"] || ndash
            io.puts "<td class='cbl' style='text-align: center;'>#{count}</td>"
            io.puts "</tr>"
        end
        io.puts "<tr>"
        io.puts "<td>Σ</td>"
        (projekt[:min_klasse]..projekt[:max_klasse]).each do |klasse|
            count = votes["klassenstufe/#{klasse}"] || ndash
            io.puts "<td class='#{klasse == projekt[:min_klasse] ? 'cbl' : ''}' style='text-align: center;'>#{count}</td>"
        end
        count = votes["total"] || ndash
        io.puts "<td class='cbl' style='text-align: center;'>#{count}</td>"
        io.puts "</tr>"
        io.puts "</table>"
        io.puts "</div>"
        io.string
    end
end


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
# File 'src/ruby/include/projekte.rb', line 283

def print_projekt_interesse_stats
    projekt = nil
    neo4j_query(<<~END_OF_QUERY, {:email => @session_user[:email]}).each do |row|
        MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User {email: $email})
        RETURN p;
    END_OF_QUERY
        projekt = row['p']
    end
    return '' if projekt.nil? || projekt[:min_klasse].nil? || projekt[:max_klasse].nil? || projekt[:capacity].nil?

    data = nil
    begin
        data = JSON.parse(File.read("/internal/projekttage/votes/project-#{projekt[:nr]}.json"))
    rescue
    end
    ts_data = nil
    begin
        ts_data = JSON.parse(File.read("/internal/projekttage/votes/ts.json"))
    rescue
    end
    return '' if ts_data.nil?

    StringIO.open do |io|
        io.puts "<h4>Vorschau deiner Projektgruppe</h4>"
        io.puts "<p>Aktuell würde deine Projektgruppe ungefähr wie folgt aussehen:</p>"
        io.puts "<ul style='list-style: disc; margin-left: 1.5em;'>"
        io.puts "<li>#{data['geschlecht_m'] + data['geschlecht_w']} Teilnehmer:innen, davon #{data['geschlecht_m']} Jungen und #{data['geschlecht_w']} Mädchen</li>"
        io.puts "<li>Klassenstufen:<ul style='list-style: disc; margin-left: 1.5em;'>"
        x = ((projekt[:min_klasse] || 5)..(projekt[:max_klasse] || 9)).select do |klasse|
            (data['klasse'][klasse.to_s] || 0) > 0
        end.map do |klasse|
            "<li>#{data['klasse'][klasse.to_s]} Kind#{data['klasse'][klasse.to_s] > 1 ? 'er' : ''} aus der #{klasse}. Klasse</li>"
        end
        io.puts x.join('')
        io.puts "</ul></li>"
        io.puts "<li>Motivation:<ul style='list-style: disc; margin-left: 1.5em;'>"
        x = [3, 2, 1, 0].select do |vote|
            (data['vote'][vote.to_s] || 0) > 0
        end.map do |vote|
            "<li>#{data['vote'][vote.to_s]} Kind#{data['vote'][vote.to_s] > 1 ? 'er' : ''} mit der Wahl: #{PROJEKT_VOTE_CODEPOINTS[vote].chr(Encoding::UTF_8)} »#{PROJEKT_VOTE_LABELS[vote]}«</li>"
        end
        io.puts x.join('')
        io.puts "</ul></li>"
        io.puts "</ul>"
        if data['vote']['0'] * 100 / (data['geschlecht_m'] + data['geschlecht_w']) > 10
            io.puts "<p>Hinweis: Du kannst die Motivation deiner Teilnehmer:innen erhöhen, indem du ggfs. deinen Titel, deinen Werbetext und / oder dein Projektbild aktualisierst.</p>"
        end
        io.puts "<p>Bitte beachte, dass sich die Zusammensetzung deiner Gruppe noch ändern wird, abhängig vom weiteren Wahlverhalten, Umwahlen oder Anpassungen in eurem Projekt.</p>"
        io.puts "<p>Bisher haben #{ts_data['email_count_voted']} von #{ts_data['email_count_total']} Schülerinnen und Schülern ihre Projekte gewählt:"
        io.puts "<div class='progress'>"
        p = ts_data['email_count_voted'] * 100 / ts_data['email_count_total']
        io.puts "<div class='bg-success progress-bar progress-bar-striped progress-bar-animated' role='progressbar' style='width: #{p}%;'>#{p.round}%</div>"
        io.puts "</div>"
        io.puts "</p>"
        io.string
    end
end


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
# File 'src/ruby/include/projekte.rb', line 667

def print_projekttage_assignment_summary
    return '' unless teacher_logged_in?
    color_for_error = ['#4aa03f', '#fad31c', '#f4951b', '#bc2326']
    StringIO.open do |io|
        projekt_for_email = {}
        projekte = {}
        assign_results = JSON.parse(File.read('/internal/projekttage/votes/assign-result.json'))

        neo4j_query(<<~END_OF_QUERY).each do |row|
            MATCH (u:User)-[:ASSIGNED_TO]->(p:Projekt)
            RETURN u.email, p.nr, p;
        END_OF_QUERY
            email = row['u.email']
            projekt_for_email[row['u.email']] = row['p.nr']
            projekte[row['p.nr']] ||= row['p']
        end

        sus_for_projekt = {}
        projekt_for_email.each_pair do |email, nr|
            sus_for_projekt[nr] ||= []
            sus_for_projekt[nr] << email
        end

        io.puts "<h4>Freie Plätze</h4>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm table-striped' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Projekt</th>"
        io.puts "<th>Klasse</th>"
        io.puts "<th>Freie Plätze</th>"
        io.puts "</tr>"
        projekte.each_pair do |nr, projekt|
            if sus_for_projekt[nr].size < projekt[:capacity]
                io.puts "<tr>"
                io.puts "<td>#{projekt[:title]}</td>"
                if projekt[:min_klasse] == projekt[:max_klasse]
                    io.puts "<td>nur #{tr_klasse(projekt[:min_klasse])}. Klasse</td>"
                else
                    io.puts "<td>#{tr_klasse(projekt[:min_klasse])}. – #{tr_klasse(projekt[:max_klasse])}. Klasse</td>"
                end
                io.puts "<td>#{projekt[:capacity] - sus_for_projekt[nr].size} von #{projekt[:capacity]} frei</td>"
                io.puts "</tr>"
            end
        end
        io.puts "</table>"
        io.puts "</div>"

        KLASSEN_ORDER.each do |klasse|
            klassenstufe = klasse.to_i
            klassenstufe = 7 if klassenstufe == 0
            next unless klassenstufe < 10
            io.puts "<h4>Klasse #{tr_klasse(klasse)}</h4>"
            io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
            io.puts "<table class='table table-sm table-striped' style='width: unset;'>"
            io.puts "<tr>"
            io.puts "<th style='text-align: right;'>Nr.</th>"
            io.puts "<th></th>"
            io.puts "<th>Name</th>"
            io.puts "<th>Projekt</th>"
            io.puts "</tr>"
            @@schueler_for_klasse[klasse].each.with_index do |email, index|
                error = assign_results['error_for_email'][email]
                io.puts "<tr>"
                io.puts "<td style='text-align: right;'>#{index + 1}.</td>"
                io.puts "<td style='text-align: center;'><span style='color: #{color_for_error[error]};'>⬤</span></td>"
                io.puts "<td>#{@@user_info[email][:display_name]}</td>"
                io.puts "<td>#{projekte[projekt_for_email[email]][:title]}</td>"
                io.puts "</tr>"
            end
            io.puts "</table>"
            io.puts "</div>"
        end

        io.string
    end
end


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
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
# File 'src/ruby/include/projekte.rb', line 414

def print_projekttage_vote_summary
    return '' unless teacher_logged_in?
    StringIO.open do |io|
        votes = {}
        votes_for_projekt = {}
        emails = Set.new()
        neo4j_query(<<~END_OF_QUERY).each do |row|
            MATCH (u:User)-[r:VOTED_FOR]->(p:Projekt)
            RETURN u.email, r, p.nr;
        END_OF_QUERY
            email = row['u.email']
            emails << email
            next unless @@user_info[email]
            next unless @@user_info[email][:roles].include?(:schueler)
            klassenstufe = @@user_info[email][:klassenstufe] || 7
            klassenstufe = 'WK' if @@user_info[email][:klasse][0, 2] == 'WK'
            vote = row['r'][:vote]
            key = "#{klassenstufe}/#{vote}"
            votes[key] ||= 0
            votes[key] += 1
            key = "#{row['p.nr']}/#{klassenstufe}"
            votes_for_projekt[key] ||= 0
            votes_for_projekt[key] += 1
            key = "#{row['p.nr']}/#{vote}"
            votes_for_projekt[key] ||= 0
            votes_for_projekt[key] += 1
            key = "#{row['p.nr']}"
            votes_for_projekt[key] ||= 0
            votes_for_projekt[key] += 1
            key = "votes_by_email/#{email}"
            votes_for_projekt[key] ||= 0
            votes_for_projekt[key] += 1
        end

        KLASSEN_ORDER.each do |klasse|
            @@schueler_for_klasse[klasse].each do |email|
                key = "votes_by_email/#{email}"
                count = votes_for_projekt[key] || 0
                count = 10 if count > 10
                key = "votes_by_klasse/#{@@user_info[email][:klasse]}/#{count}"
                votes_for_projekt[key] ||= 0
                votes_for_projekt[key] += 1
            end
        end

        io.puts "<h4>Projizierte Zusammensetzung der Projektgruppen</h4>"
        io.puts "<p>Aus dieser Tabelle lässt sich ganz gut ablesen, welche Projekte gut ankommen und welche eher weniger.</p>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Projekt</th>"
        [5, 6, 7, 8, 9, 3, 2, 1, 0, 'm', 'w', 'Σ'].each do |klasse|
            if [0, 1, 2, 3].include?(klasse)
                io.puts "<th class='#{[5, 3, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{PROJEKT_VOTE_CODEPOINTS[klasse].chr(Encoding::UTF_8)}</th>"
            else
                io.puts "<th class='#{[5, 3, 'm', 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{klasse}#{['WK', 'Σ'].include?(klasse) ? '' : '.'}</th>"
            end
        end
        io.puts "</tr>"
        ndash = "<span class='text-muted'>&ndash;</span>"
        ts_data = nil
        begin
            ts_data = JSON.parse(File.read("/internal/projekttage/votes/ts.json"))
        rescue
            return ''
        end
        all_project_data = {}
        get_projekte.each do |p|
            all_project_data[p[:nr]] = {}
            begin
                all_project_data[p[:nr]] = JSON.parse(File.read("/internal/projekttage/votes/project-#{p[:nr]}.json"))
            rescue
            end
        end
        get_projekte.sort do |a, b|
            score_for_project(b, all_project_data[b[:nr]]) <=> score_for_project(a, all_project_data[a[:nr]])
        end.each do |projekt|
            next if projekt[:capacity] == 0
            project_data = all_project_data[projekt[:nr]]
            io.puts "<tr>"
            io.puts "<td>#{projekt[:title]}</td>"
            [5, 6, 7, 8, 9].each do |klasse|
                io.puts "<td class='#{[5, 3, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{(project_data['klasse'] || {})[klasse.to_s] || ndash }</td>"
            end
            [3, 2, 1, 0].each do |vote|
                io.puts "<td class='#{[5, 3, 'Σ'].include?(vote) ? 'cbl' : ''}' style='text-align: center;'>#{(project_data['vote'] || {})[vote.to_s] || ndash }</td>"
            end
            io.puts "<td class='cbl' style='text-align: center;'>#{project_data['geschlecht_m'] || ndash}</td>"
            io.puts "<td style='text-align: center;'>#{project_data['geschlecht_w'] || ndash}</td>"
            io.puts "<td class='cbl' style='text-align: center;'>#{(project_data['geschlecht_m'] || 0) + (project_data['geschlecht_w'] || 0)}</td>"
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"
        io.puts "<h4>Projizierte Fehlerverteilung</h4>"
        io.puts "<p>Aus den projizierten Gruppen ergibt sich eine Fehlerverteilung. Der Fehler bei einer Projektzuordnung berechnet sich aus der Differenz zwischen dem höchsten Level, welches von einem SuS gewählt wurde und dem gewählten Level des zugeordneten Projekts. Wenn also jemand Projekt A mit drei Sternen gewählt hat und Projekt B bekommt, dass er gar nicht gewählt hat (0 Sterne), dann ist dieser Fehler 3 – kleinere Fehler sind also besser. Die Fehlerverteilung wird mit der Zeit schlechter, weil mehr SuS ihre Wahl getroffen haben.</p>"

        io.puts "<p>Bisher haben #{ts_data['email_count_voted']} von #{ts_data['email_count_total']} Schülerinnen und Schülern ihre Projekte gewählt:"
        io.puts "<div class='progress mb-3'>"
        p = ts_data['email_count_voted'] * 100 / ts_data['email_count_total']
        io.puts "<div class='bg-success progress-bar progress-bar-striped progress-bar-animated' role='progressbar' style='width: #{p}%;'>#{p.round}%</div>"
        io.puts "</div>"

        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Fehler</th>"
        (0..3).each do |error|
            io.puts "<td style='min-width: 3.5em; text-align: center;'>#{error}</td>"
        end
        io.puts "</tr>"
        io.puts "<tr>"
        io.puts "<th>Anteil</th>"
        (0..3).each do |error|
            io.puts "<td>#{sprintf('%1.2f%%', ts_data['errors'][error] * 100.0)}</td>"
        end
        io.puts "</tr>"
        io.puts "</table>"
        io.puts "</div>"

        io.puts "<h4>Projektinteresse</h4>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Projekt</th>"
        [5, 6, 7, 8, 9, 'WK', 3, 2, 1, 'Σ'].each do |klasse|
            if [1, 2, 3].include?(klasse)
                io.puts "<th class='#{[5, 3, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{PROJEKT_VOTE_CODEPOINTS[klasse].chr(Encoding::UTF_8)}</th>"
            else
                io.puts "<th class='#{[5, 3, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{klasse}#{['WK', 'Σ'].include?(klasse) ? '' : '.'}</th>"
            end
        end
        io.puts "</tr>"
        ndash = "<span class='text-muted'>&ndash;</span>"
        get_projekte.sort do |a, b|
            (votes_for_projekt["#{b[:nr]}"] || 0) <=> (votes_for_projekt["#{a[:nr]}"] || 0)
        end.each do |projekt|
            next if projekt[:capacity] == 0
            io.puts "<tr>"
            io.puts "<td>#{projekt[:title]}</td>"
            [5, 6, 7, 8, 9, 'WK'].each do |klasse|
                io.puts "<td class='#{[5, 3, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{votes_for_projekt["#{projekt[:nr]}/#{klasse}"] || ndash }</td>"
            end
            [3, 2, 1].each do |vote|
                io.puts "<td class='#{[5, 3, 'Σ'].include?(vote) ? 'cbl' : ''}' style='text-align: center;'>#{votes_for_projekt["#{projekt[:nr]}/#{vote}"] || ndash }</td>"
            end
            io.puts "<td class='cbl' style='text-align: center;'>#{votes_for_projekt["#{projekt[:nr]}"] || ndash }</td>"
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"

        io.puts "<h4>Interesse pro Klassenstufe</h4>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Klassenstufe</th>"
        [5, 6, 7, 8, 9, 'WK', 'Σ'].each do |klasse|
            io.puts "<th style='text-align: center;' class='#{[5, 'Σ'].include?(klasse) ? 'cbl' : ''}'>#{['WK', 'Σ'].include?(klasse) ? '' : 'Klassenstufe'} #{klasse}</th>"
        end
        io.puts "</tr>"
        [3, 2, 1].each do |vote|
            io.puts "<tr>"
            io.puts "<td>#{PROJEKT_VOTE_CODEPOINTS[vote].chr(Encoding::UTF_8)} #{PROJEKT_VOTE_LABELS[vote]}</td>"
            sum = 0
            [5, 6, 7, 8, 9, 'WK', 'Σ'].each do |klasse|
                count = votes["#{klasse}/#{vote}"] || ndash
                sum += votes["#{klasse}/#{vote}"] || 0
                count = sum if klasse == 'Σ'
                io.puts "<td class='#{[5, 'Σ'].include?(klasse) ? 'cbl' : ''}' style='text-align: center;'>#{count}</td>"
            end
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"

        io.puts "<h4>Anzahl der gewählten Projekte pro Klasse</h4>"
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-sm' style='width: unset;'>"
        io.puts "<tr>"
        io.puts "<th>Ausgewählte Projekte</th>"
        io.puts "<th>keins</th>"
        (1..10).each do |count|
            io.puts "<th style='text-align: center;'>#{count}#{count == 10 ? '+' : ''}</th>"
        end
        io.puts "</tr>"
        KLASSEN_ORDER.each do |klasse|
            next unless klasse.to_i <= 9 || klasse[0, 2] == 'WK'
            io.puts "<tr>"
            io.puts "<td>Klasse #{tr_klasse(klasse)}</td>"
            (0..10).each do |count|
                key = "votes_by_klasse/#{klasse}/#{count}"
                count = votes_for_projekt[key] || ndash
                io.puts "<td class='cbl' style='text-align: center;'>#{count}</td>"
            end
            io.puts "</tr>"
        end
        io.puts "</table>"
        io.puts "</div>"

        io.string
    end
end


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
# File 'src/ruby/include/user.rb', line 621

def print_projektwahl_countdown_panel()
    if user_eligible_for_projektwahl?
        vote_count = neo4j_query_expect_one("MATCH (u:User {email: $email})-[:VOTED_FOR]->(p:Projekt) RETURN COUNT(p) AS count;", {:email => @session_user[:email]})['count']
        if vote_count == 0
            if projekttage_phase() == 3
                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>Wähle deine Lieblingsprojekte!</b></p>"
                    io.puts "<p>Du findest den Projektkatalog im Menü unter »Projekttage«."
                    io.puts "</div>"
                    io.puts "</div>"
                    io.string
                end
            end
        end
    elsif (@session_user[:klassenstufe] || 0) == 11
        if projekttage_phase() > 0
            return StringIO.open do |io|
                rows = neo4j_query(<<~END_OF_QUERY, :email => @session_user[:email])
                    MATCH (p:Projekt)-[:ORGANIZED_BY]->(u:User {email: $email})
                    RETURN p;
                END_OF_QUERY
                unless rows.empty?
                    projekt = rows.first['p']
                    count = 0
                    count += 1 unless (projekt[:description] || '').strip.empty?
                    count += 1 unless (projekt[:photo] || '').strip.empty?
                    emoji = %w(😭 🥲 😄)[count]
                    if (projekt[:capacity] || 0) > 0
                        unless count == 2 && (Time.now.to_i - (projekt[:ts_updated] || 0) > 3600)
                            io.puts "<div class='col-lg-12 col-md-4 col-sm-6'>"
                            io.puts "<div class='hint'>"
                            io.puts "<p><b>Dein Angebot für die Projekttage</b></p>"
                            io.puts "<hr />"
                            io.puts "<span style='font-size: 300%; float: right; margin-left: 10px; margin-bottom: 10px;'>#{emoji}</span>"
                            if count == 0
                                io.puts "<p>Du hast noch keinen Werbetext für dein Projekt eingegeben und auch kein Bild hochgeladen. Bitte trage diese Informationen unter »Projekttage« nach und hilf mit, dass dieser arme Smiley wieder glücklich wird.</p>"
                            elsif count == 1
                                if (projekt[:description] || '').strip.empty?
                                    io.puts "<p>Du hast zwar schon ein Bild hochgeladen, aber noch keinen Werbetext geschrieben. You can do it!</p>"
                                else
                                    io.puts "<p>Du hast zwar schon einen Werbetext geschrieben, aber noch kein Bild hochgeladen. You can do it!</p>"
                                end
                            elsif count == 2
                                io.puts "<p>Danke, dass du alle Informationen eingetragen hast!</p>"
                            end
                            if count < 2
                                io.puts "<p><a href='/projekttage_orga' class='btn btn-success' style='white-space: normal;'>Lass uns diesen Smiley wieder glücklich machen!</a></p>"
                            end
                            io.puts "</div>"
                            io.puts "</div>"
                        end
                    end
                end
                io.string
            end
        end
    end
    return ''
end


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
# File 'src/ruby/include/public_event.rb', line 196

def print_public_event_table()
    self.class.refresh_public_event_config()
    ts = Time.now.strftime("%Y-%m-%dT%H:%M")
    StringIO.open do |io|
        if @@public_event_config.empty?
            io.puts "<p>Es sind momentan keine Veranstaltungen geplant, bitte versuchen Sie es später noch einmal.</p>"
        end
        @@public_event_config.each.with_index do |event, event_index|
            if event_index > 0
                io.puts "<hr />"
            end
            io.puts "<h3>#{event[:title]}</h3>"
            not_yet = false
            if event[:not_before] && Time.now.strftime('%Y-%m-%dT%H:%M:%S') < event[:not_before]
                not_yet = true
            end
            if event[:description]
                io.puts event[:description]
            end
            if not_yet && event[:not_before_description]
                io.puts event[:not_before_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
                        if not_yet
                            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
                        if not_yet
                            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


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
# File 'src/ruby/include/login.rb', line 731

def print_roles()
    require_user!
    StringIO.open do |io|
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        io.puts "<table class='table table-condensed table-striped table-narrow'>"
        io.puts "<thead>"
        io.puts "<tr>"
        io.puts "<th>Beschreibung</th>"
        io.puts "<th>Aktiv</th>"
        io.puts "<th>Ursprung</th>"
        if admin_logged_in? || user_with_role_logged_in?(:developer)
            io.puts "<th>Nutzer</th>"
        end
        io.puts "</tr>"
        io.puts "</thead>"
        io.puts "<tbody>"
        AVAILABLE_ROLES.each_pair do |role, description|
            unless admin_logged_in?
                next unless @session_user[:roles].include?(role)
            end
            io.puts "<tr>"
            io.puts "<td>#{description}</td>"
            if @session_user[:roles].include?(role)
                io.puts "<td><i class='fa fa-check text-success'></i></td>"
                if @session_user[:role_transitive_origin][role]
                    io.puts "<td>#{AVAILABLE_ROLES[@session_user[:role_transitive_origin][role]]}</td>"
                else
                    io.puts "<td>direkt gesetzt</td>"
                end
            else
                io.puts "<td><i class='fa fa-times text-danger'></i></td>"
                io.puts "<td></td>"
            end
            if admin_logged_in? || user_with_role_logged_in?(:developer)
                io.puts "<td><button class='btn-toggle-tr-below btn btn-xs btn-warning' style='width: 8em;'>#{(@@users_for_role[role] || []).size} Nutzer&nbsp;&nbsp;<i class='fa fa-chevron-down'></i></button></td>"
                io.puts "</tr>"
                io.puts "<tr style='display: none;'>"
                io.puts "<td colspan='4' style='font-style: italic; font-size: 90%;'>#{(@@users_for_role[role] || []).map { |x| @@user_info[x][:display_name] }.join(', ')}</td>"
            end
            io.puts "</tr>"
        end
        io.puts "</tbody>"
        io.puts "</table>"
        io.puts "</div>"
        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


2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
# File 'src/ruby/main.rb', line 2464

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


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
# File 'src/ruby/include/login.rb', line 658

def print_sessions()
    require_user!
    StringIO.open do |io|
        io.puts "<div class='table-responsive' style='max-width: 100%; overflow-x: auto;'>"
        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.puts "</div>"
        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


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
# File 'src/ruby/include/lesson.rb', line 492

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


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
# File 'src/ruby/include/user.rb', line 501

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

post '/api/remove_mobile_device' do

require_user_who_can_manage_tablets!
data = parse_request_data(:required_keys => [:code])
code = data[:code]
neo4j_query(<<~END_OF_QUERY, :code => code)
    MATCH (v:SchulTablet {code: $code})
    DELETE v
END_OF_QUERY
respond(:ok => true)

end



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
# File 'src/ruby/include/tablet.rb', line 41

def print_tablet_locations
    require_user_who_can_manage_tablets!
    tablets = []
    @@tablet_sets.select {|key, tablet_set| tablet_set[:is_tablet_set]}.each do |key, tablet_set|
        (1..tablet_set[:count])&.each do |name|
            tablets.append "#{tablet_set[:prefix].to_i}.#{name}"
        end
        tablet_set[:includes]&.each do |name|
            tablets.append "#{tablet_set[:prefix].to_i}.#{name}"
        end
        tablet_set[:includes_not]&.each do |name|
            tablets.delete "#{tablet_set[:prefix].to_i}.#{name}"
        end
    end
    StringIO.open do |io|
        io.puts "<table class='table narrow table-striped' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr><th>Tablet</th><th>Access Point</th><th>Zuletzt gesehen</th></tr>"
        io.puts "</thead><tbody>"
        for tablet in tablets do
            io.puts "<tr><td>#{tablet}</td><td></td><td></td></tr>"
        end
        io.puts "</tbody></table>"
        io.string
    end
end


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
# File 'src/ruby/include/techpost.rb', line 661

def ()
    require_user_with_role!(:can_manage_tech_problems)
    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


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
# File 'src/ruby/include/techpost.rb', line 618

def print_techpost_superuser()
    require_user_with_role!(:can_manage_tech_problems)
    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 "<br><h3>User, die Probleme melden können (Alle oben genannten plus:)</h3>"
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
        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>"
        get_technikamt_users.each do |technikamt|
            display_name = @@user_info[technikamt][:display_name]
             = @@user_info[technikamt][:nc_login]
            klasse = tr_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 "</tbody></table>"
        io.puts "</div></div>"
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'><div class='alert alert-info'><code>#{get_technikamt_users()}</code></div></div></div>"
        unless problems == []
            io.puts "<br><h3>Aktuelle Probleme im JSON-Format</h3>"
            io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
            for problem in problems do
                io.puts "<div class='alert alert-info'><code>#{problem.to_json}</code></div>"
            end
            io.puts "</div></div>"
        end
        io.puts "<br><h3>Super Funktionen</h3>"
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
        io.puts "<button class='bu-clear-all btn btn-danger'><i class='fa fa-trash'></i>&nbsp;&nbsp;Alle Probleme löschen</button>&nbsp<button class='bu-kick-all btn btn-danger'><i class='fa fa-user-times'></i>&nbsp;&nbsp;Alle Technikamt-User entfernen</button>&nbsp<button class='bu-send-welcome-mail btn btn-warning'><i class='fa fa-envelope'></i>&nbsp;&nbsp;Willkommens-E-Mails versenden</button>&nbsp;<button class='bu-clear-aula-lights btn btn-danger'><i class='fa fa-lightbulb-o'></i>&nbsp;&nbsp;Plan der Aula-Scheinwerfer zurücksetzen</button>"
        io.puts "</div></div>"
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
        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></div></div>"
        io.puts "Hinweis: Die Änderungen werden erst nach dem Neuladen der Seite sichtbar.</div></div>"
        io.string
    end
end


2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
# File 'src/ruby/main.rb', line 2447

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|
        KLASSEN_ORDER.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


2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
# File 'src/ruby/main.rb', line 2323

def print_timetable_chooser()
    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 = (email == @session_user[:email]) ? @@user_info[email][:id] : @@user_info[email][:public_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;'>"
            io.puts "<a data-id='#{@session_user[:id]}' onclick=\"load_timetable('#{@session_user[:id]}');\" class='btn btn-sm ttc active' style='width: 100%; padding: 0.25rem 0.5rem; display: inline-block;'>Persönlicher Stundenplan (#{tr_klasse(@session_user[:klasse])})</a><hr>"
            temp = StringIO.open do |tio|
                @@klassen_order.each do |klasse|
                    id = @@klassen_id[klasse]
                    tio.puts "<a data-klasse='#{klasse}' data-id='#{id}' onclick=\"load_timetable('#{id}');\" class='btn btn-sm ttc ttc-klasse'>#{tr_klasse(klasse)}</a>"
                end
                tio.string
            end
            io.puts temp

            io.puts '<hr />'
            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


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
# File 'src/ruby/include/user.rb', line 566

def print_tresor_countdown_panel()
    return '' unless teacher_logged_in?
    deadline = DEADLINE_NOTENEINTRAGUNG
    if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline && (DateTime.parse(deadline) - DateTime.now).to_f < 7.0
        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 />"
            d = DateTime.parse(deadline)
            io.puts "<p>Die Noteneingabe im Datentresor schließt am #{WEEKDAYS_LONG[d.wday]} um #{d.strftime('%H:%M')} 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 = DEADLINE_CONSIDER
    if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline && (DateTime.parse(deadline) - DateTime.now).to_f < 7.0
        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 />"
            d = DateTime.parse(deadline)
            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 #{WEEKDAYS_LONG[d.wday]} um #{d.strftime('%H:%M')} 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
    if need_sozialverhalten()
        deadline = DEADLINE_SOZIALNOTEN
        if Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= deadline && (DateTime.parse(deadline) - DateTime.now).to_f < 7.0
            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 />"
                d = DateTime.parse(deadline)
                io.puts "<p>Die Möglichkeit für Eintragungen der Noten für das Arbeits- und Sozialverhalten endet am #{WEEKDAYS_LONG[d.wday]} um #{d.strftime('%H:%M')} Uhr. Bitte tragen Sie bis dahin fehlende Noten ein, damit die Klassenleitungen rechtzeitig vor der Zeugnisausgabe die Sozialzeugnisse 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
    end
    return ''
end


598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'src/ruby/include/techpost.rb', line 598

def print_users_which_can_fix_tech_problems
    require_user_with_role!(:can_manage_tech_problems)
    StringIO.open do |io|
        io.puts "<div class='row' style='margin-bottom: 15px;'><div class='col-md-12'>"
        io.puts "<table class='table narrow table-striped' style='width: unset; min-width: 100%;'>"
        io.puts "<thead>"
        io.puts "<tr><td>User</td><td>Auswählen</td></tr>"
        io.puts "</thead><tbody>"
        @@users_for_role[:can_manage_tech_problems].each do |user|
            display_name = @@user_info[user][:display_name]
             = @@user_info[user][:nc_login]
            klasse = tr_klasse(@@user_info[user][:klasse])
            io.puts "<tr><td><img src='#{NEXTCLOUD_URL}/index.php/avatar/#{}/256' class='icon avatar-md'>&nbsp;#{display_name}</td><td><button class='btn btn-xs btn-success bu-select-user-to-fix-problem' data-email='#{user}'><i class='fa fa-check'></i></button></td></tr>"
        end
        io.puts "</tbody></table>"
        io.puts "</div></div>"
        io.string
    end
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
185
186
187
188
189
190
191
192
193
194
# File 'src/ruby/include/public_event.rb', line 106

def public_events_table()
    require_admin_or_sekretariat!
    self.class.refresh_public_event_config()
    StringIO.open do |io|
        description_for_key_and_key = {}
        row_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]
                    if entry[:row_description]
                        row_description_for_key_and_key[event[:key]] ||= {}
                        row_description_for_key_and_key[event[:key]][entry[:key]] ||= entry[:row_description]
                    end
                    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|
            STDERR.puts event.to_yaml
            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>#{[(row_description_for_key_and_key[event[:key]] || {})[key], description_for_key_and_key[event[:key]][key]].reject { |x| x.nil? }.join(' / ')}</td>"
                    if admin_logged_in?
                        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>"
                    else
                        io.puts "<td></td>"
                    end
                    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



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
# File 'src/ruby/include/login.rb', line 459

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



952
953
954
955
956
957
958
# File 'src/ruby/include/login.rb', line 952

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



328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'src/ruby/include/zeugnisse.rb', line 328

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

#redirect_on_error(&block) ⇒ Object



1592
1593
1594
1595
1596
1597
1598
1599
# File 'src/ruby/main.rb', line 1592

def redirect_on_error(&block)
    begin
        yield
    rescue
        redirect "#{WEB_ROOT}/", 302
        raise 'redirected'
    end
end

#refresh_second_factorObject



977
978
979
980
981
982
983
984
985
# File 'src/ruby/include/login.rb', line 977

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



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

def require_admin!
    assert(admin_logged_in?)
end

#require_admin_2fa_hotline!Object



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

def require_admin_2fa_hotline!
    assert(admin_2fa_hotline_logged_in?)
end

#require_admin_or_sekretariat!Object



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

def require_admin_or_sekretariat!
    assert(admin_logged_in? || sekretariat_logged_in?)
end

#require_device!Object



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

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

#require_monitor_or_user_who_can_manage_monitors!Object



266
267
268
# File 'src/ruby/include/user.rb', line 266

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

#require_running_phishing_training!Object



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

def require_running_phishing_training!
    assert(running_phishing_training?)
end

#require_running_phishing_training_hint!Object



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

def require_running_phishing_training_hint!
    assert(running_phishing_training_hint?)
end

#require_teacher!Object



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

def require_teacher!
    assert(teacher_logged_in?)
end

#require_teacher_for_lesson_or_ha_amt_logged_in(lesson_key) ⇒ Object



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

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_tablet!Object



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

def require_teacher_tablet!
    assert(teacher_tablet_logged_in?)
end

#require_technikteam!Object

def require_developer!

assert(developer_logged_in?)

end



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

def require_technikteam!
    assert(technikteam_logged_in?)
end

#require_user!Object



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

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

#require_user_who_can_manage_agr_app!Object



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

def require_user_who_can_manage_agr_app!
    assert(can_manage_agr_app_logged_in?)
end

#require_user_who_can_manage_antikenfahrt!Object



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

def require_user_who_can_manage_antikenfahrt!
    assert(user_who_can_manage_antikenfahrt_logged_in?)
end

#require_user_who_can_manage_bib!Object



262
263
264
# File 'src/ruby/include/user.rb', line 262

def require_user_who_can_manage_bib!
    assert(can_manage_bib_logged_in?)
end

#require_user_who_can_manage_monitors!Object



226
227
228
# File 'src/ruby/include/user.rb', line 226

def require_user_who_can_manage_monitors!
    assert(user_who_can_manage_monitors_logged_in?)
end

#require_user_who_can_manage_news!Object



222
223
224
# File 'src/ruby/include/user.rb', line 222

def require_user_who_can_manage_news!
    assert(user_who_can_manage_news_logged_in?)
end

#require_user_who_can_manage_salzh!Object



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

def require_user_who_can_manage_salzh!
    assert(can_manage_salzh_logged_in?)
end

#require_user_who_can_manage_tablets!Object



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

def require_user_who_can_manage_tablets!
    assert(user_who_can_manage_tablets_logged_in?)
end

#require_user_who_can_report_tech_problems!Object



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

def require_user_who_can_report_tech_problems!
    assert(user_who_can_report_tech_problems_logged_in?)
end

#require_user_who_can_upload_files!Object



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

def require_user_who_can_upload_files!
    assert(user_who_can_upload_files_logged_in?)
end

#require_user_who_can_use_aula!Object

def require_user_who_can_report_tech_problems_or_better!

assert(user_who_can_report_tech_problems_or_better_logged_in?)

end



246
247
248
# File 'src/ruby/include/user.rb', line 246

def require_user_who_can_use_aula!
    assert(user_who_can_use_aula_logged_in?)
end

#require_user_with_role!(role) ⇒ Object



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

def require_user_with_role!(role)
    assert(user_with_role_logged_in?(role))
end

#require_zeugnis_admin!Object



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

def require_zeugnis_admin!
    assert(zeugnis_admin_logged_in?)
end

#respond(hash = {}) ⇒ Object



1802
1803
1804
# File 'src/ruby/main.rb', line 1802

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

#respond_raw_with_mimetype(content, mimetype) ⇒ Object



1806
1807
1808
1809
# File 'src/ruby/main.rb', line 1806

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

#respond_raw_with_mimetype_and_filename(content, mimetype, filename) ⇒ Object



1811
1812
1813
1814
1815
# File 'src/ruby/main.rb', line 1811

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_phishing_training?Boolean

Returns:

  • (Boolean)


170
171
172
173
174
# File 'src/ruby/include/user.rb', line 170

def running_phishing_training?
    start = PHISHING_START
    ende = PHISHING_END
    user_logged_in? && (schueler_logged_in? || teacher_logged_in?) && Time.now.strftime('%Y-%m-%dT%H:%M:%S') >= start && Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= ende
end

#running_phishing_training_hint?Boolean

Returns:

  • (Boolean)


176
177
178
179
180
# File 'src/ruby/include/user.rb', line 176

def running_phishing_training_hint?
    start = PHISHING_HINT_START
    ende = PHISHING_HINT_END
    (schueler_logged_in? || teacher_logged_in?) && Time.now.strftime('%Y-%m-%dT%H:%M:%S') >= start && Time.now.strftime('%Y-%m-%dT%H:%M:%S') <= ende
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



770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# File 'src/ruby/include/directory.rb', line 770

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

#schueler_logged_in?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'src/ruby/include/user.rb', line 84

def schueler_logged_in?
    user_with_role_logged_in?(:schueler)
end

#score_for_project(nr, project_data) ⇒ Object



408
409
410
411
412
# File 'src/ruby/include/projekte.rb', line 408

def score_for_project(nr, project_data)
    vote = project_data['vote'] || {}
    x = [vote['0'] || 0, vote['1'] || 0, vote['2'] || 0, vote['3'] || 0]
    return -(x[0] * 3 + x[1] - x[2] - 3 * x[3]).to_f / (x.sum + 1)
end

#second_factor_time_leftObject



960
961
962
963
964
965
966
967
968
969
970
# File 'src/ruby/include/login.rb', line 960

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

#sekretariat_logged_in?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'src/ruby/include/user.rb', line 60

def sekretariat_logged_in?
    user_with_role_logged_in?(:sekretariat)
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



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

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

#send_welcome_mail(recipients) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'src/ruby/include/techpost.rb', line 24

def send_welcome_mail(recipients)
    for mail_adress in recipients do
        deliver_mail do
            to mail_adress
            bcc SMTP_FROM
            from SMTP_FROM

            subject "Du bist Technikamt im Dashboard!"

            StringIO.open do |io|
                io.puts "<p>Hallo!</p>"
                io.puts "<p>Das TechnikTeam hat dir soeben die Funktion „Technikamt“ im Dashboard freigeschaltet. Herzlichen Glückwunsch, du gehörst zu den Ersten, die diese Funktion nutzen dürfen!</p>"
                io.puts "<p>Du solltest die Funktion schon im Rahmen eines Workshops kennengelernt haben. Gerne kannst du jetzt die Funktion testen (schreib aber bitte immer dazu, wenn es sich um einen Test handelt).</p>"
                io.puts "<p>Falls du diese Nachricht unerwartet bekommst oder Probleme beim Anmelden im Dashboard hast, melde dich einfach bei Peter-J. Germelmann (peter-julius.germelmann@mail.gymnasiumsteglitz.de).</p>"
                io.puts "<p>Viele Grüße<br>Das TechnikTeam #{SCHUL_NAME_AN_DATIV} #{SCHUL_NAME}</p>"
                io.string
            end
        end
    end
    respond(:ok => true)
end

#session_user_has_streaming_button?Boolean

Returns:

  • (Boolean)


1574
1575
1576
1577
1578
1579
1580
1581
# File 'src/ruby/main.rb', line 1574

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

#session_user_jump_table_directionObject



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

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#{DEVELOPMENT ? ' (dev)' : ''}")
    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



448
449
450
451
452
453
454
455
# File 'src/ruby/include/user.rb', line 448

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



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

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



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

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
183
184
185
# File 'src/ruby/include/cypher.rb', line 171

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

#tablet_logged_in?Boolean

Returns:

  • (Boolean)


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

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)


126
127
128
129
130
131
132
# File 'src/ruby/include/user.rb', line 126

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:

  • (Boolean)


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

def teacher_logged_in?
    user_with_role_logged_in?(:teacher)
end

#teacher_tablet_logged_in?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'src/ruby/include/user.rb', line 104

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

#technikteam_logged_in?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'src/ruby/include/user.rb', line 40

def technikteam_logged_in?
    user_with_role_logged_in?(:technikteam)
end

#test_request_parameter(data, key, options) ⇒ Object



1523
1524
1525
1526
1527
1528
1529
# File 'src/ruby/main.rb', line 1523

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



292
293
294
295
296
# File 'src/ruby/include/user.rb', line 292

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



316
317
318
319
320
# File 'src/ruby/include/user.rb', line 316

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



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

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



328
329
330
331
332
# File 'src/ruby/include/user.rb', line 328

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_usersObject



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

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



298
299
300
301
302
# File 'src/ruby/include/user.rb', line 298

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



322
323
324
325
326
# File 'src/ruby/include/user.rb', line 322

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



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

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



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

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



334
335
336
337
338
# File 'src/ruby/include/user.rb', line 334

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_phishing_trainingObject

Put this on top of a webpage to assert that this page can be opened during a phishing training only



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

def this_is_a_page_for_phishing_training
    unless running_phishing_training?
        redirect "#{WEB_ROOT}/", 303
    end
end

#this_is_a_page_for_user_with_role(role) ⇒ Object



310
311
312
313
314
# File 'src/ruby/include/user.rb', line 310

def this_is_a_page_for_user_with_role(role)
    unless user_with_role_logged_in?(role)
        redirect "#{WEB_ROOT}/", 303
    end
end

#tr_klasse(klasse) ⇒ Object



509
510
511
# File 'src/ruby/main.rb', line 509

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

#tresor_second_factor_ttlObject



888
889
890
891
892
893
894
895
# File 'src/ruby/include/login.rb', line 888

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



1866
1867
1868
1869
1870
1871
1872
1873
# File 'src/ruby/main.rb', line 1866

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_stats_update(which) ⇒ Object



1848
1849
1850
1851
1852
1853
1854
1855
# File 'src/ruby/main.rb', line 1848

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

#trigger_update(which) ⇒ Object



1839
1840
1841
1842
1843
1844
1845
1846
# File 'src/ruby/main.rb', line 1839

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



1857
1858
1859
1860
1861
1862
1863
1864
# File 'src/ruby/main.rb', line 1857

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_eligible_for_projekt_katalog?Boolean

Returns:

  • (Boolean)


11
12
13
14
# File 'src/ruby/include/projekte.rb', line 11

def user_eligible_for_projekt_katalog?
    return true if teacher_logged_in?
    return schueler_logged_in?
end

#user_eligible_for_projektwahl?Boolean

Returns:

  • (Boolean)


16
17
18
19
20
21
# File 'src/ruby/include/projekte.rb', line 16

def user_eligible_for_projektwahl?
    return false unless schueler_logged_in?
    return false unless projekttage_phase() == 3
    klassenstufe = @session_user[:klassenstufe] || 7
    return klassenstufe >= 5 && klassenstufe <= 9
end

#user_has_role(email, role) ⇒ Object



11
12
13
# File 'src/ruby/include/user.rb', line 11

def user_has_role(email, role)
    Main.user_has_role(email, role)
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>



370
371
372
# File 'src/ruby/include/user.rb', line 370

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
# File 'src/ruby/include/sms.rb', line 19

def user_is_eligible_for_sms?
    return false unless user_logged_in?
    return teacher_logged_in?
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:

  • (Boolean)


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

def user_logged_in?
    !@session_user.nil?
end

#user_was_eligible_for_projektwahl?Boolean

Returns:

  • (Boolean)


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

def user_was_eligible_for_projektwahl?
    return false unless schueler_logged_in?
    return false unless projekttage_phase() == 4
    klassenstufe = @session_user[:klassenstufe] || 7
    return klassenstufe >= 5 && klassenstufe <= 9
end

#user_who_can_manage_antikenfahrt_logged_in?Boolean

Returns:

  • (Boolean)


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

def user_who_can_manage_antikenfahrt_logged_in?
    user_with_role_logged_in?(:can_manage_antikenfahrt)
end

#user_who_can_manage_monitors_logged_in?Boolean

Returns:

  • (Boolean)


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

def user_who_can_manage_monitors_logged_in?
    user_with_role_logged_in?(:can_manage_monitors)
end

#user_who_can_manage_news_logged_in?Boolean

Returns:

  • (Boolean)


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

def user_who_can_manage_news_logged_in?
    user_with_role_logged_in?(:can_manage_news)
end

#user_who_can_manage_tablets_logged_in?Boolean

Returns:

  • (Boolean)


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

def user_who_can_manage_tablets_logged_in?
    user_with_role_logged_in?(:can_manage_tablets)
end

#user_who_can_report_tech_problems_logged_in?Boolean

Returns:

  • (Boolean)


134
135
136
# File 'src/ruby/include/user.rb', line 134

def user_who_can_report_tech_problems_logged_in?
    user_logged_in? && check_has_technikamt(@session_user[:email])
end

#user_who_can_upload_files_logged_in?Boolean

Returns:

  • (Boolean)


20
21
22
# File 'src/ruby/include/user.rb', line 20

def user_who_can_upload_files_logged_in?
    user_with_role_logged_in?(:can_upload_files)
end

#user_who_can_use_aula_logged_in?Boolean

Returns:

  • (Boolean)


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

def user_who_can_use_aula_logged_in?
    user_with_role_logged_in?(:can_use_aula)
end

#user_with_role_logged_in?(role) ⇒ Boolean

Returns:

  • (Boolean)


15
16
17
18
# File 'src/ruby/include/user.rb', line 15

def user_with_role_logged_in?(role)
    assert(AVAILABLE_ROLES.include?(role), "Unknown role: #{role}")
    user_logged_in? && (@session_user[:roles].include?(role))
end

#vote_codes_from_token(vote_code, token, _count) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'src/ruby/include/vote.rb', line 76

def vote_codes_from_token(vote_code, token, _count)
    raise 'not used anymore'
    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)


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

def zeugnis_admin_logged_in?
    user_with_role_logged_in?(:zeugnis_admin)
end