├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── composer.json ├── contribution ├── bash │ ├── backup_database.sh │ └── check_fahrplan_updates.sh ├── certs │ └── cacert.pem └── perl │ ├── C3TT │ └── Client.pm │ ├── rpc_test.pl │ └── test-profile.sh ├── log └── .gitignore ├── scripts └── install └── src ├── Application ├── Controller │ ├── API.php │ ├── Application.php │ ├── Encodingprofiles.php │ ├── Import.php │ ├── Projects.php │ ├── Tickets.php │ ├── User.php │ ├── Workers.php │ └── XMLRPC │ │ └── Handler.php ├── Helper │ ├── EncodingProfile.php │ ├── Ticket.php │ └── Time.php ├── Migrations │ ├── 00_extensions.sql │ ├── 01_ticket_states.sql │ ├── 02_handles.sql │ ├── 03_encoding_profiles.sql │ ├── 04_projects.sql │ ├── 05_import.sql │ ├── 06_tickets.sql │ ├── 07_comments.sql │ ├── 08_log.sql │ ├── 10_function_create_missing_encoding_tickets.sql │ ├── 11_function_ticket_fahrplan_starttime.sql │ ├── 12_function_ticket_priority.sql │ ├── 13_function_ticket_progress.sql │ ├── 14_function_create_missing_recording_tickets.sql │ ├── 15_function_ticket_state.sql │ ├── 16_function_ticket_dependency.sql │ ├── 17_function_ticket_dependency_satisfied.sql │ ├── 18_function_ticket_dependency_missing.sql │ ├── 19_function_encodingprofile_dependency.sql │ ├── 20_view_parent_tickets.sql │ ├── 21_view_all_tickets.sql │ ├── 22_view_serviceable_tickets.sql │ ├── 99_testdata.sql │ ├── __2018-04-15_add_variable_dependent_ticket_state.sql │ ├── __2018-04-20_refactor_names_and_improve_performance.sql │ ├── __2018-05-14_cleanup_create_encoding_tickets.sql │ ├── __2018-05-14_cleanup_obsolete_create_missing_encoding_ticket.sql │ ├── __2018-05-14_make_creating_encoding_tickets_configurable.sql │ ├── __2018-06-01_import_auth.sql │ ├── __2018-06-21_fix-trigger-for-progress-recalculation.sql │ ├── __2018-09-17_check-missing-dependee-ticket.sql │ ├── __2018-11-02_text_properties.sql │ ├── __2019-05-24_check_dependee_ticket_state.sql │ ├── __2019-05-24_fix-dependee-ticket-missing.sql │ ├── __2019-05-24_ticket-dependency-satisfied.sql │ ├── __2019-09-28_ticket_reset.sql │ ├── __2020-01-11_drop_filter_constraint.sql │ └── __2020-04-15_ticket_initial_state.sql ├── Model │ ├── Comment.php │ ├── EncodingProfile.php │ ├── EncodingProfileProperties.php │ ├── EncodingProfileVersion.php │ ├── Handle.php │ ├── Import.php │ ├── LogEntry.php │ ├── Project.php │ ├── ProjectLanguages.php │ ├── ProjectProperties.php │ ├── ProjectTicketState.php │ ├── ProjectWorkerGroupFilter.php │ ├── Ticket.php │ ├── TicketProperties.php │ ├── TicketState.php │ ├── User.php │ ├── Worker.php │ └── WorkerGroup.php ├── Styles │ ├── _legacy.css │ ├── components │ │ ├── _encoding-profiles.scss │ │ └── _tickets.scss │ ├── images │ │ ├── background.gif │ │ ├── main.png │ │ ├── main_2x.png │ │ ├── progress.gif │ │ ├── wait.gif │ │ └── warning.png │ └── index.scss ├── View │ ├── 403.html.php │ ├── 404.html.php │ ├── 500.html.php │ ├── default.html.php │ ├── encoding │ │ └── profiles │ │ │ ├── _defaultProfile.xml │ │ │ ├── edit.html.php │ │ │ ├── index.html.php │ │ │ └── view.html.php │ ├── import │ │ ├── _header.html.php │ │ ├── index.html.php │ │ ├── repeat.html.php │ │ ├── review.html.php │ │ └── rooms.html.php │ ├── projects │ │ ├── delete.html.php │ │ ├── edit.html.php │ │ ├── index.html.php │ │ ├── settings.html.php │ │ └── settings │ │ │ ├── _header.html.php │ │ │ ├── filter │ │ │ └── edit.html.php │ │ │ ├── profiles.html.php │ │ │ ├── properties.html.php │ │ │ ├── states.html.php │ │ │ └── worker.html.php │ ├── shared │ │ ├── form │ │ │ └── properties.html.php │ │ ├── js │ │ │ └── _editor.html.php │ │ └── properties.html.php │ ├── tickets │ │ ├── duplicate.html.php │ │ ├── edit.html.php │ │ ├── edit │ │ │ └── _flash.html.php │ │ ├── edit_multiple.html.php │ │ ├── feed.html.php │ │ ├── feed.json.php │ │ ├── feed │ │ │ ├── actions.html.php │ │ │ ├── entry.html.php │ │ │ └── progress.html.php │ │ ├── index.html.php │ │ ├── index.json.php │ │ ├── index │ │ │ └── _header.html.php │ │ ├── search.html.php │ │ ├── ticket.html.php │ │ ├── view.html.php │ │ └── view │ │ │ ├── _action.html.php │ │ │ ├── _comment.html.php │ │ │ ├── _header.html.php │ │ │ ├── _log_entry.html.php │ │ │ ├── _status.html.php │ │ │ └── action │ │ │ ├── _check.html.php │ │ │ └── _cut.html.php │ ├── user │ │ ├── edit.html.php │ │ ├── index.html.php │ │ ├── login.html.php │ │ └── settings.html.php │ └── workers │ │ ├── group │ │ ├── edit.html.php │ │ └── queue.html.php │ │ └── index.html.php ├── package.json ├── webpack.config.js └── yarn.lock ├── Config ├── AccessControl.php ├── Config.Default.php └── Routes.php └── Public ├── .htaccess ├── css ├── codemirror.css └── main.css ├── favicon.ico ├── images ├── background.gif ├── main.png ├── main_2x.png ├── progress.gif ├── wait.gif └── warning.png ├── index.php ├── javascript ├── codemirror-5.7.js ├── jquery-2.1.4.min.js ├── jquery.cookie.min.js └── main.js ├── robots.txt └── xsl └── jobstyle.xsl /.gitignore: -------------------------------------------------------------------------------- 1 | src/Config/Config.php 2 | src/Public/fahrplan/ 3 | 4 | src/Application/node_modules 5 | src/Public/css/*.css.map 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/framework"] 2 | path = vendor/framework 3 | url = https://github.com/jjeising/framework 4 | branch = crs-tools-tracker 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CRS Ticket Tracker 2 | ================== 3 | 4 | The Ticket Tracker is a web platform tracking process of video recording and 5 | ingest sources and video encoding progress. It guides users through manual processes like editing and checking and provides an API for [scripts](https://github.com/crs-tools/crs-scripts) doing post processing and encoding. 6 | 7 | 8 | Requirements 9 | ------------ 10 | 11 | - \>= PHP 7.1.0 12 | - ext/curl 13 | - ext/intl 14 | - ext/mbstring 15 | - ext/openssl 16 | - ext/xsl 17 | - ext/xmlrpc 18 | - pecl/apcu 19 | - pecl/xdiff 20 | 21 | - PostgreSQL >= 9.2 database 22 | - ltree feature, often found in separate "-contrib" packages 23 | 24 | Note: [libxdiff0](https://github.com/a-tze/libxdiff ) and [pecl/xdiff](https://github.com/a-tze/php7-xdiff) are available as debian packages. 25 | 26 | 27 | Install 28 | ------- 29 | After checkout run 30 | 31 | ```bash 32 | git submodule init 33 | git submodule update 34 | ``` 35 | 36 | Then you may try `composer install` to satisfy all requirements. Then running 37 | 38 | ```bash 39 | ./scripts/install 40 | ``` 41 | 42 | sets the database config in `src/Config/Config.php` and tries to setup tables 43 | and initial data. 44 | 45 | 46 | Contribute 47 | ---------- 48 | 49 | We welcome any contributions and pull requests. 50 | Please open an issue before implementing big features or working on large 51 | reworks as there may be overlaps with existing development. 52 | We may not accept all requests if we don't see fit or certain quality standards 53 | are not met. 54 | 55 | Contributors may have to a agree to a Contributors License Agreement allowing 56 | relicensing, soon. 57 | 58 | 59 | License 60 | ------- 61 | 62 | Copyright 2018 Jannes Jeising 63 | Copyright 2018 Peter Große 64 | 65 | Licensed under the Apache License, Version 2.0. 66 | Excluding graphics and name, please see LICENSE file for details. 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fem/tracker", 3 | "description": "CRS Ticket Tracker", 4 | 5 | "type": "project", 6 | "license": "Apache-2.0", 7 | 8 | "config": { 9 | "vendor-dir": "vendor" 10 | }, 11 | 12 | "require": { 13 | "php": ">=7.1.0", 14 | 15 | "ext-apcu": "*", 16 | "ext-curl": "*", 17 | "ext-intl": "*", 18 | "ext-mbstring": "*", 19 | "ext-openssl": "*", 20 | "ext-xdiff": "*", 21 | "ext-xmlrpc": "*", 22 | "ext-xsl": "*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contribution/bash/backup_database.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$2" ] ; then 4 | echo "Usage:\n\n\t$0 \n\n" >&2 5 | exit 1 6 | fi 7 | 8 | dump=`which pg_dump` 9 | 10 | if [ ! -x "$dump" ] ; then 11 | echo "No pg_dump binary found in path!" >&2 12 | exit 2 13 | fi 14 | 15 | dbname="$1" 16 | targetfile="$2" 17 | 18 | 19 | # dump the schema first 20 | 21 | $dump --create --clean --if-exists --schema-only "$dbname" > "$targetfile" 22 | 23 | # dump data of tables in correct order, i.e. the order that is necessary for successful 24 | # restore due to dependencies 25 | 26 | for table in tbl_ticket_state tbl_handle tbl_user tbl_worker_group tbl_worker tbl_encoding_profile \ 27 | tbl_encoding_profile_version tbl_encoding_profile_property tbl_project tbl_project_language \ 28 | tbl_project_property tbl_project_ticket_state tbl_project_worker_group \ 29 | tbl_project_worker_group_filter tbl_project_encoding_profile tbl_user_project_restrictions \ 30 | tbl_import tbl_ticket tbl_ticket_property tbl_comment tbl_log ; do 31 | 32 | $dump --data-only --disable-triggers --table=$table "$dbname" >> "$targetfile" 33 | done 34 | 35 | -------------------------------------------------------------------------------- /contribution/bash/check_fahrplan_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z $1 ]]; then 4 | echo "no destination folder given" 5 | exit 1 6 | fi 7 | 8 | URL=https://events.ccc.de/congress/2013/Fahrplan/schedule.xml 9 | 10 | DESTDIR=$1 11 | TMPFILE=/tmp/schedule_`whoami`.xml 12 | 13 | wget -N -q $URL -O $TMPFILE 14 | 15 | if [[ $? -ne 0 ]]; then 16 | exit 0 17 | fi 18 | 19 | LAST_UPDATE=$(ls -t1 ${DESTDIR}*.xml 2>/dev/null | head -1) 20 | if [[ -n $LAST_UPDATE ]]; then 21 | DIFF=$(diff -u $LAST_UPDATE $TMPFILE 2>/dev/null) 22 | if [[ $? -eq 0 ]]; then 23 | exit 0 24 | fi 25 | 26 | echo "Fahrplan update!" 27 | echo "================" 28 | echo "$DIFF" 29 | else 30 | echo "Initial Fahrplan download!" 31 | fi 32 | 33 | VERSION=$(sed -n "s/.*version>\(.*\)<\/version.*/\1/p" $TMPFILE) 34 | NEWFILE="fahrplan_${VERSION}_$(date +%s).xml" 35 | echo "new fahrplan file: $NEWFILE" 36 | 37 | mv $TMPFILE "${DESTDIR}/${NEWFILE}" 38 | -------------------------------------------------------------------------------- /contribution/perl/C3TT/Client.pm: -------------------------------------------------------------------------------- 1 | # C3TT::CLient 2 | # 3 | # Copyright (c) 2013 Peter Große , all rights reserved 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the same terms as Perl itself. 6 | 7 | package C3TT::Client; 8 | 9 | =head1 NAME 10 | 11 | C3TT::Client - Client for interacting with the C3 Ticket Tracker via XML-RPC 12 | 13 | =head1 VERSION 14 | 15 | Version 0.5 16 | 17 | =cut 18 | 19 | our $VERSION = '0.5'; 20 | 21 | =head1 SYNOPSIS 22 | 23 | Generic usage 24 | 25 | use C3TT::CLient; 26 | 27 | my $rpc = C3TT::Client->new( $uri, $worker_group_token, $secret ); 28 | 29 | Call a remote method 30 | 31 | my $states = $rpc->getVersion(); 32 | 33 | =head1 DESCRIPTION 34 | 35 | C3TT::Client is a library for interacting with the C3TT via XML-RPC with automatic encoding 36 | of all arguments 37 | 38 | =head1 METHODS 39 | 40 | =head2 new ($url, $worker_group_token, $secret) 41 | 42 | Create C3TT:Client object. 43 | 44 | =cut 45 | 46 | use strict; 47 | use warnings; 48 | 49 | use XML::RPC::Fast; 50 | use vars qw($AUTOLOAD); 51 | 52 | use Data::Dumper; 53 | use Net::Domain qw(hostname hostfqdn); 54 | use Digest::SHA qw(hmac_sha256_hex); 55 | use URI::Escape qw(uri_escape); 56 | use IO::Socket::SSL; 57 | use URI::Escape; 58 | 59 | use constant PREFIX => 'C3TT.'; 60 | 61 | # Number of repetitions to perform when the communication with the tracker raises an exception 62 | # E.g.: the client will wait 10s after a fail before retrying (10 * 6 = 1 minute) 63 | use constant REMOTE_CALL_TRIES => 180; 64 | use constant REMOTE_CALL_SLEEP => 10; 65 | 66 | sub new { 67 | my $prog = shift; 68 | my $self; 69 | 70 | $self->{url} = shift; 71 | $self->{token} = shift; 72 | $self->{secret} = shift; 73 | 74 | if (!defined($self->{url})) { 75 | ($self->{secret}, $self->{token}, $self->{url}) = ($ENV{'CRS_SECRET'}, $ENV{'CRS_TOKEN'}, $ENV{'CRS_TRACKER'}); 76 | } 77 | 78 | # create remote handle 79 | $self->{remote} = XML::RPC::Fast->new($self->{url}.'?group='.$self->{token}.'&hostname='.hostfqdn, ua => 'LWP', timeout => 30); 80 | 81 | bless $self; 82 | 83 | my $version = $self->getVersion(); 84 | die "be future compatible - tracker RPC API version is $version!\nI'd be more happy with '4.0'.\n" unless ($version eq '4.0'); 85 | 86 | return $self; 87 | } 88 | 89 | sub AUTOLOAD { 90 | 91 | my $name = $AUTOLOAD; 92 | $name =~ s/.*://; 93 | 94 | if($name eq 'DESTROY') { 95 | return; 96 | } 97 | 98 | my $self = shift; 99 | 100 | if(!defined $self->{remote}) { 101 | print "No RPC available."; 102 | exit 1; 103 | } 104 | 105 | my @args = @_; 106 | 107 | ##################### 108 | # generate signature 109 | ##################### 110 | # assemble static part of signature arguments 111 | # 1. URL 2. method name 3. worker group token 4. hostname 112 | my @signature_args = (uri_escape_utf8($self->{url}), PREFIX.$name, $self->{token}, hostfqdn); 113 | 114 | # include method arguments if any given 115 | if(defined $args[0]) { 116 | foreach my $arg (@args) { 117 | push(@signature_args, (ref($arg) eq 'HASH') ? hash_serialize($arg) : uri_escape_utf8($arg)); 118 | } 119 | } 120 | 121 | # generate hash over url escaped line containing a concatenation of above signature arguments 122 | my $signature = hmac_sha256_hex(join('%26',@signature_args), $self->{secret}); 123 | 124 | # add signature as additional parameter 125 | push(@args,$signature); 126 | 127 | ############## 128 | # remote call 129 | ############## 130 | my $nLoop = REMOTE_CALL_TRIES; 131 | while($nLoop-- > 0) { 132 | my $r; 133 | eval { 134 | $r = $self->{remote}->call(PREFIX.$name, @args); 135 | }; 136 | 137 | if($@) { 138 | print "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"; 139 | print "$@"; 140 | print "!!!!!!!!!!!!!! sleeping ".REMOTE_CALL_SLEEP." s !!!!!!!!!!!!!!\n"; 141 | sleep(REMOTE_CALL_SLEEP); 142 | print "\nretrying $nLoop more times"; 143 | } 144 | else { 145 | return $r; 146 | } 147 | } 148 | 149 | print "\ngiving up with\n"; 150 | die $@ 151 | } 152 | 153 | sub hash_serialize { 154 | my($data) = @_; 155 | 156 | my $result = ""; 157 | for my $key (keys %$data) { 158 | $result .= '&' if length $result; 159 | $result .= '%5B' . uri_escape_utf8($key) . '%5D=' . uri_escape_utf8($data->{$key}); 160 | } 161 | return $result; 162 | } 163 | 164 | 1; 165 | -------------------------------------------------------------------------------- /contribution/perl/rpc_test.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -I. 2 | use C3TT::Client; 3 | use Data::Dumper; 4 | use POSIX qw(strftime); 5 | use boolean ':all'; 6 | use utf8; 7 | 8 | my $opt = shift; 9 | my $debug = 0; 10 | $debug = 1 if ($opt eq 'debug'); 11 | 12 | # init 13 | 14 | unless (defined($ENV{'CRS_TRACKER'})) { 15 | print STDERR "\nyou need to give tracker credentials via env variables, \nsee test-profile.sh how to do that.\n\n"; 16 | die; 17 | } 18 | 19 | # result printing 20 | 21 | sub print_check { 22 | my ($value, $expected, undef) = @_; 23 | print "value:\n".Dumper($value) if ($debug); 24 | if (!defined($expected)) { 25 | if (defined($value)) { 26 | print "OK (returned something)\n"; 27 | } else { 28 | print "FAIL: nothing returned\n"; 29 | sleep 5; 30 | } 31 | return; 32 | } 33 | if ($value eq $expected) { 34 | print "OK\n"; 35 | } else { 36 | print "FAIL\n"; 37 | print "value:\n".Dumper($value) ."\n"; 38 | print "expected:\n".Dumper($expected) ."\n"; 39 | sleep 5; 40 | } 41 | } 42 | 43 | # Tests: 44 | 45 | my $rpc = C3TT::Client->new(); 46 | 47 | print "testing getVersion(): "; 48 | print_check($rpc->getVersion(), '4.0'); 49 | 50 | print "testing getEncodingProfiles(): "; 51 | print_check($rpc->getEncodingProfiles()); 52 | 53 | my $tmp = $rpc->getEncodingProfiles(); 54 | $tmp = $$tmp[0]{id}; 55 | print "testing getEncodingProfiles($tmp): "; 56 | print_check($rpc->getEncodingProfiles($tmp)); 57 | 58 | my $start_filter = {}; 59 | $start_filter->{'Record.StartedBefore'} = strftime('%FT%TZ', gmtime(time)); 60 | print "testing assignNextUnassignedForState('recording', 'recording', $start_filter): "; 61 | my $ticket = $rpc->assignNextUnassignedForState('recording', 'recording', $start_filter); 62 | print_check($ticket); 63 | 64 | print Dumper($ticket) if($debug); 65 | 66 | # There should be assigned tickets right now, because we have one 67 | print "testing getAssignedForState('recording', 'recording', $start_filter): "; 68 | print_check($rpc->getAssignedForState('recording', 'recording', $start_filter)); 69 | 70 | my $tid = $ticket->{'id'}; 71 | my $pid = $ticket->{'project_id'}; 72 | my $state = $ticket->{'ticket_state'}; 73 | my $type = $ticket->{'ticket_type'}; 74 | 75 | print "testing ping ($tid, 'foo'): "; 76 | print_check($rpc->ping($tid, 'foo'), 'OK'); 77 | 78 | print "testing addLog ($tid, 'foo'): "; 79 | print_check($rpc->addLog ($tid, 'foo'), true); 80 | 81 | print "testing with umlauts addLog ($tid, 'foo-€'): "; 82 | print_check($rpc->addLog ($tid, "foo-€"), true); 83 | 84 | print "testing getNextState($pid, $type, $state): "; 85 | print_check($rpc->getNextState($pid, $type, $state)); 86 | 87 | print "testing getPreviousState($pid, $type, $state): "; 88 | print_check($rpc->getPreviousState($pid, $type, $state)); 89 | 90 | print "testing getTicketNextState($tid): "; 91 | print_check($rpc->getTicketNextState($tid)->{'ticket_state'}, 'recorded'); 92 | 93 | print "testing setTicketNextState($tid): "; 94 | print_check($rpc->setTicketNextState($tid), true); 95 | 96 | ### get another ticket 97 | 98 | $ticket = $rpc->assignNextUnassignedForState('recording', 'recording', $start_filter); 99 | $tid = $ticket->{'id'}; 100 | 101 | print "testing setTicketDone($tid): "; 102 | print_check($rpc->setTicketDone($tid), true); 103 | 104 | 105 | ### get yet another ticket (encoding ticket) 106 | 107 | print "testing assignNextUnassignedForState('encoding', 'encoding') (no filter): "; 108 | $ticket = $rpc->assignNextUnassignedForState('encoding', 'encoding'); 109 | if (!$ticket) { 110 | print STDERR "need an encoding ticket in ready to encode state!\n"; 111 | exit 1; 112 | } 113 | $tid = $ticket->{'id'}; 114 | 115 | print "testing getTicketProperites($tid): "; 116 | print_check($rpc->getTicketProperties($tid)); 117 | 118 | my $token = time(); 119 | my $pattern = 'RpcTest.Token'; 120 | my $props = { }; 121 | $props->{$pattern} = $token; 122 | 123 | print "testing setTicketProperties($tid, $props): "; 124 | print_check($rpc->setTicketProperties($tid, $props), true); 125 | 126 | print "testing getTicketProperties($tid, $pattern): "; 127 | print_check($rpc->getTicketProperties($tid, $pattern)->{$pattern}, $token); 128 | 129 | print "testing getTicketProperties($tid): "; 130 | print_check($rpc->getTicketProperties($tid)->{$pattern}, $token); 131 | 132 | print "testing getJobFile($tid): "; 133 | print_check($rpc->getJobFile($tid)); 134 | 135 | print "testing setTicketFailed($tid, 'Failtest'): "; 136 | print_check($rpc->setTicketFailed($tid, 'Failtest'), true); 137 | 138 | $token .= "-€"; 139 | $props->{$pattern} = $token; 140 | 141 | print "testing with umlaut setTicketProperties($tid, $props): "; 142 | print_check($rpc->setTicketProperties($tid, $props), true); 143 | 144 | print "testing with umlaut getTicketProperties($tid, $pattern): "; 145 | print_check($rpc->getTicketProperties($tid, $pattern)->{$pattern}, $token); 146 | 147 | -------------------------------------------------------------------------------- /contribution/perl/test-profile.sh: -------------------------------------------------------------------------------- 1 | # edit this file and source it in the shell before running the tests 2 | 3 | export CRS_TRACKER=http://localhost/rpc 4 | export CRS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 5 | export CRS_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 6 | 7 | 8 | # a place to store CA certs 9 | # needs to be a hashed dir (man c_rehash) 10 | #export HTTPS_CA_DIR=/etc/ssl/certs 11 | 12 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /src/Application/Controller/API.php: -------------------------------------------------------------------------------- 1 | true]; 14 | 15 | protected function setProject($action, array $arguments) { 16 | if (!isset($arguments['project_slug'])) { 17 | return; 18 | } 19 | 20 | $this->project = Project::findBy(['slug' => $arguments['project_slug']]); 21 | } 22 | 23 | public function tickets_fahrplan() { 24 | $tickets = Ticket::findAll() 25 | ->select('id, fahrplan_id, title') 26 | ->where([ 27 | 'project_id' => $this->project['id'], 28 | 'ticket_type' => 'meta' 29 | ]) 30 | ->scoped([ 31 | 'with_default_properties', 32 | 'with_encoding_profile_name', 33 | 'order_list' 34 | ]); 35 | 36 | return $this->_respond($tickets); 37 | } 38 | 39 | public function tickets_released() { 40 | $tickets = Ticket::findAll() 41 | ->select( 42 | 'fahrplan_id' 43 | ) 44 | ->where([ 45 | 'project_id' => $this->project['id'], 46 | 'ticket_type' => 'encoding', 47 | 'ticket_state' => 'released' 48 | ]) 49 | ->scoped([ 50 | 'with_recording', 51 | 'with_merged_properties' => [[ 52 | 'Fahrplan.GUID' => 'fahrplan_guid', 53 | 'Record.Cutdiffseconds' => 'duration', 54 | 'YouTube.Url0' => 'youtube_url', 55 | 'YouTube.Url1' => 'youtube_url_translated', 56 | 'YouTube.Url2' => 'youtube_url_translated_2' 57 | ]] 58 | ]); 59 | 60 | return $this->_respond($tickets); 61 | } 62 | 63 | protected function _respond($data) { 64 | if ($data instanceOf Model_Resource) { 65 | $data = $data->toArray(); 66 | } 67 | 68 | if ($this->respondTo('json')) { 69 | $this->Response->setContent(json_encode($data)); 70 | return $this->Response; 71 | } 72 | 73 | return Response::error(400); 74 | } 75 | 76 | } 77 | 78 | ?> -------------------------------------------------------------------------------- /src/Application/Controller/Application.php: -------------------------------------------------------------------------------- 1 | true, 14 | 'setProject' => true 15 | ]; 16 | 17 | protected $catch = [ 18 | 'NotFound' => 'notFound', 19 | 'ActionNotAllowed' => 'notAllowed' 20 | ]; 21 | 22 | protected $projectReadOnlyAccess = null; 23 | 24 | public function __construct() { 25 | User::recall(); 26 | } 27 | 28 | protected function addHeaders() { 29 | $this->Response->addHeader( 30 | 'Content-Security-Policy', 31 | 'default-src \'self\';' . 32 | 'font-src \'none\'; style-src \'self\' \'unsafe-inline\'; frame-src \'none\'; object-src \'none\';' 33 | ); 34 | 35 | $this->Response->addHeader('X-Content-Type-Options', 'nosniff'); 36 | $this->Response->addHeader('X-Frame-Options', 'DENY'); 37 | $this->Response->addHeader('X-XSS-Protection', '1; mode=block'); 38 | } 39 | 40 | protected function setProject($action, array $arguments) { 41 | if (!isset($arguments['project_slug'])) { 42 | return; 43 | } 44 | 45 | if (empty($arguments['project_slug'])) { 46 | $this->keepFlash(); 47 | return $this->redirect('projects', 'index'); 48 | } 49 | 50 | $this->project = Project::findAll() 51 | ->where(['slug' => $arguments['project_slug']]); 52 | 53 | if (User::isLoggedIn() and User::getCurrent()['restrict_project_access']) { 54 | $this->project->scoped(['filter_restricted' => [User::getCurrent()['id']]]); 55 | } 56 | 57 | $this->project = $this->project->first(); 58 | 59 | if ($this->project === null) { 60 | throw new EntryNotFoundException(); 61 | } 62 | 63 | $this->project['project_slug'] = $this->project['slug']; 64 | 65 | if ( 66 | $this->project['read_only'] and 67 | $this->projectReadOnlyAccess !== null and 68 | empty($this->projectReadOnlyAccess[$action]) 69 | ) { 70 | $this->flash('You can\'t alter tickets in this project because it\'s read only'); 71 | return $this->redirect('tickets', 'index', $this->project); 72 | } 73 | } 74 | 75 | public function notFound() { 76 | return $this->render('404', ['responseCode' => 404]); 77 | } 78 | 79 | public function notAllowed() { 80 | if (!User::isLoggedIn()) { 81 | $_SESSION[Model_Authentication_Session::SESSION_UNSAFE_KEY] = [ 82 | 'return_to' => $this->Request->getPath() 83 | ]; 84 | 85 | $this->flash('You have to log in to view this page'); 86 | return $this->redirect('user', 'login'); 87 | } 88 | 89 | return $this->render('403', ['responseCode' => 403]); 90 | } 91 | 92 | // TODO: redirectWithReference($default, array('ref1' => […], 'ref2' => …)) 93 | 94 | protected function flashView($view, $type = self::FLASH_NOTICE) { 95 | $this->flash('', $type, ['render' => $view]); 96 | } 97 | 98 | protected function flashViewNow($view, $type = self::FLASH_NOTICE) { 99 | $this->flashNow('', $type, ['render' => $view]); 100 | } 101 | 102 | } 103 | 104 | ?> 105 | -------------------------------------------------------------------------------- /src/Application/Controller/Encodingprofiles.php: -------------------------------------------------------------------------------- 1 | profiles = EncodingProfile::findAll() 14 | ->scoped(['with_version_count']) 15 | ->orderBy('slug'); 16 | 17 | return $this->render('encoding/profiles/index'); 18 | } 19 | 20 | public function view(array $arguments) { 21 | $this->profile = EncodingProfile::findOrThrow($arguments['id']); 22 | 23 | $this->form('encodingprofiles', 'compare', $this->profile); 24 | $this->versions = $this->profile->Versions->orderBy('revision DESC'); 25 | 26 | return $this->render('encoding/profiles/view'); 27 | } 28 | 29 | public function compare(array $arguments) { 30 | $this->profile = EncodingProfile::findOrThrow($arguments['id']); 31 | 32 | $values = $this->form()->getValues(); 33 | 34 | if (!isset($values['version_a']) or !isset($values['version_b'])) { 35 | return $this->redirect('encodingprofiles', 'view', $this->profile); 36 | } 37 | 38 | if ($values['version_a'] == $values['version_b']) { 39 | $this->flash('Cannot compare a version with itself'); 40 | return $this->redirect('encodingprofiles', 'view', $this->profile); 41 | } 42 | 43 | $versions = EncodingProfileVersion::findAll(array()) 44 | /*->select('xml_template')*/ 45 | ->where(array('id' => array($values['version_a'], $values['version_b']))) 46 | ->indexBy('id', 'xml_template') 47 | ->toArray(); 48 | 49 | $this->Response->setContentType('text/plain'); 50 | $this->Response->setContent(xdiff_string_diff( 51 | $versions[$values['version_a']], 52 | $versions[$values['version_b']] 53 | )); 54 | } 55 | 56 | public function create() { 57 | $this->form(); 58 | 59 | if ($this->form->wasSubmitted() and EncodingProfile::create($this->form->getValues())) { 60 | $this->flash('Encoding profile created'); 61 | return $this->redirect('encodingprofiles', 'index'); 62 | } 63 | 64 | $this->profiles = EncodingProfile::findAll() 65 | ->select('id, name') 66 | ->orderBy('slug') 67 | ->indexBy('id', 'name'); 68 | 69 | return $this->render('encoding/profiles/edit'); 70 | } 71 | 72 | public function edit(array $arguments) { 73 | $this->profile = EncodingProfile::findOrThrow($arguments['id']); 74 | 75 | $this->form(); 76 | 77 | if ($this->form->getValue('save') and $this->profile->save($this->form->getValues())) { 78 | $error = EncodingProfileVersion::isTemplateValid($this->form->getValue('xml_template')); 79 | 80 | // TODO: move to Model validation 81 | if ($error !== true) { 82 | $this->flashNow('Template error: ' . $error); 83 | } else { 84 | if ($this->form->getValue('create_version')) { 85 | $version = new EncodingProfileVersion([ 86 | 'encoding_profile_id' => $this->profile['id'] 87 | // TODO: save based version 88 | ]); 89 | 90 | $this->flash('Encoding profile updated, new profile version created'); 91 | } else { 92 | $version = EncodingProfileVersion::find($this->form->getValue('version')); 93 | $this->flash('Encoding profile updated'); 94 | } 95 | 96 | $version->save($this->form->getValues()); 97 | 98 | return $this->redirect('encodingprofiles', 'index'); 99 | } 100 | } 101 | 102 | if ($this->form->wasSubmitted()) { 103 | $this->version = EncodingProfileVersion::findBy([ 104 | 'id' => $this->form->getValue('version'), 105 | 'encoding_profile_id' => $arguments['id'] 106 | ], [], []); 107 | } elseif (isset($arguments['version'])) { 108 | $this->version = EncodingProfileVersion::findBy([ 109 | 'id' => $arguments['version'], 110 | 'encoding_profile_id' => $arguments['id'] 111 | ], [], []); 112 | } else { 113 | $this->version = $this->profile->LatestVersion; 114 | } 115 | 116 | $this->versions = $this->profile 117 | ->Versions 118 | ->orderBy('revision DESC') 119 | ->select('id, revision, description, created'); 120 | $this->profiles = EncodingProfile::findAll() 121 | ->select('id, name') 122 | ->orderBy('slug') 123 | ->whereNot(['id' => $this->profile['id']]) 124 | ->indexBy('id', 'name'); 125 | 126 | return $this->render('encoding/profiles/edit'); 127 | } 128 | 129 | public function delete(array $arguments) { 130 | $profile = EncodingProfile::findOrThrow($arguments['id']); 131 | 132 | if ($profile->destroy()) { 133 | $this->flash('Encoding profile ' . $profile['name'] . ' deleted'); 134 | } 135 | 136 | return $this->View->redirect('encodingprofiles', 'index'); 137 | } 138 | 139 | } 140 | 141 | ?> -------------------------------------------------------------------------------- /src/Application/Controller/Workers.php: -------------------------------------------------------------------------------- 1 | groups = WorkerGroup::findAll() 22 | ->includes(['Worker']) 23 | ->orderBy('title'); 24 | 25 | return $this->render('workers/index'); 26 | } 27 | 28 | public function queue(array $arguments) { 29 | $this->group = WorkerGroup::findOrThrow($arguments['id']); 30 | 31 | if ($this->group['paused']) { 32 | $this->flashNow('Worker group is paused'); 33 | } 34 | 35 | $projects = $this->group->Project->pluck('id'); 36 | $tickets = Ticket::findAll() 37 | ->from('view_serviceable_tickets', 'tbl_ticket') 38 | ->where([ 39 | 'project_id' => $projects, 40 | 'service_executable' => true 41 | ]); 42 | 43 | if (User::isRestricted()) { 44 | $tickets->scoped([ 45 | 'filter_restricted' => [User::getCurrent()['id']] 46 | ]); 47 | } 48 | 49 | $this->filtered = $this->group 50 | ->getFilteredTickets($projects, $tickets); 51 | 52 | $tickets = $tickets->pluck('id'); 53 | 54 | if (!empty($tickets)) { 55 | $this->queue = Ticket::findAll() 56 | // Join Handle for parent tickets, children should not be assigned 57 | ->andSelect('ticket_priority(id) AS priority_product') 58 | ->join([ 59 | 'Handle', 60 | 'Project' 61 | ]) 62 | ->scoped([ 63 | 'with_child', 64 | 'with_default_properties', 65 | 'with_encoding_profile_name', 66 | 'with_progress', 67 | 'order_priority' 68 | ]) 69 | ->orWhere([ 70 | 'id' => $tickets, 71 | 'child.id' => $tickets 72 | ]); 73 | } 74 | 75 | return $this->render('workers/group/queue'); 76 | } 77 | 78 | public function pause(array $arguments) { 79 | $group = WorkerGroup::findOrThrow($arguments['id']); 80 | 81 | if ($group->save(['paused' => true])) { 82 | $this->flash('Worker group paused'); 83 | } 84 | 85 | return $this->redirect('workers', 'index'); 86 | } 87 | 88 | public function unpause(array $arguments) { 89 | $group = WorkerGroup::findOrThrow($arguments['id']); 90 | 91 | if ($group->save(['paused' => false])) { 92 | $this->flash('Worker group continued'); 93 | } 94 | 95 | return $this->redirect('workers', 'index'); 96 | } 97 | 98 | public function create_group() { 99 | $this->form(); 100 | 101 | $group = new WorkerGroup($this->form->getValues()); 102 | $group['token'] = Random::friendly(32); 103 | $group['secret'] = Random::friendly(32); 104 | 105 | if ($this->form->wasSubmitted() and $group->save()) { 106 | $this->flash('Worker group created'); 107 | return $this->redirect('workers', 'index'); 108 | } 109 | 110 | return $this->render('workers/group/edit'); 111 | } 112 | 113 | public function edit_group(array $arguments) { 114 | $this->group = WorkerGroup::findOrThrow($arguments['id']); 115 | 116 | $this->form(); 117 | 118 | if ($this->form->wasSubmitted()) { 119 | if ($this->form->getValue('create_secret')) { 120 | $this->group['secret'] = Random::friendly(32); 121 | } 122 | 123 | if ($this->group->save($this->form->getValues())) { 124 | if ($this->form->getValue('create_secret')) { 125 | $this->flashNow('New secret created'); 126 | } else { 127 | $this->flash('Worker group updated'); 128 | return $this->redirect('workers', 'index'); 129 | } 130 | } 131 | } 132 | 133 | return $this->render('workers/group/edit'); 134 | } 135 | 136 | public function delete_group(array $arguments) { 137 | WorkerGroup::deleteOrThrow($arguments['id']); 138 | 139 | $this->flash('Worker group deleted'); 140 | return $this->redirect('workers', 'index'); 141 | } 142 | 143 | } 144 | 145 | ?> 146 | -------------------------------------------------------------------------------- /src/Application/Helper/EncodingProfile.php: -------------------------------------------------------------------------------- 1 | format('d.m.Y H:i') . ')'; 7 | } 8 | 9 | function encodingProfileTitle(array $entry) { 10 | return $entry['name'] . ' (r' . $entry['revision'] . ')'; 11 | } 12 | 13 | ?> -------------------------------------------------------------------------------- /src/Application/Helper/Ticket.php: -------------------------------------------------------------------------------- 1 | '', 16 | '300' => '5 minutes', 17 | '600' => '10 minutes', 18 | '1200' => '20 minutes', 19 | '1800' => '30 minutes', 20 | '3600' => '60 minutes', 21 | '5400' => '90 minutes' 22 | ]; 23 | } 24 | 25 | ?> -------------------------------------------------------------------------------- /src/Application/Helper/Time.php: -------------------------------------------------------------------------------- 1 | $dateTime->format('Y-m-d H:i:s'), 12 | 'data-tooltip' => true, 13 | 'datetime' => $dateTime->format('c') 14 | ], 15 | timeRelativeDifference($dateTime, null, $prefix) 16 | ); 17 | } 18 | 19 | function timeRelativeDifference(DateTime $dateTime, $now = null, $prefix = 'on ') { 20 | if ($now === null) { 21 | $now = new DateTime(); 22 | } 23 | 24 | $seconds = ($now->getTimestamp() - $dateTime->getTimestamp()); 25 | $minutes = round($seconds / 60); 26 | $hours = round($minutes / 60); 27 | 28 | if ($seconds < 10) { 29 | return 'a second ago'; 30 | } elseif ($seconds < 45) { 31 | return $seconds . ' seconds ago'; 32 | } elseif ($seconds < 90) { 33 | return 'a minute ago'; 34 | } elseif ($minutes < 45) { 35 | return $minutes . ' minutes ago'; 36 | } elseif ($minutes < 90) { 37 | return 'an hour ago'; 38 | } elseif ($hours < 7) { 39 | return $hours . ' hours ago'; 40 | } 41 | 42 | $days = (($hours + ((int) $dateTime->format('H'))) / 24); 43 | 44 | if ($days < 1) { 45 | return 'today at ' . $dateTime->format('H:i'); 46 | } elseif ($days < 2) { 47 | return 'yesterday at ' . $dateTime->format('H:i'); 48 | } elseif ($days < 7) { 49 | return $dateTime->format('l') . ' at ' . $dateTime->format('H:i'); 50 | } 51 | 52 | $year = $dateTime->format('Y'); 53 | 54 | return $prefix . $dateTime->format('M j') . 55 | (($now->format('Y') !== $year)? (', ' . $year) : ''); 56 | } 57 | 58 | function formatDuration($duration) { 59 | $seconds = $duration % 60; 60 | $minutes = ($duration / 60) % 60; 61 | $hours = floor($duration / 60 / 60); 62 | 63 | return (($hours > 0)? $hours . 'h ' : '') . 64 | (($minutes > 0)? $minutes . 'm ' : '') . 65 | (($seconds > 0)? $seconds . 's ' : ''); 66 | } 67 | 68 | ?> 69 | -------------------------------------------------------------------------------- /src/Application/Migrations/00_extensions.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE EXTENSION IF NOT EXISTS ltree SCHEMA pg_catalog; 6 | 7 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/02_handles.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | ---------------------------------------- 6 | --- users, worker groups and workers --- 7 | ---------------------------------------- 8 | 9 | CREATE TABLE tbl_handle 10 | ( 11 | id bigserial NOT NULL, 12 | last_seen timestamp with time zone NOT NULL DEFAULT now(), 13 | name character varying(128) NOT NULL, 14 | CONSTRAINT tbl_handle_pkey PRIMARY KEY (id) 15 | ) 16 | WITHOUT OIDS; 17 | 18 | CREATE OR REPLACE FUNCTION valid_handle() RETURNS TRIGGER AS $$ 19 | BEGIN 20 | IF NEW.handle_id IS NULL THEN 21 | RETURN NEW; 22 | END IF; 23 | IF NOT EXISTS(SELECT id FROM tbl_handle WHERE id = NEW.handle_id) 24 | THEN RAISE EXCEPTION 'Handle % not found', NEW.handle_id; 25 | END IF; 26 | RETURN NEW; 27 | END $$ LANGUAGE plpgsql; 28 | 29 | CREATE TABLE tbl_user 30 | ( 31 | id bigint NOT NULL DEFAULT nextval('tbl_handle_id_seq'::regclass), 32 | last_seen timestamp with time zone, 33 | name character varying(128) NOT NULL, 34 | password character(60) DEFAULT NULL::bpchar, 35 | persistence_token character(32) DEFAULT NULL::bpchar, 36 | remember_token character(32) DEFAULT NULL::bpchar, 37 | role character varying(32) DEFAULT 'user'::character varying, 38 | restrict_project_access bool NOT NULL DEFAULT FALSE, 39 | failed_login_count integer DEFAULT 0, 40 | last_login timestamp with time zone, 41 | CONSTRAINT tbl_user_pk PRIMARY KEY (id), 42 | CONSTRAINT tbl_user_name_uq UNIQUE (name), 43 | CONSTRAINT tbl_user_persistence_token_key UNIQUE (persistence_token), 44 | CONSTRAINT tbl_user_remember_token_key UNIQUE (remember_token) 45 | ) 46 | INHERITS (tbl_handle) 47 | WITHOUT OIDS; 48 | 49 | CREATE TABLE tbl_worker_group 50 | ( 51 | id bigserial NOT NULL, 52 | title character varying(256) NOT NULL, 53 | token character(32) NOT NULL, 54 | secret character(32) NOT NULL, 55 | paused boolean DEFAULT FALSE NOT NULL, 56 | CONSTRAINT tbl_worker_group_pk PRIMARY KEY (id), 57 | CONSTRAINT tbl_worker_group_token_uq UNIQUE (token) 58 | ) 59 | WITHOUT OIDS; 60 | 61 | CREATE TABLE tbl_worker 62 | ( 63 | id bigint NOT NULL DEFAULT nextval('tbl_handle_id_seq'::regclass), 64 | last_seen timestamp with time zone NOT NULL DEFAULT now(), 65 | name character varying(128) NOT NULL, 66 | worker_group_id bigint NOT NULL, 67 | description character varying(256), 68 | CONSTRAINT tbl_worker_pkey PRIMARY KEY (id), 69 | CONSTRAINT tbl_worker_worker_group_id_fkey FOREIGN KEY (worker_group_id) 70 | REFERENCES tbl_worker_group (id) MATCH SIMPLE 71 | ON UPDATE NO ACTION ON DELETE NO ACTION, 72 | CONSTRAINT tbl_worker_name_group_uq UNIQUE (name, worker_group_id) 73 | ) 74 | INHERITS (tbl_handle) 75 | WITHOUT OIDS; 76 | 77 | COMMIT; 78 | -------------------------------------------------------------------------------- /src/Application/Migrations/03_encoding_profiles.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | ------------------------- 6 | --- encoding profiles --- 7 | ------------------------- 8 | 9 | CREATE TABLE tbl_encoding_profile 10 | ( 11 | id bigserial NOT NULL, 12 | name character varying(256) NOT NULL, 13 | slug character varying(64), 14 | extension character varying(16), 15 | mirror_folder character varying(256), 16 | depends_on bigint, 17 | CONSTRAINT tbl_encoding_profile_pk PRIMARY KEY (id), 18 | CONSTRAINT tbl_encoding_profile_depends_on_fkey FOREIGN KEY (depends_on) 19 | REFERENCES tbl_encoding_profile (id) MATCH SIMPLE 20 | ON UPDATE NO ACTION ON DELETE NO ACTION 21 | ) 22 | WITHOUT OIDS; 23 | 24 | CREATE TABLE tbl_encoding_profile_version 25 | ( 26 | id bigserial NOT NULL, 27 | encoding_profile_id bigint NOT NULL, 28 | revision bigint NOT NULL DEFAULT 1, 29 | created timestamp with time zone NOT NULL DEFAULT now(), 30 | description character varying(4096), 31 | xml_template text NOT NULL, 32 | CONSTRAINT tbl_encoding_profile_version_pkey PRIMARY KEY (id), 33 | CONSTRAINT tbl_encoding_profile_version_encoding_profile_id_fkey FOREIGN KEY (encoding_profile_id) 34 | REFERENCES tbl_encoding_profile (id) MATCH SIMPLE 35 | ON UPDATE NO ACTION ON DELETE NO ACTION, 36 | CONSTRAINT tbl_encoding_profile_version_encoding_profile_id_revision_key UNIQUE (encoding_profile_id, revision) 37 | ) 38 | WITHOUT OIDS; 39 | 40 | CREATE TABLE tbl_encoding_profile_property 41 | ( 42 | encoding_profile_id bigint NOT NULL, 43 | name ltree NOT NULL CHECK (char_length(name::text) > 0), 44 | value text NOT NULL, 45 | CONSTRAINT tbl_encoding_profile_property_pk PRIMARY KEY (encoding_profile_id, name), 46 | CONSTRAINT tbl_encoding_profile_property_project_fk FOREIGN KEY (encoding_profile_id) 47 | REFERENCES tbl_encoding_profile (id) MATCH SIMPLE 48 | ON UPDATE CASCADE ON DELETE CASCADE 49 | ) 50 | WITHOUT OIDS; 51 | 52 | CREATE OR REPLACE FUNCTION increment_encoding_profile_revision() RETURNS trigger AS 53 | $BODY$ 54 | DECLARE 55 | rev integer; 56 | BEGIN 57 | SELECT COALESCE(MAX(revision),0) + 1 INTO rev FROM tbl_encoding_profile_version WHERE encoding_profile_id = NEW.encoding_profile_id; 58 | 59 | NEW.revision := rev; 60 | 61 | RETURN NEW; 62 | END; 63 | $BODY$ 64 | LANGUAGE plpgsql VOLATILE; 65 | 66 | CREATE TRIGGER increment_encoding_profile_revision BEFORE INSERT ON tbl_encoding_profile_version FOR EACH ROW EXECUTE PROCEDURE increment_encoding_profile_revision(); 67 | 68 | COMMIT; 69 | -------------------------------------------------------------------------------- /src/Application/Migrations/04_projects.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | --------------- 6 | --- project --- 7 | --------------- 8 | 9 | CREATE TABLE tbl_project 10 | ( 11 | id bigserial NOT NULL, 12 | title character varying(256) NOT NULL CHECK (char_length(title::text) > 0), 13 | slug character varying(64) NOT NULL CHECK (char_length(slug::text) > 0), 14 | dependee_ticket_trigger_state enum_ticket_state NOT NULL DEFAULT 'released', 15 | read_only boolean NOT NULL DEFAULT false, 16 | created timestamp with time zone NOT NULL DEFAULT now(), 17 | modified timestamp with time zone NOT NULL DEFAULT now(), 18 | CONSTRAINT tbl_project_pk PRIMARY KEY (id), 19 | CONSTRAINT tbl_project_slug_uk UNIQUE (slug) 20 | ) 21 | WITHOUT OIDS; 22 | 23 | CREATE TABLE tbl_project_language 24 | ( 25 | project_id bigint NOT NULL, 26 | language character varying(50) NOT NULL CHECK (char_length(language::text) > 0), 27 | description character varying(256) NOT NULL, 28 | CONSTRAINT tbl_project_language_pk PRIMARY KEY (project_id, language), 29 | CONSTRAINT tbl_project_language_project_fk FOREIGN KEY (project_id) 30 | REFERENCES tbl_project (id) MATCH SIMPLE 31 | ON UPDATE CASCADE ON DELETE CASCADE 32 | ) 33 | WITHOUT OIDS; 34 | 35 | CREATE TABLE tbl_project_property 36 | ( 37 | project_id bigint NOT NULL, 38 | name ltree NOT NULL CHECK (char_length(name::text) > 0), 39 | value text NOT NULL, 40 | CONSTRAINT tbl_project_property_pk PRIMARY KEY (project_id, name), 41 | CONSTRAINT tbl_project_property_project_fk FOREIGN KEY (project_id) 42 | REFERENCES tbl_project (id) MATCH SIMPLE 43 | ON UPDATE CASCADE ON DELETE CASCADE 44 | ) 45 | WITHOUT OIDS; 46 | 47 | CREATE TABLE tbl_project_ticket_state 48 | ( 49 | project_id bigint NOT NULL, 50 | ticket_type enum_ticket_type NOT NULL, 51 | ticket_state enum_ticket_state NOT NULL, 52 | service_executable boolean NOT NULL DEFAULT false, 53 | CONSTRAINT tbl_project_ticket_state_pk PRIMARY KEY (project_id, ticket_type, ticket_state), 54 | CONSTRAINT tbl_project_ticket_state_project_fk FOREIGN KEY (project_id) 55 | REFERENCES tbl_project (id) MATCH SIMPLE 56 | ON UPDATE CASCADE ON DELETE CASCADE 57 | ) 58 | WITHOUT OIDS; 59 | 60 | CREATE TABLE tbl_project_worker_group 61 | ( 62 | project_id bigint NOT NULL, 63 | worker_group_id bigint NOT NULL, 64 | CONSTRAINT tbl_project_worker_group_pk PRIMARY KEY (project_id, worker_group_id), 65 | CONSTRAINT tbl_project_worker_group_group_fk FOREIGN KEY (worker_group_id) 66 | REFERENCES tbl_worker_group (id) MATCH SIMPLE 67 | ON UPDATE CASCADE ON DELETE CASCADE, 68 | CONSTRAINT tbl_project_worker_group_project_fk FOREIGN KEY (project_id) 69 | REFERENCES tbl_project (id) MATCH SIMPLE 70 | ON UPDATE CASCADE ON DELETE CASCADE 71 | ) 72 | WITHOUT OIDS; 73 | 74 | CREATE TABLE tbl_project_worker_group_filter 75 | ( 76 | id bigserial NOT NULL, 77 | project_id bigint NOT NULL, 78 | worker_group_id bigint NOT NULL, 79 | property_key ltree NOT NULL CHECK (char_length(property_key::text) > 0), 80 | property_value character varying(8196) NOT NULL, 81 | CONSTRAINT tbl_project_worker_group_filter_pk PRIMARY KEY (id), 82 | CONSTRAINT tbl_project_worker_group_filter_group_fk FOREIGN KEY (worker_group_id) 83 | REFERENCES tbl_worker_group (id) MATCH SIMPLE 84 | ON UPDATE CASCADE ON DELETE CASCADE, 85 | CONSTRAINT tbl_project_worker_group_filter_project_fk FOREIGN KEY (project_id) 86 | REFERENCES tbl_project (id) MATCH SIMPLE 87 | ON UPDATE CASCADE ON DELETE CASCADE 88 | ) 89 | WITHOUT OIDS; 90 | 91 | CREATE TABLE tbl_project_encoding_profile 92 | ( 93 | project_id bigint NOT NULL, 94 | encoding_profile_version_id bigint NOT NULL, 95 | priority double precision NOT NULL DEFAULT 1, 96 | auto_create boolean NOT NULL default true, 97 | CONSTRAINT tbl_project_encoding_profile_pkey PRIMARY KEY (project_id, encoding_profile_version_id), 98 | CONSTRAINT tbl_project_encoding_profile_encoding_profile_version_id_fkey FOREIGN KEY (encoding_profile_version_id) 99 | REFERENCES tbl_encoding_profile_version (id) MATCH SIMPLE 100 | ON UPDATE NO ACTION ON DELETE NO ACTION, 101 | CONSTRAINT tbl_project_encoding_profile_project_id_fkey FOREIGN KEY (project_id) 102 | REFERENCES tbl_project (id) MATCH SIMPLE 103 | ON UPDATE CASCADE ON DELETE CASCADE 104 | ) 105 | WITHOUT OIDS; 106 | 107 | CREATE TABLE tbl_user_project_restrictions 108 | ( 109 | user_id bigint NOT NULL, 110 | project_id bigint NOT NULL, 111 | role character varying(32) DEFAULT 'user'::character varying, 112 | CONSTRAINT tbl_user_project_restrictions_pk PRIMARY KEY (user_id, project_id), 113 | CONSTRAINT tbl_user_project_restrictions_user_fk FOREIGN KEY (user_id) 114 | REFERENCES tbl_user (id) MATCH SIMPLE 115 | ON UPDATE CASCADE ON DELETE CASCADE, 116 | CONSTRAINT tbl_user_project_restrictions_project_fk FOREIGN KEY (project_id) 117 | REFERENCES tbl_project (id) MATCH SIMPLE 118 | ON UPDATE CASCADE ON DELETE CASCADE 119 | ) 120 | WITHOUT OIDS; 121 | 122 | COMMIT; 123 | -------------------------------------------------------------------------------- /src/Application/Migrations/05_import.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE enum_import_auth_type AS ENUM ( 2 | 'basic', 3 | 'header'); 4 | 5 | CREATE TABLE tbl_import 6 | ( 7 | id bigserial NOT NULL, 8 | project_id bigint NOT NULL, 9 | user_id bigint NOT NULL, 10 | url text NOT NULL, 11 | auth_type enum_import_auth_type, 12 | auth_user character varying(256), 13 | auth_password character varying(256), 14 | auth_header text, 15 | xml xml NOT NULL, 16 | version character varying(128) NOT NULL, 17 | rooms json, 18 | changes json, 19 | created timestamp with time zone NOT NULL, 20 | finished timestamp with time zone, 21 | PRIMARY KEY (id), 22 | CONSTRAINT tbl_import_user_fk FOREIGN KEY (user_id) 23 | REFERENCES tbl_user (id) 24 | ON UPDATE SET NULL ON DELETE SET NULL, 25 | CONSTRAINT tbl_import_project_fk FOREIGN KEY (project_id) 26 | REFERENCES tbl_project (id) MATCH SIMPLE 27 | ON UPDATE CASCADE ON DELETE CASCADE 28 | ) WITH OIDS; -------------------------------------------------------------------------------- /src/Application/Migrations/07_comments.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE TABLE tbl_comment 6 | ( 7 | id bigserial NOT NULL, 8 | ticket_id bigint NOT NULL, 9 | referenced_ticket_id bigint, 10 | handle_id bigint NOT NULL, 11 | created timestamp with time zone NOT NULL DEFAULT now(), 12 | comment text, 13 | CONSTRAINT tbl_comment_pk PRIMARY KEY (id), 14 | CONSTRAINT tbl_comment_log_fkt_ticket_fk FOREIGN KEY (ticket_id) REFERENCES tbl_ticket (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE, 15 | CONSTRAINT tbl_comment_log_fkt_referenced_ticket_fk FOREIGN KEY (referenced_ticket_id) REFERENCES tbl_ticket (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE SET NULL 16 | ) 17 | WITHOUT OIDS; 18 | CREATE TRIGGER valid_handle BEFORE INSERT OR UPDATE ON tbl_comment FOR EACH ROW EXECUTE PROCEDURE valid_handle(); 19 | 20 | COMMIT; 21 | -------------------------------------------------------------------------------- /src/Application/Migrations/08_log.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE TABLE tbl_log 6 | ( 7 | id bigserial NOT NULL, 8 | ticket_id bigint NOT NULL, 9 | created timestamp with time zone NOT NULL DEFAULT now(), 10 | from_state enum_ticket_state, 11 | to_state enum_ticket_state, 12 | handle_id bigint NOT NULL, 13 | "comment" text, 14 | event character varying(255) NOT NULL, 15 | CONSTRAINT tbl_log_pk PRIMARY KEY (id), 16 | CONSTRAINT tbl_log_ticket_fk FOREIGN KEY (ticket_id) 17 | REFERENCES tbl_ticket (id) MATCH SIMPLE 18 | ON UPDATE CASCADE ON DELETE CASCADE 19 | ) 20 | WITHOUT OIDS; 21 | 22 | -- trigger 23 | CREATE TRIGGER valid_handle BEFORE INSERT OR UPDATE ON tbl_log FOR EACH ROW EXECUTE PROCEDURE valid_handle(); 24 | 25 | COMMIT; 26 | -------------------------------------------------------------------------------- /src/Application/Migrations/10_function_create_missing_encoding_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION create_missing_encoding_tickets(param_project_id bigint) RETURNS integer AS $$ 6 | DECLARE 7 | row_count integer; 8 | BEGIN 9 | row_count := 0; 10 | 11 | INSERT INTO tbl_ticket (parent_id, project_id, fahrplan_id, priority, ticket_type, ticket_state, encoding_profile_version_id) 12 | (SELECT 13 | t1.id as parent_id, 14 | t1.project_id, 15 | t1.fahrplan_id, 16 | pep.priority, 17 | 'encoding' as ticket_type, 18 | ticket_state_initial(param_project_id, 'encoding') AS ticket_state, 19 | pep.encoding_profile_version_id 20 | FROM 21 | tbl_project_encoding_profile pep 22 | JOIN 23 | tbl_encoding_profile_version epv ON pep.encoding_profile_version_id = epv.id 24 | JOIN 25 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 26 | LEFT OUTER JOIN 27 | tbl_ticket t1 ON pep.project_id = t1.project_id 28 | LEFT JOIN 29 | tbl_ticket t2 ON t2.parent_id = t1.id AND t2.encoding_profile_version_id = epv.id 30 | WHERE 31 | t1.ticket_type = 'meta' AND 32 | t2.id IS NULL AND 33 | pep.project_id = param_project_id AND 34 | pep.auto_create IS TRUE 35 | ORDER BY t1.id ASC, ep.id ASC); 36 | GET DIAGNOSTICS row_count = ROW_COUNT; 37 | return row_count; 38 | END; 39 | $$ LANGUAGE plpgsql; 40 | 41 | COMMIT; 42 | -------------------------------------------------------------------------------- /src/Application/Migrations/11_function_ticket_fahrplan_starttime.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_fahrplan_starttime(param_ticket_id bigint) RETURNS integer AS $$ 6 | DECLARE 7 | unixtime integer; 8 | BEGIN 9 | SELECT 10 | EXTRACT(EPOCH FROM p.value::timestamp with time zone) INTO unixtime 11 | FROM tbl_ticket_property p 12 | WHERE p.name = 'Fahrplan.DateTime' AND p.ticket_id = param_ticket_id; 13 | 14 | RETURN unixtime; 15 | END 16 | $$ LANGUAGE plpgsql; 17 | 18 | COMMIT; 19 | -------------------------------------------------------------------------------- /src/Application/Migrations/12_function_ticket_priority.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_priority(param_ticket_id bigint) RETURNS float AS $$ 6 | DECLARE 7 | priority float; 8 | BEGIN 9 | SELECT 10 | CASE WHEN t.parent_id IS NOT NULL THEN 11 | pt.priority * t.priority * COALESCE(extract(EPOCH FROM CURRENT_TIMESTAMP) / ticket_fahrplan_starttime(pt.id), 1) * COALESCE((SELECT pep.priority FROM tbl_project_encoding_profile pep WHERE pep.project_id = t.project_id AND pep.encoding_profile_version_id = t.encoding_profile_version_id),1) 12 | ELSE 13 | t.priority * COALESCE(extract(EPOCH FROM CURRENT_TIMESTAMP) / ticket_fahrplan_starttime(t.id), 1) 14 | END INTO priority 15 | FROM 16 | tbl_ticket t 17 | LEFT JOIN 18 | tbl_ticket pt ON pt.id = t.parent_id 19 | WHERE 20 | t.id = param_ticket_id; 21 | 22 | RETURN priority; 23 | END 24 | $$ LANGUAGE plpgsql; 25 | 26 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/13_function_ticket_progress.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_state_progress(param_project_id bigint, param_ticket_type enum_ticket_type, param_ticket_state enum_ticket_state) RETURNS float AS $$ 6 | DECLARE 7 | progress float; 8 | BEGIN 9 | SELECT 10 | CEIL(SUM(ts2.percent_progress) / (SELECT GREATEST(1, SUM(ts3.percent_progress)) FROM tbl_ticket_state ts3 JOIN tbl_project_ticket_state pts ON pts.ticket_type = ts3.ticket_type AND pts.ticket_state = ts3.ticket_state WHERE ts3.ticket_type = ts1.ticket_type AND pts.project_id = param_project_id) * 100) 11 | INTO 12 | progress 13 | FROM 14 | tbl_ticket_state ts1 15 | JOIN 16 | tbl_project_ticket_state pts ON pts.ticket_type = ts1.ticket_type AND pts.ticket_state = ts1.ticket_state 17 | JOIN 18 | tbl_ticket_state ts2 ON ts1.ticket_type = ts2.ticket_type AND ts1.sort >= ts2.sort 19 | JOIN 20 | tbl_project_ticket_state pts2 ON pts2.ticket_type = ts2.ticket_type AND pts2.ticket_state = ts2.ticket_state AND pts2.project_id = pts.project_id 21 | WHERE 22 | pts.project_id = param_project_id AND ts1.ticket_type = param_ticket_type AND ts1.ticket_state = param_ticket_state 23 | GROUP BY 24 | ts1.ticket_state, ts1.ticket_type, ts1.sort 25 | ; 26 | IF progress IS NULL THEN 27 | progress := 0; 28 | END IF; 29 | 30 | RETURN progress; 31 | END 32 | $$ LANGUAGE plpgsql; 33 | 34 | 35 | CREATE OR REPLACE FUNCTION ticket_progress(param_ticket_id bigint) RETURNS float AS $$ 36 | DECLARE 37 | progress float; 38 | BEGIN 39 | SELECT SUM(percent_progress) / COUNT(id) INTO progress FROM ( 40 | SELECT t.id, ticket_state_progress(t.project_id, t.ticket_type, t.ticket_state) AS percent_progress FROM tbl_ticket t WHERE t.id = param_ticket_id AND t.parent_id IS NOT NULL 41 | UNION 42 | SELECT t.id, ticket_state_progress(t.project_id, t.ticket_type, t.ticket_state) AS percent_progress FROM tbl_ticket t WHERE t.parent_id = param_ticket_id 43 | ) as all_tickets; 44 | IF progress IS NULL THEN 45 | progress := 0; 46 | END IF; 47 | 48 | RETURN progress; 49 | END 50 | $$ LANGUAGE plpgsql; 51 | 52 | 53 | COMMIT; 54 | -------------------------------------------------------------------------------- /src/Application/Migrations/14_function_create_missing_recording_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION create_missing_recording_tickets(param_project_id bigint) RETURNS integer AS $$ 6 | DECLARE 7 | row_count integer; 8 | BEGIN 9 | row_count := 0; 10 | 11 | IF NOT EXISTS 12 | (SELECT 1 13 | FROM tbl_project_ticket_state s 14 | WHERE s.project_id = param_project_id AND s.ticket_type = 'recording'::enum_ticket_type) 15 | THEN RETURN row_count; 16 | END IF; 17 | 18 | INSERT INTO tbl_ticket (parent_id, project_id, fahrplan_id, ticket_type, ticket_state) 19 | (SELECT 20 | t1.id as parent_id, 21 | t1.project_id, 22 | t1.fahrplan_id, 23 | 'recording' as ticket_type, 24 | ticket_state_initial(param_project_id, 'recording') AS ticket_state 25 | FROM 26 | tbl_ticket t1 27 | LEFT JOIN 28 | tbl_ticket t2 ON t2.parent_id = t1.id AND t2.ticket_type = 'recording' 29 | WHERE 30 | t1.ticket_type = 'meta' AND 31 | t1.project_id = param_project_id 32 | GROUP BY 33 | t1.id 34 | HAVING COUNT(t2.id) = 0 35 | ) 36 | ; 37 | 38 | GET DIAGNOSTICS row_count = ROW_COUNT; 39 | RETURN row_count; 40 | END; 41 | $$ LANGUAGE plpgsql; 42 | 43 | COMMIT; 44 | -------------------------------------------------------------------------------- /src/Application/Migrations/16_function_ticket_dependency.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_dependee_ticket_state(param_depender_ticket_id bigint) 6 | RETURNS enum_ticket_state AS 7 | $$ 8 | DECLARE 9 | state enum_ticket_state; 10 | BEGIN 11 | SELECT 12 | dependee.ticket_state INTO state 13 | FROM 14 | tbl_ticket depender 15 | JOIN 16 | tbl_encoding_profile_version epv ON epv.id = depender.encoding_profile_version_id 17 | JOIN 18 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 19 | JOIN 20 | tbl_encoding_profile ep2 ON ep2.id = ep.depends_on 21 | JOIN 22 | tbl_encoding_profile_version epv2 ON epv2.encoding_profile_id = ep2.id 23 | JOIN 24 | tbl_ticket dependee ON dependee.encoding_profile_version_id = epv2.id AND dependee.parent_id = depender.parent_id 25 | WHERE 26 | depender.id = param_depender_ticket_id; 27 | RETURN state; 28 | END 29 | $$ 30 | LANGUAGE plpgsql; 31 | 32 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/17_function_ticket_dependency_satisfied.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_dependee_ticket_state_satisfied(param_depender_ticket_id bigint) 6 | RETURNS boolean AS 7 | $$ 8 | DECLARE 9 | satisfaction boolean; 10 | dependee_state enum_ticket_state; 11 | BEGIN 12 | SELECT ticket_dependee_ticket_state(param_depender_ticket_id) 13 | INTO dependee_state; 14 | 15 | SELECT 16 | depender_ticket_state.sort < dependee_ticket_state.sort AND 17 | dependee_ticket_state.sort >= configured_trigger_state.sort INTO satisfaction 18 | FROM 19 | tbl_ticket depender_ticket 20 | JOIN 21 | tbl_ticket_state depender_ticket_state ON 22 | depender_ticket_state.ticket_type = depender_ticket.ticket_type AND 23 | depender_ticket_state.ticket_state = depender_ticket.ticket_state 24 | JOIN 25 | tbl_project project ON depender_ticket.project_id = project.id 26 | JOIN 27 | tbl_ticket_state dependee_ticket_state ON 28 | dependee_ticket_state.ticket_type = depender_ticket.ticket_type AND 29 | dependee_ticket_state.ticket_state = dependee_state 30 | JOIN 31 | tbl_ticket_state configured_trigger_state ON 32 | configured_trigger_state.ticket_type = depender_ticket.ticket_type AND 33 | configured_trigger_state.ticket_state = project.dependee_ticket_trigger_state 34 | WHERE 35 | depender_ticket.id = param_depender_ticket_id AND 36 | -- if given ticket is no encoding ticket, function will return NULL 37 | depender_ticket.ticket_type = 'encoding'; 38 | 39 | RETURN satisfaction; 40 | END 41 | $$ 42 | LANGUAGE plpgsql; 43 | 44 | COMMIT; 45 | -------------------------------------------------------------------------------- /src/Application/Migrations/18_function_ticket_dependency_missing.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | -- The function is named in this way so it might be clear that it only 6 | -- returns true if: 7 | -- - given ID is an encoding ticket 8 | -- - given ID's profile does have a dependency 9 | -- - no encoding ticket among the given ID's sibling tickets has that dependee profile 10 | -- In all other cases, including the cases where this check does not 11 | -- make any sense, it will return false. 12 | 13 | CREATE OR REPLACE FUNCTION ticket_dependee_missing(param_depender_ticket_id bigint) 14 | RETURNS boolean AS 15 | $$ 16 | DECLARE 17 | result boolean; 18 | BEGIN 19 | 20 | SELECT 21 | epv.id IS NOT NULL -- check this is actually an encoding ticket 22 | AND ep.depends_on IS NOT NULL -- check that this is a ticket of a depending profile 23 | AND dependee.id IS NULL -- this is the error condition to return true on 24 | INTO result 25 | FROM 26 | tbl_ticket depender 27 | LEFT JOIN 28 | tbl_encoding_profile_version epv ON epv.id = depender.encoding_profile_version_id 29 | LEFT JOIN 30 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 31 | LEFT JOIN 32 | tbl_encoding_profile ep2 ON ep2.id = ep.depends_on 33 | LEFT JOIN 34 | tbl_encoding_profile_version epv2 ON epv2.encoding_profile_id = ep2.id 35 | INNER JOIN 36 | tbl_ticket dependee ON dependee.encoding_profile_version_id = epv2.id AND dependee.parent_id = depender.parent_id 37 | WHERE 38 | depender.id = param_depender_ticket_id; 39 | 40 | RETURN COALESCE(result, false); 41 | 42 | END 43 | $$ 44 | LANGUAGE plpgsql; 45 | 46 | COMMIT; 47 | -------------------------------------------------------------------------------- /src/Application/Migrations/19_function_encodingprofile_dependency.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | -- function returns a table with all encoding profiles, which depend 6 | -- on the given encoding profile 7 | 8 | -- second parameter is optional and is used in recursive to prevent endless loop 9 | 10 | CREATE OR REPLACE FUNCTION encoding_profile_all_dependees(param_profile_id bigint, param_known_ids bigint[] DEFAULT array[]::bigint[]) 11 | RETURNS TABLE (id bigint) AS 12 | $$ 13 | DECLARE 14 | p_id bigint; 15 | dependee_profile_ids bigint[]; 16 | BEGIN 17 | IF array_dims(param_known_ids) ISNULL THEN 18 | param_known_ids := array[param_profile_id]; 19 | END IF; 20 | 21 | FOR p_id IN 22 | SELECT p.id 23 | FROM tbl_encoding_profile p 24 | WHERE depends_on=param_profile_id AND NOT (p.id = ANY(param_known_ids)) 25 | LOOP 26 | dependee_profile_ids := p_id || dependee_profile_ids; 27 | dependee_profile_ids := ARRAY(SELECT e.id from encoding_profile_all_dependees(p_id, dependee_profile_ids || param_known_ids) e) || dependee_profile_ids; 28 | END LOOP; 29 | RETURN QUERY SELECT DISTINCT unnest(dependee_profile_ids); 30 | END 31 | $$ 32 | LANGUAGE plpgsql; 33 | 34 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/20_view_parent_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP VIEW IF EXISTS view_parent_tickets; 6 | 7 | CREATE OR REPLACE VIEW view_parent_tickets AS 8 | SELECT 9 | t.*, 10 | pstart.value::timestamp with time zone AS time_start, 11 | pstart.value::timestamp with time zone + pdur.value::time without time zone::interval AS time_end, 12 | t.progress AS ticket_progress 13 | FROM 14 | tbl_ticket t 15 | LEFT JOIN 16 | tbl_ticket_property pdur ON pdur.ticket_id = t.id AND pdur.name = 'Fahrplan.Duration' 17 | LEFT JOIN 18 | tbl_ticket_property pstart ON pstart.ticket_id = t.id AND pstart.name = 'Fahrplan.DateTime'::ltree 19 | WHERE 20 | t.ticket_type = 'meta'; 21 | 22 | COMMIT; 23 | -------------------------------------------------------------------------------- /src/Application/Migrations/21_view_all_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP VIEW IF EXISTS view_all_tickets; 6 | 7 | CREATE OR REPLACE VIEW view_all_tickets AS 8 | SELECT 9 | t.*, 10 | pstart.value::timestamp with time zone AS time_start, 11 | pstart.value::timestamp with time zone + pdur.value::time without time zone::interval AS time_end, 12 | t.progress AS ticket_progress, 13 | (SELECT tr.ticket_state FROM tbl_ticket tr WHERE tr.ticket_type = 'recording'::enum_ticket_type AND (tr.parent_id = t.id OR tr.parent_id = t.parent_id) LIMIT 1) AS recording_ticket_state 14 | FROM 15 | tbl_ticket t 16 | LEFT JOIN 17 | tbl_ticket_property pdur ON pdur.ticket_id = t.id AND pdur.name = 'Fahrplan.Duration'::ltree 18 | LEFT JOIN 19 | tbl_ticket_property pstart ON pstart.ticket_id = t.id AND pstart.name = 'Fahrplan.DateTime'::ltree 20 | ORDER BY 21 | t.project_id ASC, time_start ASC, t.parent_id ASC; 22 | 23 | COMMIT; 24 | -------------------------------------------------------------------------------- /src/Application/Migrations/22_view_serviceable_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP VIEW IF EXISTS view_serviceable_tickets; 6 | 7 | CREATE OR REPLACE VIEW view_serviceable_tickets AS 8 | SELECT 9 | t.*, 10 | pstart.value::timestamp with time zone AS time_start, 11 | pstart.value::timestamp with time zone + pdur.value::time without time zone::interval AS time_end, 12 | proom.value AS room, 13 | parent.priority * t.priority * 14 | COALESCE( 15 | extract(EPOCH FROM CURRENT_TIMESTAMP) / 16 | extract(EPOCH FROM pstart.value::timestamp with time zone) 17 | , 1) * COALESCE(pep.priority, 1) AS calculated_priority 18 | FROM 19 | tbl_ticket t 20 | JOIN 21 | tbl_ticket parent ON parent.id = t.parent_id 22 | LEFT JOIN 23 | tbl_ticket_property pdur ON pdur.ticket_id = COALESCE(t.parent_id, t.id) AND pdur.name = 'Fahrplan.Duration'::ltree 24 | LEFT JOIN 25 | tbl_ticket_property pstart ON pstart.ticket_id = COALESCE(t.parent_id, t.id) AND pstart.name = 'Fahrplan.DateTime'::ltree 26 | LEFT JOIN 27 | tbl_ticket_property proom ON proom.ticket_id = COALESCE(t.parent_id,t.id) AND proom.name = 'Fahrplan.Room'::ltree 28 | LEFT JOIN 29 | tbl_project pj ON pj.id = t.project_id 30 | LEFT JOIN 31 | tbl_project_encoding_profile pep ON pep.project_id = pj.id AND pep.encoding_profile_version_id = t.encoding_profile_version_id 32 | LEFT JOIN 33 | tbl_ticket_state configured_trigger_state ON 34 | configured_trigger_state.ticket_type = 'encoding' AND 35 | configured_trigger_state.ticket_state = pj.dependee_ticket_trigger_state 36 | LEFT JOIN 37 | tbl_ticket_state depender_ticket_state ON 38 | depender_ticket_state.ticket_type = t.ticket_type AND 39 | depender_ticket_state.ticket_state = t.ticket_state 40 | LEFT JOIN 41 | tbl_encoding_profile_version epv ON epv.id = t.encoding_profile_version_id 42 | LEFT JOIN 43 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 44 | LEFT JOIN 45 | tbl_encoding_profile dependee_profile ON dependee_profile.id = ep.depends_on 46 | -- Using lateral join to split evaluation at this point. The tables and 47 | -- corresponding WHERE clauses up to this JOIN do already filter out most 48 | -- of the ticket candidates so that the following JOINs do not exhaust ressources. 49 | LEFT JOIN LATERAL 50 | -- Since different projects can have different versions of encoding profiles 51 | -- assigned, no straight join is possible here. Selecting array of ids here 52 | -- to be used in following JOIN clause. 53 | ( 54 | SELECT array_agg(id) as ids FROM tbl_encoding_profile_version 55 | GROUP BY encoding_profile_id 56 | HAVING encoding_profile_id = dependee_profile.id 57 | ) dependee_profile_version ON true 58 | LEFT JOIN 59 | tbl_ticket dependee_ticket ON 60 | dependee_ticket.parent_id = t.parent_id AND 61 | dependee_ticket.encoding_profile_version_id = ANY (dependee_profile_version.ids) 62 | LEFT JOIN 63 | tbl_ticket_state dependee_ticket_state ON 64 | -- The case of empty dependee_ticket row is handled by the COALESCE in the WHERE clause. 65 | dependee_ticket_state.ticket_type = dependee_ticket.ticket_type AND 66 | dependee_ticket_state.ticket_state = dependee_ticket.ticket_state 67 | 68 | WHERE 69 | pj.read_only = false AND 70 | t.ticket_type IN ('recording','encoding','ingest') AND 71 | parent.ticket_state = 'staged' AND 72 | parent.failed = false AND 73 | t.failed = false AND 74 | (ep.depends_on IS NULL OR dependee_ticket.id IS NOT NULL) AND 75 | COALESCE(dependee_ticket.failed, false) = false AND 76 | COALESCE(pep.priority, 1) > 0 AND 77 | COALESCE(dependee_ticket_state.sort >= configured_trigger_state.sort, true) AND 78 | COALESCE(dependee_ticket_state.sort > depender_ticket_state.sort, true) 79 | ORDER BY 80 | calculated_priority DESC; 81 | 82 | COMMIT; 83 | -------------------------------------------------------------------------------- /src/Application/Migrations/99_testdata.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | --SET search_path TO test; 4 | 5 | INSERT INTO tbl_project (title, slug) VALUES ('test project', 'test'); 6 | 7 | INSERT INTO tbl_encoding_profile (name, slug, extension) VALUES ('format 1', 'fmt1', 'ext1'); 8 | INSERT INTO tbl_encoding_profile (name, slug, extension) VALUES ('format 2', 'fmt2', 'ext2'); 9 | 10 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (1, 'bla'); 11 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (1, 'bla'); 12 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (1, 'bla neu'); 13 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (2, 'blubb'); 14 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (2, 'blubb'); 15 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (2, 'blubb'); 16 | INSERT INTO tbl_encoding_profile_version (encoding_profile_id, xml_template) VALUES (2, 'blubb neu'); 17 | 18 | INSERT INTO tbl_project_encoding_profile (project_id, encoding_profile_version_id) VALUES (1, 3); 19 | INSERT INTO tbl_project_encoding_profile (project_id, encoding_profile_version_id) VALUES (1, 7); 20 | 21 | INSERT INTO tbl_project_ticket_state (project_id, ticket_type, ticket_state, service_executable) (SELECT 1 as project_id, ticket_type, ticket_state, service_executable FROM tbl_ticket_state WHERE ticket_type <> 'ingest'); 22 | 23 | INSERT INTO tbl_ticket (project_id, title, fahrplan_id, priority, ticket_type, ticket_state) VALUES (1, 'test ticket', 1, 1, 'meta', 'staging'); 24 | 25 | INSERT INTO tbl_ticket_property (ticket_id, name, value) VALUES (1, 'Fahrplan.Date', '2010-01-01'); 26 | INSERT INTO tbl_ticket_property (ticket_id, name, value) VALUES (1, 'Fahrplan.Start', '10:00'); 27 | INSERT INTO tbl_ticket_property (ticket_id, name, value) VALUES (1, 'Fahrplan.Duration', '01:00'); 28 | 29 | SELECT create_missing_encoding_tickets(1,null); 30 | 31 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-04-15_add_variable_dependent_ticket_state.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | ALTER TABLE tbl_project 6 | ADD COLUMN dependent_ticket_trigger_state enum_ticket_state 7 | NOT NULL DEFAULT 'released'; 8 | 9 | COMMIT; 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-05-14_cleanup_create_encoding_tickets.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP FUNCTION create_missing_encoding_tickets(bigint, bigint); 6 | 7 | CREATE OR REPLACE FUNCTION create_missing_encoding_tickets(param_project_id bigint) RETURNS integer AS $$ 8 | DECLARE 9 | row_count integer; 10 | BEGIN 11 | row_count := 0; 12 | 13 | INSERT INTO tbl_ticket (parent_id, project_id, fahrplan_id, priority, ticket_type, ticket_state, encoding_profile_version_id) 14 | (SELECT 15 | t1.id as parent_id, 16 | t1.project_id, 17 | t1.fahrplan_id, 18 | pep.priority, 19 | 'encoding' as ticket_type, 20 | ticket_state_initial(param_project_id, 'encoding') AS ticket_state, 21 | pep.encoding_profile_version_id 22 | FROM 23 | tbl_project_encoding_profile pep 24 | JOIN 25 | tbl_encoding_profile_version epv ON pep.encoding_profile_version_id = epv.id 26 | JOIN 27 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 28 | LEFT OUTER JOIN 29 | tbl_ticket t1 ON pep.project_id = t1.project_id 30 | LEFT JOIN 31 | tbl_ticket t2 ON t2.parent_id = t1.id AND t2.encoding_profile_version_id = epv.id 32 | WHERE 33 | t1.ticket_type = 'meta' AND 34 | t2.id IS NULL AND 35 | pep.project_id = param_project_id 36 | ORDER BY t1.id ASC, ep.id ASC); 37 | 38 | GET DIAGNOSTICS row_count = ROW_COUNT; 39 | return row_count; 40 | END; 41 | $$ LANGUAGE plpgsql; 42 | 43 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-05-14_cleanup_obsolete_create_missing_encoding_ticket.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP FUNCTION create_missing_encoding_ticket(bigint, bigint); 6 | 7 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-05-14_make_creating_encoding_tickets_configurable.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | ALTER TABLE tbl_project_encoding_profile 6 | ADD COLUMN auto_create boolean NOT NULL DEFAULT true; 7 | 8 | CREATE OR REPLACE FUNCTION create_missing_encoding_tickets(param_project_id bigint) RETURNS integer AS $$ 9 | DECLARE 10 | row_count integer; 11 | BEGIN 12 | row_count := 0; 13 | 14 | INSERT INTO tbl_ticket (parent_id, project_id, fahrplan_id, priority, ticket_type, ticket_state, encoding_profile_version_id) 15 | (SELECT 16 | t1.id as parent_id, 17 | t1.project_id, 18 | t1.fahrplan_id, 19 | pep.priority, 20 | 'encoding' as ticket_type, 21 | ticket_state_initial(param_project_id, 'encoding') AS ticket_state, 22 | pep.encoding_profile_version_id 23 | FROM 24 | tbl_project_encoding_profile pep 25 | JOIN 26 | tbl_encoding_profile_version epv ON pep.encoding_profile_version_id = epv.id 27 | JOIN 28 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 29 | LEFT OUTER JOIN 30 | tbl_ticket t1 ON pep.project_id = t1.project_id 31 | LEFT JOIN 32 | tbl_ticket t2 ON t2.parent_id = t1.id AND t2.encoding_profile_version_id = epv.id 33 | WHERE 34 | t1.ticket_type = 'meta' AND 35 | t2.id IS NULL AND 36 | pep.project_id = param_project_id AND 37 | pep.auto_create IS TRUE 38 | ORDER BY t1.id ASC, ep.id ASC); 39 | GET DIAGNOSTICS row_count = ROW_COUNT; 40 | return row_count; 41 | END; 42 | $$ LANGUAGE plpgsql; 43 | 44 | COMMIT; -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-06-01_import_auth.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE enum_import_auth_type AS ENUM ( 2 | 'basic', 3 | 'header'); 4 | 5 | ALTER TABLE tbl_import ADD COLUMN auth_type enum_import_auth_type; 6 | ALTER TABLE tbl_import ADD COLUMN auth_user character varying(256); 7 | ALTER TABLE tbl_import ADD COLUMN auth_password character varying(256); 8 | ALTER TABLE tbl_import ADD COLUMN auth_header text; 9 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-06-21_fix-trigger-for-progress-recalculation.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE OR REPLACE FUNCTION update_ticket_progress() 4 | RETURNS trigger AS 5 | $BODY$ 6 | BEGIN 7 | IF TG_OP = 'DELETE' THEN 8 | UPDATE tbl_ticket SET progress = ticket_progress(OLD.parent_id) WHERE id = OLD.parent_id; 9 | ELSE 10 | UPDATE tbl_ticket SET progress = ticket_progress(NEW.id) WHERE id = NEW.id; 11 | UPDATE tbl_ticket SET progress = ticket_progress(NEW.parent_id) WHERE id = NEW.parent_id; 12 | END IF; 13 | RETURN NULL; 14 | END 15 | $BODY$ 16 | LANGUAGE plpgsql VOLATILE; 17 | 18 | DROP TRIGGER IF EXISTS progress_trigger1 ON tbl_ticket; 19 | 20 | CREATE TRIGGER progress_trigger 21 | AFTER INSERT OR DELETE OR UPDATE OF ticket_state, parent_id 22 | ON tbl_ticket 23 | FOR EACH ROW EXECUTE PROCEDURE update_ticket_progress(); 24 | 25 | COMMIT; 26 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2018-09-17_check-missing-dependee-ticket.sql: -------------------------------------------------------------------------------- 1 | 2 | \i 18_function_ticket_dependency_missing.sql 3 | 4 | \i 22_view_serviceable_tickets.sql 5 | 6 | DO language plpgsql $$ 7 | BEGIN 8 | RAISE WARNING 'DO NOT FORGET: it is very likely that you have to give GRANT on the recreated view_serviceable_tickets to your tracker DB user after executing this script!'; 9 | END 10 | $$; 11 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2019-05-24_check_dependee_ticket_state.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | DROP VIEW IF EXISTS view_serviceable_tickets; 6 | 7 | CREATE OR REPLACE VIEW view_serviceable_tickets AS 8 | SELECT 9 | t.*, 10 | pstart.value::timestamp with time zone AS time_start, 11 | pstart.value::timestamp with time zone + pdur.value::time without time zone::interval AS time_end, 12 | proom.value AS room, 13 | parent.priority * t.priority * 14 | COALESCE( 15 | extract(EPOCH FROM CURRENT_TIMESTAMP) / 16 | extract(EPOCH FROM pstart.value::timestamp with time zone) 17 | , 1) * COALESCE(pep.priority, 1) AS calculated_priority 18 | FROM 19 | tbl_ticket t 20 | JOIN 21 | tbl_ticket parent ON parent.id = t.parent_id 22 | LEFT JOIN 23 | tbl_ticket_property pdur ON pdur.ticket_id = COALESCE(t.parent_id, t.id) AND pdur.name = 'Fahrplan.Duration'::ltree 24 | LEFT JOIN 25 | tbl_ticket_property pstart ON pstart.ticket_id = COALESCE(t.parent_id, t.id) AND pstart.name = 'Fahrplan.DateTime'::ltree 26 | LEFT JOIN 27 | tbl_ticket_property proom ON proom.ticket_id = COALESCE(t.parent_id,t.id) AND proom.name = 'Fahrplan.Room'::ltree 28 | LEFT JOIN 29 | tbl_project pj ON pj.id = t.project_id 30 | LEFT JOIN 31 | tbl_project_encoding_profile pep ON pep.project_id = pj.id AND pep.encoding_profile_version_id = t.encoding_profile_version_id 32 | LEFT JOIN 33 | tbl_ticket_state configured_trigger_state ON 34 | configured_trigger_state.ticket_type = 'encoding' AND 35 | configured_trigger_state.ticket_state = pj.dependee_ticket_trigger_state 36 | LEFT JOIN 37 | tbl_ticket_state depender_ticket_state ON 38 | depender_ticket_state.ticket_type = t.ticket_type AND 39 | depender_ticket_state.ticket_state = t.ticket_state 40 | LEFT JOIN 41 | tbl_encoding_profile_version epv ON epv.id = t.encoding_profile_version_id 42 | LEFT JOIN 43 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 44 | LEFT JOIN 45 | tbl_encoding_profile dependee_profile ON dependee_profile.id = ep.depends_on 46 | -- Using lateral join to split evaluation at this point. The tables and 47 | -- corresponding WHERE clauses up to this JOIN do already filter out most 48 | -- of the ticket candidates so that the following JOINs do not exhaust ressources. 49 | LEFT JOIN LATERAL 50 | -- Since different projects can have different versions of encoding profiles 51 | -- assigned, no straight join is possible here. Selecting array of ids here 52 | -- to be used in following JOIN clause. 53 | ( 54 | SELECT array_agg(id) as ids FROM tbl_encoding_profile_version 55 | GROUP BY encoding_profile_id 56 | HAVING encoding_profile_id = dependee_profile.id 57 | ) dependee_profile_version ON true 58 | LEFT JOIN 59 | tbl_ticket dependee_ticket ON 60 | dependee_ticket.parent_id = t.parent_id AND 61 | dependee_ticket.encoding_profile_version_id = ANY (dependee_profile_version.ids) 62 | LEFT JOIN 63 | tbl_ticket_state dependee_ticket_state ON 64 | -- The case of empty dependee_ticket row is handled by the COALESCE in the WHERE clause. 65 | dependee_ticket_state.ticket_type = dependee_ticket.ticket_type AND 66 | dependee_ticket_state.ticket_state = dependee_ticket.ticket_state 67 | 68 | WHERE 69 | pj.read_only = false AND 70 | t.ticket_type IN ('recording','encoding','ingest') AND 71 | parent.ticket_state = 'staged' AND 72 | parent.failed = false AND 73 | t.failed = false AND 74 | (ep.depends_on IS NULL OR dependee_ticket.id IS NOT NULL) AND 75 | COALESCE(dependee_ticket.failed, false) = false AND 76 | COALESCE(pep.priority, 1) > 0 AND 77 | COALESCE(dependee_ticket_state.sort >= configured_trigger_state.sort, true) AND 78 | COALESCE(dependee_ticket_state.sort > depender_ticket_state.sort, true) 79 | ORDER BY 80 | calculated_priority DESC; 81 | 82 | COMMIT; 83 | DO language plpgsql $$ 84 | BEGIN 85 | RAISE WARNING 'DO NOT FORGET: it is very likely that you have to give GRANT on the recreated view_serviceable_tickets to your tracker DB user after executing this script!'; 86 | END 87 | $$; 88 | 89 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2019-05-24_fix-dependee-ticket-missing.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_dependee_missing(param_depender_ticket_id bigint) 6 | RETURNS boolean AS 7 | $$ 8 | DECLARE 9 | result boolean; 10 | BEGIN 11 | 12 | SELECT 13 | epv.id IS NOT NULL -- check this is actually an encoding ticket 14 | AND ep.depends_on IS NOT NULL -- check that this is a ticket of a depending profile 15 | AND dependee.id IS NULL -- this is the error condition to return true on 16 | INTO result 17 | FROM 18 | tbl_ticket depender 19 | LEFT JOIN 20 | tbl_encoding_profile_version epv ON epv.id = depender.encoding_profile_version_id 21 | LEFT JOIN 22 | tbl_encoding_profile ep ON epv.encoding_profile_id = ep.id 23 | LEFT JOIN 24 | tbl_encoding_profile ep2 ON ep2.id = ep.depends_on 25 | LEFT JOIN 26 | tbl_encoding_profile_version epv2 ON epv2.encoding_profile_id = ep2.id 27 | INNER JOIN 28 | tbl_ticket dependee ON dependee.encoding_profile_version_id = epv2.id AND dependee.parent_id = depender.parent_id 29 | WHERE 30 | depender.id = param_depender_ticket_id; 31 | 32 | RETURN COALESCE(result, false); 33 | 34 | END 35 | $$ 36 | LANGUAGE plpgsql; 37 | 38 | COMMIT; 39 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2019-05-24_ticket-dependency-satisfied.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | CREATE OR REPLACE FUNCTION ticket_dependee_ticket_state_satisfied(param_depender_ticket_id bigint) 6 | RETURNS boolean AS 7 | $$ 8 | DECLARE 9 | satisfaction boolean; 10 | dependee_state enum_ticket_state; 11 | BEGIN 12 | SELECT ticket_dependee_ticket_state(param_depender_ticket_id) 13 | INTO dependee_state; 14 | 15 | SELECT 16 | depender_ticket_state.sort < dependee_ticket_state.sort AND 17 | dependee_ticket_state.sort >= configured_trigger_state.sort INTO satisfaction 18 | FROM 19 | tbl_ticket depender_ticket 20 | JOIN 21 | tbl_ticket_state depender_ticket_state ON 22 | depender_ticket_state.ticket_type = depender_ticket.ticket_type AND 23 | depender_ticket_state.ticket_state = depender_ticket.ticket_state 24 | JOIN 25 | tbl_project project ON depender_ticket.project_id = project.id 26 | JOIN 27 | tbl_ticket_state dependee_ticket_state ON 28 | dependee_ticket_state.ticket_type = depender_ticket.ticket_type AND 29 | dependee_ticket_state.ticket_state = dependee_state 30 | JOIN 31 | tbl_ticket_state configured_trigger_state ON 32 | configured_trigger_state.ticket_type = depender_ticket.ticket_type AND 33 | configured_trigger_state.ticket_state = project.dependee_ticket_trigger_state 34 | WHERE 35 | depender_ticket.id = param_depender_ticket_id AND 36 | -- if given ticket is no encoding ticket, function will return NULL 37 | depender_ticket.ticket_type = 'encoding'; 38 | 39 | RETURN satisfaction; 40 | END 41 | $$ 42 | LANGUAGE plpgsql; 43 | 44 | COMMIT; 45 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2019-09-28_ticket_reset.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | -- function also resets handle_id and failed flag 6 | CREATE OR REPLACE FUNCTION ticket_reset(param_ticket_id bigint, param_ticket_types enum_ticket_type[] DEFAULT array[]::enum_ticket_type[]) 7 | RETURNS TABLE (ticket_id bigint, from_state enum_ticket_state, to_state enum_ticket_state, follows_meta_ticket boolean, follows_encoding_ticket boolean) AS 8 | $$ 9 | DECLARE 10 | ticket tbl_ticket%rowtype; 11 | inital_state enum_ticket_state; 12 | BEGIN 13 | --first get the full given ticket 14 | SELECT * INTO ticket FROM tbl_ticket where id = param_ticket_id; 15 | 16 | IF NOT FOUND THEN 17 | RAISE WARNING 'ticket with id % does not exist', param_ticket_id; 18 | RETURN; 19 | END IF; 20 | 21 | --reset the given ticket to inital state, but only if it matches the ticket type filter 22 | IF array_length(param_ticket_types, 1) IS NULL OR (array_position(param_ticket_types, ticket.ticket_type) IS NOT NULL) THEN 23 | inital_state := ticket_state_initial(ticket.project_id, ticket.ticket_type); 24 | UPDATE tbl_ticket SET (failed, handle_id) = (FALSE, NULL) WHERE id = ticket.id; 25 | IF inital_state <> ticket.ticket_state THEN 26 | UPDATE tbl_ticket SET ticket_state=inital_state WHERE id = param_ticket_id; 27 | ticket_id := ticket.id; 28 | from_state := ticket.ticket_state; 29 | to_state := inital_state; 30 | follows_meta_ticket := FALSE; 31 | follows_encoding_ticket := FALSE; 32 | RETURN NEXT; 33 | END IF; 34 | END IF; 35 | 36 | --reset the children of the given ticket 37 | FOR ticket IN 38 | SELECT * FROM tbl_ticket WHERE parent_id = param_ticket_id AND (array_length(param_ticket_types, 1) IS NULL OR (array_position(param_ticket_types, ticket_type) IS NOT NULL)) 39 | LOOP 40 | inital_state := ticket_state_initial(ticket.project_id, ticket.ticket_type); 41 | UPDATE tbl_ticket SET (failed, handle_id) = (FALSE, NULL) WHERE id = ticket.id; 42 | IF inital_state <> ticket.ticket_state THEN 43 | UPDATE tbl_ticket SET ticket_state = inital_state WHERE id = ticket.id; 44 | ticket_id := ticket.id; 45 | from_state := ticket.ticket_state; 46 | to_state := inital_state; 47 | follows_meta_ticket := TRUE; 48 | follows_encoding_ticket := FALSE; 49 | RETURN NEXT; 50 | END IF; 51 | END LOOP; 52 | 53 | --reset all tickets with encoding profiles depending on given ticket's profile 54 | FOR ticket IN 55 | SELECT 56 | t2.* 57 | FROM 58 | tbl_ticket t2 59 | JOIN 60 | tbl_encoding_profile_version epv2 ON t2.encoding_profile_version_id = epv2.id 61 | JOIN ( 62 | SELECT 63 | encoding_profile_all_dependees(epv.encoding_profile_id) AS id, 64 | tt.parent_id 65 | FROM 66 | tbl_ticket tt 67 | JOIN 68 | tbl_encoding_profile_version epv ON tt.encoding_profile_version_id = epv.id 69 | WHERE 70 | tt.id = param_ticket_id AND 71 | (array_length(param_ticket_types, 1) IS NULL OR (array_position(param_ticket_types, tt.ticket_type) IS NOT NULL)) 72 | ) pd ON pd.id = epv2.encoding_profile_id AND t2.parent_id = pd.parent_id 73 | LOOP 74 | inital_state := ticket_state_initial(ticket.project_id, ticket.ticket_type); 75 | UPDATE tbl_ticket SET (failed, handle_id) = (FALSE, NULL) WHERE id = ticket.id; 76 | IF inital_state <> ticket.ticket_state THEN 77 | UPDATE tbl_ticket SET ticket_state = inital_state WHERE id = ticket.id; 78 | ticket_id := ticket.id; 79 | from_state := ticket.ticket_state; 80 | to_state := inital_state; 81 | follows_meta_ticket := FALSE; 82 | follows_encoding_ticket := TRUE; 83 | RETURN NEXT; 84 | END IF; 85 | END LOOP; 86 | 87 | END 88 | $$ 89 | LANGUAGE plpgsql; 90 | 91 | -- function returns a table with all encoding profiles, which depend 92 | -- on the given encoding profile 93 | 94 | -- second parameter is optional and is used in recursive to prevent endless loop 95 | 96 | CREATE OR REPLACE FUNCTION encoding_profile_all_dependees(param_profile_id bigint, param_known_ids bigint[] DEFAULT array[]::bigint[]) 97 | RETURNS TABLE (id bigint) AS 98 | $$ 99 | DECLARE 100 | p_id bigint; 101 | dependee_profile_ids bigint[]; 102 | BEGIN 103 | IF array_dims(param_known_ids) ISNULL THEN 104 | param_known_ids := array[param_profile_id]; 105 | END IF; 106 | 107 | FOR p_id IN 108 | SELECT p.id 109 | FROM tbl_encoding_profile p 110 | WHERE depends_on=param_profile_id AND NOT (p.id = ANY(param_known_ids)) 111 | LOOP 112 | dependee_profile_ids := p_id || dependee_profile_ids; 113 | dependee_profile_ids := ARRAY(SELECT e.id from encoding_profile_all_dependees(p_id, dependee_profile_ids || param_known_ids) e) || dependee_profile_ids; 114 | END LOOP; 115 | RETURN QUERY SELECT DISTINCT unnest(dependee_profile_ids); 116 | END 117 | $$ 118 | LANGUAGE plpgsql; 119 | 120 | COMMIT; 121 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2020-01-11_drop_filter_constraint.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | ALTER TABLE tbl_project_worker_group_filter DROP CONSTRAINT tbl_project_worker_group_filter_uq; 6 | 7 | COMMIT; 8 | 9 | -------------------------------------------------------------------------------- /src/Application/Migrations/__2020-04-15_ticket_initial_state.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | SET ROLE TO postgres; 4 | 5 | -- trigger function to set initial ticket state from project settings if it is not given 6 | 7 | CREATE OR REPLACE FUNCTION set_ticket_initial_state() 8 | RETURNS trigger AS 9 | $BODY$ 10 | DECLARE 11 | next_state record; 12 | BEGIN 13 | IF (NEW.ticket_state IS NULL) THEN 14 | NEW.ticket_state := ticket_state_initial(NEW.project_id, NEW.ticket_type); 15 | END IF; 16 | 17 | RETURN NEW; 18 | END 19 | $BODY$ 20 | LANGUAGE plpgsql VOLATILE; 21 | 22 | DROP TRIGGER IF EXISTS initial_state_trigger ON tbl_ticket; 23 | 24 | CREATE TRIGGER initial_state_trigger BEFORE INSERT OR UPDATE ON tbl_ticket FOR EACH ROW EXECUTE PROCEDURE set_ticket_initial_state(); 25 | 26 | COMMIT; 27 | 28 | -------------------------------------------------------------------------------- /src/Application/Model/Comment.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'foreign_key' => ['handle_id'], 10 | 'select' => 'name AS handle_name' 11 | ], 12 | 'Ticket' => [ 13 | 'foreign_key' => ['ticket_id'] 14 | ], 15 | 'ReferencedTicket' => [ 16 | 'class_name' => 'Ticket', 17 | 'foreign_key' => ['referenced_ticket_id'] 18 | ], 19 | 'User' => [ 20 | 'foreign_key' => ['handle_id'], 21 | 'select' => 'name AS user_name' 22 | ] 23 | ]; 24 | 25 | } 26 | 27 | ?> -------------------------------------------------------------------------------- /src/Application/Model/EncodingProfile.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class_name' => 'EncodingProfileVersion', 15 | 'foreign_key' => ['encoding_profile_id'], 16 | 'order_by' => 'tbl_encoding_profile_version.revision DESC', 17 | 'join' => false 18 | ] 19 | ]; 20 | 21 | public $hasMany = [ 22 | 'Properties' => [ 23 | 'class_name' => 'EncodingProfileProperties', 24 | 'foreign_key' => ['encoding_profile_id'] 25 | ], 26 | 'Versions' => [ 27 | 'class_name' => 'EncodingProfileVersion', 28 | 'foreign_key' => ['encoding_profile_id'] 29 | ] 30 | ]; 31 | 32 | public $acceptNestedEntriesFor = [ 33 | 'Properties' => true, 34 | 'Versions' => true // TODO: disable destroy 35 | ]; 36 | 37 | public static function with_version_count(Model_Resource $resource) { 38 | $resource->select( 39 | '*', 40 | EncodingProfileVersion::findAll() 41 | ->select('COUNT(*)') 42 | ->where('encoding_profile_id = ' . self::TABLE . '.id') 43 | ->selectAs('versions_count') 44 | ); 45 | } 46 | 47 | } 48 | 49 | ?> -------------------------------------------------------------------------------- /src/Application/Model/EncodingProfileProperties.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'foreign_key' => ['encoding_profile_id'] 14 | ] 15 | ]; 16 | 17 | public function defaultScope(Model_Resource $resource) { 18 | $resource 19 | ->orderBy('name') 20 | ->indexBy('name'); 21 | } 22 | 23 | } 24 | 25 | ?> 26 | -------------------------------------------------------------------------------- /src/Application/Model/EncodingProfileVersion.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'foreign_key' => ['encoding_profile_id']/*, 10 | 'select' => 'name AS encoding_profile_name'*/ 11 | ], 12 | ]; 13 | 14 | // Shortcuts from encoding profile 15 | public $hasMany = [ 16 | 'Properties' => [ 17 | 'class_name' => 'EncodingProfileProperties', 18 | 'foreign_key' => ['encoding_profile_id'] 19 | ], 20 | 'Ticket' => [ 21 | 'foreign_key' => ['encoding_profile_id'] 22 | ] 23 | ]; 24 | 25 | public $hasAndBelongsToMany = [ 26 | 'Project' => [] 27 | ]; 28 | 29 | public function getJobfile(array $properties) { 30 | libxml_use_internal_errors(true); 31 | 32 | $template = new DOMDocument(); 33 | 34 | // prepare template 35 | if (!$template->loadXML($this['xml_template'])) { 36 | throw EncodingProfileTemplateException::fromLibXMLErrors(); 37 | } 38 | 39 | // Process templates as XSL 40 | 41 | $content = new DOMDocument('1.0', 'UTF-8'); 42 | 43 | $parent = $content->createElement('properties'); 44 | 45 | foreach ($properties as $name => $value) { 46 | $element = $content->createElement('property'); 47 | $element->setAttribute('name', $name); 48 | $element->appendChild(new DOMText($value)); 49 | 50 | $parent->appendChild($element); 51 | } 52 | 53 | $content->appendChild($parent); 54 | 55 | $processor = self::_getXSLTProcessor(); 56 | 57 | if (!$processor->importStylesheet($template)) { 58 | throw EncodingProfileTemplateException::fromLibXMLErrors(); 59 | } 60 | 61 | // pretty print 62 | $output = $processor->transformToDoc($content); 63 | $output->insertBefore($output->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="/xsl/jobstyle.xsl"'), $output->firstChild); 64 | 65 | if (!$output or !$output instanceOf DOMDocument) { 66 | throw EncodingProfileTemplateException::fromLibXMLErrors(); 67 | } 68 | 69 | $output->formatOutput = true; 70 | $output->encoding = 'UTF-8'; 71 | 72 | return $output->saveXml(); 73 | } 74 | 75 | // TODO: move to Model validation 76 | public static function isTemplateValid($string) { 77 | libxml_use_internal_errors(true); 78 | 79 | try { 80 | $template = new DOMDocument(); 81 | 82 | // prepare template 83 | if (!$template->loadXML($string)) { 84 | throw EncodingProfileTemplateException::fromLibXMLErrors(); 85 | } 86 | 87 | $processor = self::_getXSLTProcessor(); 88 | 89 | if (!$processor->importStylesheet($template)) { 90 | throw EncodingProfileTemplateException::fromLibXMLErrors(); 91 | } 92 | } catch (EncodingProfileTemplateException $e) { 93 | return $e->getMessage(); 94 | } 95 | 96 | return true; 97 | } 98 | 99 | private static function _getXSLTProcessor() { 100 | $processor = new XSLTProcessor(); 101 | 102 | $processor->setSecurityPrefs( 103 | XSL_SECPREF_READ_FILE | 104 | XSL_SECPREF_WRITE_FILE | 105 | XSL_SECPREF_CREATE_DIRECTORY | 106 | XSL_SECPREF_READ_NETWORK | 107 | XSL_SECPREF_WRITE_NETWORK 108 | ); 109 | 110 | return $processor; 111 | } 112 | 113 | } 114 | 115 | class EncodingProfileTemplateException extends RuntimeException { 116 | 117 | public static function fromLibXMLErrors() { 118 | $errors = libxml_get_errors(); 119 | libxml_clear_errors(); 120 | 121 | if (empty($errors)) { 122 | return new static('Unknown error'); 123 | } 124 | 125 | $errors = array_map(function($error) { 126 | return sprintf( 127 | '%s, %d:%d', 128 | trim($error->message), 129 | $error->line, 130 | $error->column 131 | ); 132 | }, $errors); 133 | 134 | return new static(implode('; ', $errors)); 135 | } 136 | 137 | } 138 | 139 | ?> 140 | -------------------------------------------------------------------------------- /src/Application/Model/Handle.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Application/Model/Import.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'foreign_key' => ['user_id'], 10 | 'select' => 'name AS user_name' 11 | ] 12 | ]; 13 | 14 | public function without_xml(Model_Resource $resource) { 15 | $resource->select('id, user_id, url, version, rooms, created, finished'); 16 | } 17 | 18 | } 19 | 20 | ?> -------------------------------------------------------------------------------- /src/Application/Model/ProjectLanguages.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'foreign_key' => ['project_id'] 14 | ] 15 | ]; 16 | 17 | public function defaultScope(Model_Resource $resource) { 18 | $resource->orderBy('language'); 19 | } 20 | 21 | } 22 | 23 | ?> -------------------------------------------------------------------------------- /src/Application/Model/ProjectProperties.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'foreign_key' => ['project_id'] 14 | ] 15 | ]; 16 | 17 | public function defaultScope(Model_Resource $resource) { 18 | $resource 19 | ->orderBy('name') 20 | ->indexBy('name'); 21 | } 22 | 23 | } 24 | 25 | ?> -------------------------------------------------------------------------------- /src/Application/Model/ProjectTicketState.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'foreign_key' => ['project_id', 'ticket_type', 'ticket_state'] 12 | ] 13 | ]; 14 | 15 | public $belongsTo = [ 16 | 'State' => [ 17 | 'class_name' => 'TicketState', 18 | 'foreign_key' => ['ticket_type', 'ticket_state'], 19 | 'select' => 'sort' 20 | ] 21 | ]; 22 | 23 | // TODO: use Ticket::queryNextState / queryPreviousState? 24 | public static function getNextState($project, $type, $state) { 25 | $handle = Database::$Instance->query( 26 | 'SELECT * FROM ticket_state_next(?, ?, ?)', 27 | [$project, $type, $state] 28 | ); 29 | $row = $handle->fetch(); 30 | 31 | return ($row === false)? null : $row; 32 | } 33 | 34 | public static function getPreviousState($project, $type, $state) { 35 | $handle = Database::$Instance->query( 36 | 'SELECT * FROM ticket_state_previous(?, ?, ?)', 37 | [$project, $type, $state] 38 | ); 39 | 40 | $row = $handle->fetch(); 41 | return ($row === false)? null : $row; 42 | } 43 | 44 | public static function getCommenceState($project, $type) { 45 | $handle = Database::$Instance->query( 46 | 'SELECT ticket_state_commence(?, ?)', 47 | [$project, $type] 48 | ); 49 | 50 | return $handle->fetch()['ticket_state_commence']; 51 | } 52 | 53 | public static function createAll($project) { 54 | return (new Database_Query(self::TABLE)) 55 | ->insertFrom(TicketState::findAll()->select( 56 | // TODO: this needs better support 57 | Database::$Instance->quote($project) . 58 | ' AS project_id, ticket_type, ticket_state, service_executable' 59 | )) 60 | ->execute(); 61 | } 62 | } 63 | 64 | ?> -------------------------------------------------------------------------------- /src/Application/Model/ProjectWorkerGroupFilter.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Application/Model/TicketState.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'foreign_key' => ['ticket_type', 'ticket_state'], 12 | 'select' => '(ticket_state IS NOT NULL) AS project_enabled, service_executable AS project_service_executable' 13 | ] 14 | ]; 15 | 16 | public $hasMany = [ 17 | 'Ticket' => [ 18 | 'foreign_key' => ['ticket_type', 'ticket_state'] 19 | ] 20 | ]; 21 | 22 | protected static $_actions = [ 23 | 'cut' => 'cutting', 24 | 'check' => 'checking' 25 | ]; 26 | 27 | public static function getStateByAction($action) { 28 | if (!isset(self::$_actions[$action])) { 29 | return false; 30 | } 31 | 32 | return self::$_actions[$action]; 33 | } 34 | 35 | public function defaultScope(Model_Resource $resource) { 36 | $resource->orderBy('ticket_type, sort'); 37 | } 38 | } 39 | 40 | ?> -------------------------------------------------------------------------------- /src/Application/Model/User.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'foreign_key' => ['project_id'], 28 | 'self_key' => ['user_id'], 29 | 'via' => 'tbl_user_project_restrictions', 30 | 'select' => 'tbl_project.id' 31 | ] 32 | ]; 33 | 34 | public $acceptNestedEntriesFor = [ 35 | 'Project' => true 36 | ]; 37 | 38 | public static function isRestricted() { 39 | if (!static::isLoggedIn()) { 40 | return false; 41 | } 42 | 43 | return self::$Session->get()['restrict_project_access']; 44 | } 45 | 46 | } 47 | 48 | ?> 49 | -------------------------------------------------------------------------------- /src/Application/Model/Worker.php: -------------------------------------------------------------------------------- 1 | join(Ticket::TABLE, [ 14 | static::TABLE . '.id = handle_id', 15 | 'project_id' => $project 16 | ]); 17 | } 18 | 19 | } 20 | 21 | ?> -------------------------------------------------------------------------------- /src/Application/Model/WorkerGroup.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'foreign_key' => ['worker_group_id'], 15 | 'order_by' => 'last_seen DESC' 16 | ] 17 | ]; 18 | 19 | public $hasAndBelongsToMany = [ 20 | 'Project' => [ 21 | 'foreign_key' => ['project_id'], 22 | 'self_key' => ['worker_group_id'], 23 | 'via' => 'tbl_project_worker_group' 24 | ] 25 | ]; 26 | 27 | public static function worker_group_filter_count(Model_Resource $resource, Project $project) { 28 | $resource->andSelect( 29 | Database_Query::selectFrom( 30 | ProjectWorkerGroupFilter::TABLE, 31 | 'COUNT(*)' 32 | ) 33 | ->where([ 34 | 'worker_group_id = ' . self::TABLE . '.id', 35 | 'project_id' => $project['id'] 36 | ]) 37 | ->selectAs('filter_count') 38 | ); 39 | } 40 | 41 | public function getFilteredTickets(array $projects, Model_Resource $tickets) { 42 | $filtersByProject = $this->_findWorkerGroupFiltersByProject($projects); 43 | $filteredTickets = []; 44 | 45 | foreach ($tickets as $ticket) { 46 | $properties = $ticket->MergedProperties->toArray(); 47 | 48 | if (empty($filtersByProject[$ticket['project_id']])) { 49 | continue; 50 | } 51 | 52 | $filters = self::_evaluateFilters( 53 | $filtersByProject[$ticket['project_id']], 54 | $properties 55 | ); 56 | 57 | if (!empty($filters)) { 58 | $filteredTickets[$ticket['parent_id']] = $filters; 59 | } 60 | } 61 | 62 | return $filteredTickets; 63 | } 64 | 65 | public function filterTickets(array $projects, Model_Resource $tickets) { 66 | $filtersByProject = $this->_findWorkerGroupFiltersByProject($projects); 67 | 68 | $tickets->filter(function(array $entry) use ($tickets, $filtersByProject) { 69 | if (empty($filtersByProject[$entry['project_id']])) { 70 | return true; 71 | } 72 | 73 | // TODO: not so beautiful hack, can we get a Model object as argument? 74 | $ticket = new Ticket(); 75 | $ticket->init($entry); 76 | 77 | return (self::_evaluateFilters( 78 | $filtersByProject[$ticket['project_id']], 79 | $ticket->MergedProperties->toArray() 80 | ) === []); 81 | }); 82 | } 83 | 84 | private function _findWorkerGroupFiltersByProject(array $projects) { 85 | return (new Model_Resource_Grouped( 86 | ProjectWorkerGroupFilter::findAll() 87 | ->where([ 88 | 'worker_group_id' => $this['id'], 89 | 'project_id' => $projects 90 | ]), 91 | 'project_id' 92 | )) 93 | ->toArray(); 94 | } 95 | 96 | private static function _evaluateFilters(array $filters, array $properties) { 97 | $valid = false; 98 | $unmatchedFilters = []; 99 | 100 | foreach ($filters as $filter) { 101 | if ( 102 | isset($properties[$filter['property_key']]) and 103 | (string) $properties[$filter['property_key']]['value'] === $filter['property_value'] 104 | or 105 | !isset($properties[$filter['property_key']]) and $filter['property_value'] === '' 106 | ) { 107 | // TODO: for "NOT"/"NONE": set $filter here, break 108 | $valid = true; 109 | continue; 110 | } 111 | 112 | $unmatchedFilters[] = $filter; 113 | // TODO: for "AND" set $valid=false and break here 114 | // $valid = false; 115 | // break; 116 | } 117 | 118 | if ($valid) { 119 | return []; 120 | } 121 | 122 | return $unmatchedFilters; 123 | } 124 | 125 | } 126 | 127 | ?> -------------------------------------------------------------------------------- /src/Application/Styles/components/_encoding-profiles.scss: -------------------------------------------------------------------------------- 1 | div.table table tr.encoding-profile-version td { 2 | padding: 0; 3 | } 4 | 5 | table tr td.encoding-profile-select { 6 | white-space: nowrap; 7 | } 8 | 9 | @media screen and (max-width: 505px) { 10 | div.table.encoding-profiles table .slug, 11 | div.table.encoding-profiles table .ext { display: none; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Application/Styles/images/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/background.gif -------------------------------------------------------------------------------- /src/Application/Styles/images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/main.png -------------------------------------------------------------------------------- /src/Application/Styles/images/main_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/main_2x.png -------------------------------------------------------------------------------- /src/Application/Styles/images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/progress.gif -------------------------------------------------------------------------------- /src/Application/Styles/images/wait.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/wait.gif -------------------------------------------------------------------------------- /src/Application/Styles/images/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Application/Styles/images/warning.png -------------------------------------------------------------------------------- /src/Application/Styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "legacy"; 2 | 3 | @import "components/_encoding-profiles"; 4 | @import "components/_tickets"; 5 | -------------------------------------------------------------------------------- /src/Application/View/403.html.php: -------------------------------------------------------------------------------- 1 |

Access denied

-------------------------------------------------------------------------------- /src/Application/View/404.html.php: -------------------------------------------------------------------------------- 1 |

Page not found

-------------------------------------------------------------------------------- /src/Application/View/500.html.php: -------------------------------------------------------------------------------- 1 | layout(false); ?> 2 | 3 | 4 | 5 | C3 Ticket Tracker 6 | 7 | 8 | 9 | 10 | 11 |
12 |
    13 | 14 |
15 |
16 | 19 |
20 |

Server error

21 |

22 |

getMessage(); ?>

23 |
24 | 25 | -------------------------------------------------------------------------------- /src/Application/View/encoding/profiles/_defaultProfile.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Application/View/encoding/profiles/edit.html.php: -------------------------------------------------------------------------------- 1 | title((isset($profile))? ('Edit encoding profile ' . $profile['name'] . ' | ') : 'Create encoding profile | '); ?> 2 | 3 | 4 |
5 |

Edit encoding profile

6 | 7 |
    8 |
  • 9 | 10 |
  • linkTo('encodingprofiles', 'view', $profile, 'versions', 'Show all versions'); ?>
  • 11 | 12 | 13 |
  • linkTo('encodingprofiles', 'edit', $profile, 'edit', 'Edit encoding profile…'); ?>
  • 14 | 15 | 16 |
  • linkTo('encodingprofiles', 'delete', $profile, 'delete', 'Delete encoding profile'); ?>
  • 17 | 18 |
  • 19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |

Create new encoding profile

27 | 28 |
    29 |
  • input('name', 'Name', $profile['name'], array('class' => 'wide')); ?>
  • 30 |
  • input('slug', 'Slug', $profile['slug'], array('class' => 'narrow')); ?>
  • 31 | 32 |
  • select('depends_on', 'Depends on', array('' => '') + $profiles->toArray(), $profile['depends_on']); ?>
  • 33 |
34 |
35 | 36 |
37 | Encoding 38 |
    39 | getValue('save'))? false : Form::REQUEST_METHOD_FORM; ?> 41 |
  • select('version', 'Version', $versions->indexBy('id','encodingProfileVersionTitle')->toArray(), $version['id'], array('data-submit-on-change' => true)); ?>
  • 42 |
  • LatestVersion['id']) { 43 | echo $f->hidden('create_version', 1, array('readonly' => true)); 44 | echo $f->checkbox('create_version', 'Create new version when editing the template', true, array('disabled' => true), false); 45 | echo 'Editing an old version always creates a new version.'; 46 | } else { 47 | echo $f->checkbox('create_version', 'Create new version when editing the template', false, [], $useRequestValue); 48 | } 49 | ?>
  • 50 | 51 |
  • input('description', 'Description', $version['description'], array('class' => 'wide'), $useRequestValue); ?>
  • 52 |
  • textarea('xml_template', 'XML encoding template', $version['xml_template'], array('class' => 'extra-wide', 'data-has-editor' => true), $useRequestValue); ?>
  • 53 | 54 |
  • input('versions[0][description]', 'Description', '', array('class' => 'wide')); ?>
  • 55 |
  • textarea('versions[0][xml_template]', 'XML encoding template', file_get_contents(APPLICATION . 'View/encoding/profiles/_defaultProfile.xml'), array('class' => 'extra-wide', 'data-has-editor' => true)); ?>
  • 56 | 57 |
58 |
59 | 60 |
61 | Properties 62 | render('shared/form/properties', [ 63 | 'f' => $f, 64 | 'properties' => [ 65 | 'for' => (!empty($profile))? $profile->Properties : null, 66 | 'field' => 'properties', 67 | 'description' => 'property', 68 | 'key' => 'name', 69 | 'value' => 'value' 70 | ] 71 | ]); ?> 72 |
73 | 74 |
75 |
    76 |
  • submit((isset($profile))? 'Save changes' : 'Create new encoding profile', array('name' => 'save')) . ' or '; 78 | echo $this->linkTo('encodingprofiles', 'index', (isset($profile))? 'discard changes' : 'discard encoding profile', array('class' => 'reset')); 79 | ?>
  • 80 |
81 |
82 | 83 | 84 | render('shared/js/_editor'); ?> -------------------------------------------------------------------------------- /src/Application/View/encoding/profiles/index.html.php: -------------------------------------------------------------------------------- 1 | title('Encoding profiles | ') ?> 2 | 3 | 4 |
    5 |
  • 6 |
  • linkTo('encodingprofiles', 'create', 'create', 'Create new encoding profile…'); ?>
  • 7 |
  • 8 |
9 | 10 | 11 |
12 |

Encoding profiles

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 39 | 40 | 41 | 42 |
NameSlug  
43 | 44 |

45 | No existing encoding profiles found. 46 | linkTo('encodingprofiles', 'create', 'Create new encoding profile') . '.'; 48 | } ?> 49 |

50 | 51 |
-------------------------------------------------------------------------------- /src/Application/View/encoding/profiles/view.html.php: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | 4 |
  • linkTo('encodingprofiles', 'view', $profile, 'versions', 'Show all versions'); ?>
  • 5 | 6 | 7 |
  • linkTo('encodingprofiles', 'edit', $profile, 'edit', 'Edit encoding profile…'); ?>
  • 8 | 9 | 10 |
  • linkTo('encodingprofiles', 'delete', $profile, 'delete', 'Delete encoding profile'); ?>
  • 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 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 |
CreatedDescription
radio('version_a', null, $version['id']) . $f->radio('version_b', null, $version['id']); ?> 36 | rformat('d.m.Y H:i'); ?>
49 | 50 |
56 | 57 |

58 | submit('Compare versions'); ?> 59 |

60 | 61 |
62 | 63 | render('shared/js/_editor'); ?> 64 | -------------------------------------------------------------------------------- /src/Application/View/import/_header.html.php: -------------------------------------------------------------------------------- 1 | title($title . ' | '); ?> 2 | 3 |
4 |

5 | 6 |
    7 |
  • 8 | 9 | 10 |
  • linkTo('tickets', 'create', $project, 'create', 'Create new ticket…'); ?>
  • 11 | 12 | 13 |
  • linkTo('import', 'index', $project, 'import', 'Import tickets…'); ?>
  • 14 | 15 |
  • 16 |
17 |
-------------------------------------------------------------------------------- /src/Application/View/import/index.html.php: -------------------------------------------------------------------------------- 1 | title('Import | '); ?> 2 | 3 | render('import/_header', ['title' => 'Import tickets']); ?> 4 | 5 | 6 | 7 |
8 | Continue import 9 |
    10 |
  • 11 |
  • 12 | 13 |

    14 | You have an unfinished import created . 15 |

    16 |
  • 17 |
  • input('url','XML URL', $unfinishedImport['url'], ['readonly' => true, 'class' => 'wide']); ?>
  • 18 |
  • submit('Cancel', ['name' => 'cancel']) . $f->submit('Continue'); ?>
  • 19 |
20 |
21 | 22 | 23 | 24 | 'ticket-import']); ?> 25 |
26 | New import 27 |
    28 |
  • 29 | input('url','XML URL', '', ['class' => 'wide', 'disabled' => $project['read_only']]); ?> 30 |
  • 31 |
  • 32 | select( 33 | 'auth_type', 34 | 'Authentication', 35 | [ 36 | '' => 'No Authentication', 37 | 'basic' => 'Basic HTTP Authentication', 38 | 'header' => 'Authentication Header' 39 | ], 40 | '', 41 | ['data-import-auth-type-value'] 42 | ); ?> 43 |
  • 44 |
  • input('auth_user', 'Username', '', ['data-import-auth-type' => 'basic']); ?>
  • 45 |
  • password('auth_password', 'Password', ['data-import-auth-type' => 'basic']); ?>
  • 46 |
  • input('auth_header', 'Authentication Header', '', ['class' => 'wide', 'data-import-auth-type' => 'header']); ?>
  • 47 |
  • submit('Create new import', ['disabled' => $project['read_only']]); ?>
  • 48 |
49 |
50 | 51 | 52 | getRows() > 0): ?> 53 |

Previous imports

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | format('Y-m-d H:i:s')); 69 | $url = parse_url($import['url']); ?> 70 | 71 | 76 | 81 | 82 | 83 | 84 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 |
URLVersionImportedUser
72 | 73 | 74 | 75 | linkTo( 77 | 'import', 'download', $import, $project, ['.xml'], 78 | (!empty($import['version']))? $import['version'] : ('' . $date . ''), 79 | (!empty($import['version']))? $import['version'] : $date 80 | ); ?>
101 | 102 | -------------------------------------------------------------------------------- /src/Application/View/import/repeat.html.php: -------------------------------------------------------------------------------- 1 | render('import/_header', ['title' => 'Repeat import']); ?> 2 | 3 | 4 |
5 |
    6 |
  • 7 | radio('source', 'Fetch new version from source', 'url', true); ?> 8 | Access to load an updated version of the Fahrplan. 9 |
  • 10 |
  • 11 | radio('source', 'Use previously downloaded Fahrplan XML', 'xml'); ?> 12 | Version 13 |
  • 14 |
  • 15 | checkbox('apply_rooms', 'Apply previous room selection', true); ?> 16 | 17 | $enabled) { 18 | if (!$enabled) { 19 | $rooms[] = '' . h($room) . ''; 20 | } else { 21 | $rooms[] = h($room); 22 | } 23 | } echo implode(', ', $rooms); ?> 24 | 25 |
  • 26 |
  • submit('Repeat import') . ' or ' . 27 | $this->linkTo('import', 'index', $project, 'return to index', ['class' => '']); ?>
  • 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/Application/View/import/rooms.html.php: -------------------------------------------------------------------------------- 1 | render('import/_header', ['title' => 'Import: select rooms']); ?> 2 | 3 | 4 |
5 |
    6 | 7 |
  • checkbox('rooms[' . $room . ']', $room, (isset($selectedRooms[$room]))? $selectedRooms[$room] : true, [], false); ?>
  • 8 | 9 |
  • submit('Cancel', ['name' => 'cancel']) . $f->submit('Continue'); ?>
  • 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/Application/View/projects/delete.html.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Delete project linkTo('projects', 'settings', $project, $project['title']); ?>?

4 |
    5 |
  • 6 | 7 |

    8 | Are you sure you want to delete this project? 9 | 10 | All related tickets, their properties, comments and log entries will be permanently erased.
    11 | You can't undo this action. 12 |
    13 |

    14 |
  • 15 |
  • submit('Delete project'); ?> or linkTo('projects', 'settings', $project, 'return without doing anything'); ?>
  • 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/Application/View/projects/edit.html.php: -------------------------------------------------------------------------------- 1 | title((!empty($project))? ('Edit project | ') : 'Create new project | '); 2 | echo $f = $form(array('id' => 'project-edit')); ?> 3 |
4 | 5 |

Edit project linkTo('projects', 'settings', array('project_slug' => $project['slug']), h($project['title'])); ?>

6 | 7 |

Create new project

8 | 9 |
    10 |
  • input('title', 'Title', (!empty($project))? $project['title'] : '', ['class' => 'wide']); ?>
  • 11 |
  • input('slug', 'Slug', (!empty($project))? $project['slug'] : '', ['class' => 'narrow']); ?>
  • 12 |
  • checkbox('read_only', 'Archive project, disable write access.', (!empty($project))? $project['read_only'] : false); ?>
  • 13 |
14 |
15 |
16 |
    17 |
  • 18 | submit('Save changes') . ' or '; 20 | echo $this->linkTo('projects', 'settings', $project, 'discard changes', ['class' => 'reset']); 21 | } else { 22 | echo $f->submit('Create new project') . ' or '; 23 | echo $this->linkTo('projects', 'index', 'discard project', ['class' => 'reset']); 24 | } ?> 25 |
  • 26 |
27 |
28 | -------------------------------------------------------------------------------- /src/Application/View/projects/index.html.php: -------------------------------------------------------------------------------- 1 | title('Projects | '); ?> 2 | 3 | 4 |
5 |

Projects

6 | 7 | 8 |
    9 |
  • 10 |
  • linkTo('projects', 'create', 'create', 'Create new project'); ?>
  • 11 |
  • 12 |
13 | 14 |
15 | 16 | 17 |
    18 | 19 | 20 |
  • 21 | Your project access is restricted. More projects may exist. 22 |
  • 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | Archived projects 31 |
    32 | 33 |
  • 34 | linkTo('tickets', 'feed', ['project_slug' => $project['slug']], h($project['title']) . '', $project['title'], ['class' => 'link']); ?> 35 | 36 |
      37 |
    • linkTo('tickets', 'index', ['project_slug' => $project['slug']], 'tickets'); ?>
    • 38 | 39 | 40 |
    • linkTo('projects', 'settings', ['project_slug' => $project['slug']], 'project settings'); ?>
    • 41 | 42 |
    43 |
  • 44 | 45 | 46 |
47 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /src/Application/View/projects/settings.html.php: -------------------------------------------------------------------------------- 1 | title('Settings | '); ?> 2 | render('projects/settings/_header'); ?> 3 | 4 |

Statistics

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | 32 | 39 | 40 | 41 | 42 | 49 | 56 | 57 | 58 | 59 | 66 | 67 | 68 | 69 |
CountRecording Duration
All tickets
Staging tickets 26 | 31 | 33 | 38 |
Staged tickets 43 | 48 | 50 | 55 |
Closed tickets 60 | 65 |
70 | 71 |

Properties

72 | render('shared/properties'); ?> 73 | 74 |

Other

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 87 | 88 | 89 |
Project
Id 85 | 86 |
-------------------------------------------------------------------------------- /src/Application/View/projects/settings/_header.html.php: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |
6 | archived (read only) 7 |
8 | 9 | 10 |
    11 |
  • 12 | 13 | 14 |
  • linkTo('projects', 'duplicate', $project, 'duplicate', 'Duplicate project'); ?>
  • 15 | 16 | 17 | 18 |
  • linkTo('projects', 'edit', $project, 'edit name', 'Edit project name'); ?>
  • 19 | 20 | 21 | 22 |
  • linkTo('projects', 'delete', $project, 'delete', 'Delete project…'); ?>
  • 23 | 24 |
  • 25 |
26 |
27 | 28 |
    29 | >linkTo('projects', 'settings', $project, 'Info &
    Statistics', 'Info & Statistics', ['class' => 'info']); ?> 30 | 31 | 32 | >linkTo('projects', 'properties', $project, 'Properties &
    Languages', 'Properties & Languages', ['class' => 'properties']); ?> 33 | 34 | 35 | 36 | >linkTo('projects', 'profiles', $project, 'Encoding profiles', ['class' => 'encoding']); ?> 37 | 38 | 39 | 40 | >linkTo('projects', 'states', $project, 'States', ['class' => 'states']); ?> 41 | 42 | 43 | 44 | >linkTo('projects', 'worker', $project, 'Worker groups', ['class' => 'worker']); ?> 45 | 46 | 47 | >linkTo('projects', 'webhooks', $project, 'Webhooks', ['class' => 'hooks']); ?>*/ ?> 48 |
-------------------------------------------------------------------------------- /src/Application/View/projects/settings/filter/edit.html.php: -------------------------------------------------------------------------------- 1 | getRows() > 0); 2 | $this->title(($hasFilter)? 'Edit Filter | ' : 'Add Filter | '); ?> 3 | render('projects/settings/_header'); ?> 4 | 5 | $project['read_only']]); ?> 6 |
7 |

filter for assignment

8 | 9 |
    10 |
  • 11 | select('evaluation', 'Evaluation', [ 12 | 'not' => 'None of the properties has to match', 13 | 'or' => 'At least one of the properties has to match', 14 | 'and' => 'All of the properties have to match' 15 | ], 'or', ['disabled' => true]); ?> 16 |
  • 17 |
18 |
19 | 20 |
21 | Property Conditions 22 | render('shared/form/properties', [ 23 | 'f' => $f, 24 | 'properties' => [ 25 | 'for' => $workerGroupFilter, 26 | 'field' => 'WorkerGroupFilter', 27 | 'description' => 'property condition', 28 | 'key' => 'property_key', 29 | 'value' => 'property_value', 30 | 31 | 'hidden' => [ 32 | 'id', 33 | 'worker_group_id' => $workerGroup['id'] 34 | ] 35 | ] 36 | ]); ?> 37 |
38 | 39 |
40 |
    41 |
  • 42 | submit(($hasFilter)? 'Save changes' : 'Add filter'); ?> or 43 | linkTo('projects', 'worker', $project, ($hasFilter)? 'discard changes' : 'discard filter', ['class' => 'reset']); ?> 44 |
  • 45 |
46 |
47 | -------------------------------------------------------------------------------- /src/Application/View/projects/settings/profiles.html.php: -------------------------------------------------------------------------------- 1 | title('Encoding profiles | '); ?> 2 | render('projects/settings/_header'); ?> 3 | 4 | $project['read_only']]) ?> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | $version): ?> 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | selectForResource? ?> 28 | 39 | 49 | 60 | 61 | 67 | 68 | 69 | 70 | getRows() > 0): ?> 71 | 72 | 73 | 94 | 104 | 114 | 115 | 116 | 117 | 118 |
NameVersionPriorityAuto create
21 | Warning: Encoding profile dependee missing, add a version for . 22 |
EncodingProfile['name']; ?> 29 | select( 30 | 'versions[' . $index . '][1]', 31 | null, 32 | $version->EncodingProfile->Versions->indexBy('id', 'encodingProfileVersionTitle')->orderBy('revision')->toArray(), 33 | $version['id'], 34 | ['data-encoding-profile-version-id' => $version['id'], 'data-encoding-profile-index' => $index], 35 | false 36 | ) . // TODO: show "x newer versions", maybe JS? 37 | $f->hidden('versions[' . $index . '][0]', $version['id']); ?> 38 | 40 | select( 41 | 'priority[' . $version['id'] . ']', 42 | null, 43 | ['0' => 'disabled'] + Ticket::$priorities, 44 | $version['priority'], 45 | ['data-submit-on-change' => true], 46 | false 47 | ); ?> 48 | checkbox( 51 | 'auto_create[' . $version['id'] . '][1]', 52 | null, 53 | $version['auto_create'], 54 | ['data-submit-on-change' => true], 55 | false 56 | ). 57 | $f->hidden('auto_create[' . $version['id'] . '][0]', $version['auto_create']); 58 | ?> 59 |
74 | register('add[encoding_profile_version_id]'); 76 | } ?> 77 | 93 | 95 | select( 96 | 'add[priority]', 97 | null, 98 | ['0' => 'disabled'] + Ticket::$priorities, 99 | 1, 100 | [], 101 | false 102 | ); ?> 103 | checkbox( 106 | 'add[auto_create]', 107 | null, 108 | true, 109 | [], 110 | false 111 | ); 112 | ?> 113 |
119 | 120 | -------------------------------------------------------------------------------- /src/Application/View/projects/settings/properties.html.php: -------------------------------------------------------------------------------- 1 | title('Properties & Languages | '); ?> 2 | render('projects/settings/_header'); ?> 3 | 4 | $project['read_only']]); ?> 5 |
6 |
7 | submit('Save changes'); ?> 8 |
9 |
10 |
11 | Properties 12 | render('shared/form/properties', [ 13 | 'f' => $f, 14 | 'properties' => [ 15 | 'for' => (!empty($project))? $project->Properties : null, 16 | 'field' => 'properties', 17 | 'description' => 'property', 18 | 'key' => 'name', 19 | 'value' => 'value' 20 | ] 21 | ]); ?> 22 |
23 | 24 |
25 | Languages 26 | render('shared/form/properties', [ 27 | 'f' => $f, 28 | 'properties' => [ 29 | 'for' => (!empty($project))? $project->Languages : null, 30 | 'field' => 'languages', 31 | 'description' => 'language', 32 | 'key' => 'language', 33 | 'value' => 'description' 34 | ] 35 | ]); ?> 36 |
37 | 38 |
39 |
    40 |
  • 41 | submit('Save changes') . ' or '; 42 | echo $this->linkTo('projects', 'properties', $project, 'discard changes', ['class' => 'reset']); ?> 43 |
  • 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/Application/View/projects/settings/states.html.php: -------------------------------------------------------------------------------- 1 | title('States | '); ?> 2 | render('projects/settings/_header'); ?> 3 | 4 | 6 | 7 | $project['read_only']]); ?> 8 |
9 |
10 | submit('Save changes'); ?> 11 |
12 |
13 |
14 |
15 | select('dependee_ticket_trigger_state', 16 | 'Minimum required state for encoding tickets to activate dependent tickets', 17 | $encodingStates, (!empty($project))? $project['dependee_ticket_trigger_state'] : null) ?> 18 |
19 |
20 | $state): ?> 21 | 1): 25 | $typeRows = 0; ?> 26 | 27 | 28 | 29 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 62 | 65 | 66 | register('States[' . $index . '][_destroy]'); ?> 67 | 68 | 69 |
TypeStateService
checkbox( 51 | 'States[' . $index . '][ticket_state]', 52 | $state['ticket_state'], 53 | $state['project_enabled'], 54 | ['value' => $state['ticket_state']] + 55 | (($state['project_enabled'])? 56 | ['data-association-destroy' => 'States[' . $index . '][_destroy]'] : 57 | []), 58 | false 59 | ) . 60 | $f->hidden('States[' . $index . '][ticket_type]', $state['ticket_type']); 61 | ?>checkbox('States[' . $index . '][service_executable]', null, $state['project_service_executable'], [], false); 64 | } ?>
70 |
71 | 72 | -------------------------------------------------------------------------------- /src/Application/View/projects/settings/worker.html.php: -------------------------------------------------------------------------------- 1 | title('Worker groups | '); ?> 2 | render('projects/settings/_header'); ?> 3 | 4 | $project['read_only']]); ?> 5 |
6 |
7 | submit('Save assignment'); ?> 8 |
9 |
10 | 11 |
    12 | $group): ?> 13 |
  • 14 | checkbox( 15 | 'WorkerGroup[' . $index . '][worker_group_id]', 16 | h($group['title']) . 17 | (($group['paused'])? ' (paused)' : ''), 18 | isset($workerGroupAssignment[$group['id']]), 19 | ['value' => $group['id']] + 20 | ((isset($workerGroupAssignment[$group['id']]))? 21 | ['data-association-destroy' => 'WorkerGroup[' . $index . '][_destroy]'] : 22 | []), 23 | false 24 | ); ?> 25 | 26 | linkTo( 27 | 'projects', 'edit_filter', $group, $project, 28 | ($group['filter_count'] > 0)? 29 | (($group['filter_count'] === 1)? 30 | 'Edit 1 filter' : 'Edit ' . $group['filter_count'] . ' filters') 31 | : 'Add filter' 32 | ); ?> 33 | 34 | linkTo('workers', 'queue', $group, 'Queue'); 36 | } ?> 37 |
  • 38 | register('WorkerGroup[' . $index . '][_destroy]'); 39 | endforeach; ?> 40 |
41 | -------------------------------------------------------------------------------- /src/Application/View/shared/form/properties.html.php: -------------------------------------------------------------------------------- 1 |
    2 | $property): 4 | if (!empty($property['virtual'])) { 5 | continue; 6 | } ?> 7 |
  • 8 | hidden($properties['field'] . '[' . $index . '][' . $properties['key'] . ']', $property[$properties['key']]); 9 | 10 | $showTextarea = (strpos($property[$properties['value']], "\n") !== false); 11 | 12 | if ($showTextarea) { 13 | echo $f->textarea( 14 | $properties['field'] . '[' . $index . '][' . $properties['value'] . ']', 15 | $property[$properties['key']], 16 | $property[$properties['value']], 17 | [ 18 | 'class' => 'wide', 19 | 'data-property-index' => $index, 20 | 'data-property-destroy' => $properties['field'] . '[' . $index . '][_destroy]' 21 | ] + 22 | ((isset($properties['placeholder']))? ['placeholder' => $properties['placeholder']] : []) 23 | ); 24 | } else { 25 | echo $f->input( 26 | $properties['field'] . '[' . $index . '][' . $properties['value'] . ']', 27 | $property[$properties['key']], 28 | $property[$properties['value']], 29 | [ 30 | 'class' => 'wide', 31 | 'data-property-index' => $index, 32 | 'data-property-destroy' => $properties['field'] . '[' . $index . '][_destroy]' 33 | ] + 34 | ((isset($properties['placeholder']))? ['placeholder' => $properties['placeholder']] : []) 35 | ); 36 | } 37 | 38 | $f->register($properties['field'] . '[' . $index . '][_destroy]'); 39 | 40 | if (isset($properties['hidden'])) { 41 | foreach ($properties['hidden'] as $key => $value) { 42 | if (is_int($key)) { 43 | echo $f->hidden($properties['field'] . '[' . $index . '][' . $value . ']', $property[$value]); 44 | } else { 45 | echo $f->hidden($properties['field'] . '[' . $index . '][' . $key . ']', $value); 46 | } 47 | } 48 | } ?> 49 |
  • 50 | register($properties['field'] . '[][' . $properties['key'] . ']'); 54 | $f->register($properties['field'] . '[][' . $properties['value'] . ']'); 55 | 56 | if (isset($properties['hidden'])) { 57 | foreach ($properties['hidden'] as $key => $value) { 58 | if (is_int($key)) { 59 | continue; 60 | } 61 | 62 | echo $f->hidden($properties['field'] . '[][' . $key . ']', $value, ['data-properties-hidden' => true]); 63 | } 64 | } ?> 65 |
-------------------------------------------------------------------------------- /src/Application/View/shared/js/_editor.html.php: -------------------------------------------------------------------------------- 1 | contentFor('stylesheets', ''); 3 | $this->contentFor('scripts', ''); 4 | ?> -------------------------------------------------------------------------------- /src/Application/View/shared/properties.html.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 |
1)? implode('.', array_slice($parts, 1)) : $property['name']); 22 | echo (!empty($property['virtual']))? ('' . $key . '') : $key; 23 | ?> 25 | 80 and ($pos = mb_strpos($property['value'], ' ', 80)) !== false) { 26 | echo nl2br(h(mb_substr($property['value'], 0, $pos + 1))) . '' . nl2br(h(mb_substr($property['value'], $pos + 1))) . ''; 27 | } else { 28 | echo nl2br(h($property['value'])); 29 | } ?> 30 |
-------------------------------------------------------------------------------- /src/Application/View/tickets/duplicate.html.php: -------------------------------------------------------------------------------- 1 | title('Duplicate ticket ' . $ticket['title'] . ' | '); ?> 2 | 3 | render('tickets/view/_header', [ 4 | 'titlePrefix' => 'Duplicate ', 5 | 'showDetails' => false, 6 | 'currentAction' => 'duplicate' 7 | ]); ?> 8 | 9 | 'ticket-edit')); ?> 10 |
11 |
    12 |
  • 13 | 14 |

    15 | When duplicating a ticket all properties will be copied,
    16 | comments and log entries are ignored. 17 |

    18 |
  • 19 |
  • 20 | input('fahrplan_id', 'New Fahrplan ID', null, ['class' => 'narrow']); ?> 21 | You have to set a new Fahrplan ID because it's unique inside a project. 22 |
  • 23 |
  • 24 | select('ticket_state', 'State', $states->indexBy('ticket_state', 'ticket_state')->toArray(), $ticket['ticket_state']); ?> 25 |
  • 26 |
27 |
28 |
29 | Children 30 |
    31 |
  • 32 | select('duplicate_recording_ticket', 'Recording ticket', [ 33 | '' => 'Create new recording ticket', 34 | '1' => 'Duplicate recording ticket' 35 | ]); ?> 36 |
  • 37 |
  • 38 | 39 |

    New encoding tickets will be created for the duplicated ticket.

    40 |
  • 41 |
42 |
43 |
44 |
    45 |
  • 46 | submit('Create duplicate') . ' or ' . 47 | $this->linkTo('tickets', 'view', $ticket, $project, 'discard changes', ['class' => 'reset']); ?> 48 |
  • 49 |
50 |
51 | -------------------------------------------------------------------------------- /src/Application/View/tickets/edit.html.php: -------------------------------------------------------------------------------- 1 | title('Create new ticket | '); 3 | } else { 4 | $this->title('Edit ticket ' . $ticket['title'] . ' | '); 5 | } ?> 6 | 7 | render('tickets/view/_header', [ 9 | 'titlePrefix' => 'Edit ', 10 | 'showDetails' => false, 11 | 'currentAction' => 'edit' 12 | ]); 13 | else: ?> 14 |
15 |

Create new ticket

16 | 17 |
    18 |
  • 19 |
  • linkTo('tickets', 'create', $project, 'create', 'Create new ticket…'); ?>
  • 20 | 21 | 22 |
  • linkTo('import', 'index', $project, 'import', 'Import tickets…'); ?>
  • 23 | 24 |
  • 25 |
26 |
27 | 28 | 29 | 'ticket-edit']); ?> 30 |
31 |
    32 | 33 |
  • input('fahrplan_id', 'Fahrplan ID', null, ['class' => 'narrow']); ?>
  • 34 | 35 | 36 |
  • input('title', 'Title', (!empty($ticket))? $ticket['title'] : '', ['class' => 'wide', 'disabled' => (!empty($ticket) and $ticket['parent_id'] !== null)]); ?>
  • 37 | 38 | 39 |
  • 40 | 41 |

    42 | linkTo('encodingprofiles', 'view', $profile, $profile['name']); 44 | } else { 45 | echo $profile['name']; 46 | } 47 | 48 | echo ' (r' . $profile['revision'] . ')'; ?> 49 |

    50 | 51 |
  • 52 | 53 | 54 |
  • select( 55 | 'priority', 'Priority', 56 | Ticket::$priorities, 57 | (!empty($ticket))? $ticket['priority'] : '1' 58 | ); ?>
  • 59 |
  • select( 60 | 'handle_id', 'Assignee', 61 | ['' => '–'] + $users, 62 | (!empty($ticket))? $ticket['handle_id'] : '', 63 | [ 64 | 'data-current-user-id' => User::getCurrent()['id'], 65 | 'data-current-user-name' => User::getCurrent()['name'] 66 | ] 67 | ); ?>
  • 68 |
  • 69 | checkbox('group_needs_attention', 'Ticket group needs attention', (!empty($ticket))? $ticket->needsAttention() : false); ?> 70 |
  • 71 | register('comment'); ?> 72 |
73 |
74 |
75 | State 76 |
    77 |
  • 78 |
    79 | 80 | 81 | 82 | 83 | 84 | select('ticket_state', null, $states, (!empty($ticket))? $ticket['ticket_state'] : null, ['id' => 'ticket-edit-state']) ?> 85 |
    86 |
  • 87 |
  • checkbox('failed', 'Current state failed', (!empty($ticket))? $ticket['failed'] : false); ?>
  • 88 |
89 |
90 |
91 | Properties 92 | render('shared/form/properties', [ 93 | 'f' => $f, 94 | 'properties' => [ 95 | 'for' => (!empty($ticket))? $ticket->Properties : null, 96 | 'field' => 'properties', 97 | 'description' => 'property', 98 | 'key' => 'name', 99 | 'value' => 'value' 100 | ] 101 | ]); ?> 102 |
103 |
104 |
    105 |
  • 106 | submit('Create ticket') . ' or '; 108 | echo $this->linkTo('tickets', 'index', $project, 'discard ticket', ['class' => 'reset']); 109 | } else { 110 | echo $f->submit('Save ticket') . ' or '; 111 | echo $this->linkTo('tickets', 'view', $ticket, $project, 'discard changes', ['class' => 'reset']); 112 | echo $f->hidden('last_modified', $ticket['modified']); 113 | } ?> 114 |
  • 115 |
116 |
117 | -------------------------------------------------------------------------------- /src/Application/View/tickets/edit/_flash.html.php: -------------------------------------------------------------------------------- 1 | Ticket was changed while editing, didn't save. linkTo('tickets', 'edit', $ticket, $project, 'Reload', 'Reload latest changes. Save ticket again to overwrite changes.'); ?> -------------------------------------------------------------------------------- /src/Application/View/tickets/edit_multiple.html.php: -------------------------------------------------------------------------------- 1 | title('Edit ' . $tickets->getRows() . ' ' . $ticketType . ' tickets | '); ?> 2 | 3 |
4 |

5 | 6 | Edit linkTo( 7 | 'tickets', 'search', $project, ['?id=' . implode(',', $tickets->pluck('id'))], 8 | $tickets->getRows() . ' ' . $ticketType . ' tickets' 9 | ); ?> 10 | 11 |

12 |
13 | 14 | 'ticket-edit-multiple']); ?> 15 |
16 |
    17 |
  • select( 18 | 'priority', 'Priority', 19 | Ticket::$priorities, 20 | '1' 21 | ); ?>
  • 22 |
  • select( 23 | 'handle_id', 'Assignee', 24 | ['' => '–'] + $users, 25 | (!empty($ticket))? $ticket['handle_id'] : '', 26 | [ 27 | 'data-current-user-id' => User::getCurrent()['id'], 28 | 'data-current-user-name' => User::getCurrent()['name'] 29 | ] 30 | ); ?>
  • 31 |
  • 32 | checkbox('needs_attention', 'Tickets needs attention', false, [], false); ?> 33 | register('needs_attention', ['0', '1']); ?> 34 |
  • 35 |
36 |
37 |
38 | State 39 |
    40 |
  • 41 | 42 | 43 | 44 | 45 | 46 | select('ticket_state', null, $states, null, ['id' => 'ticket-edit-state']); ?> 47 |
  • 48 |
  • 49 | checkbox('failed', 'Selected state failed', false, [], false); ?> 50 | register('failed', ['0', '1']); ?> 51 |
  • 52 |
53 |
54 |
55 | Properties 56 | render('shared/form/properties', [ 57 | 'f' => $f, 58 | 'properties' => [ 59 | 'for' => $properties, 60 | 'field' => 'properties', 61 | 'description' => 'property', 62 | 'key' => 'name', 63 | 'value' => 'value', 64 | 'placeholder' => 'Multiple values' 65 | ] 66 | ]); ?> 67 |
68 |
69 |
    70 |
  • 71 | submit('Save tickets') . ' or '; 72 | echo $this->linkTo('tickets', 'index', $project, 'discard changes', ['class' => 'reset']); ?> 73 |
  • 74 |
75 |
76 | -------------------------------------------------------------------------------- /src/Application/View/tickets/feed.html.php: -------------------------------------------------------------------------------- 1 | title('Feed | '); ?> 2 | 3 |
4 | 5 | 6 | 7 |
8 | render('tickets/feed/progress'); ?> 9 | Overall progress 10 |
11 | 12 | 13 | render('tickets/feed/actions'); ?> 14 |
15 | 16 |
    17 | render('tickets/feed/entry', ['entry' => $entry]); 19 | } ?> 20 |
-------------------------------------------------------------------------------- /src/Application/View/tickets/feed.json.php: -------------------------------------------------------------------------------- 1 | render('tickets/feed/entry', ['entry' => $entry]); 5 | } 6 | 7 | return [ 8 | 'entries' => $entries, 9 | 'progress' =>$this->render('tickets/feed/progress'), 10 | 'actions' => $this->render('tickets/feed/actions') 11 | ]; ?> -------------------------------------------------------------------------------- /src/Application/View/tickets/feed/actions.html.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
  • 4 | 5 | linkTo('tickets', 'index', $project, array('?t=cutting'), 'recording task' . (($stats['cutting'] != 1)? 's' : '') . ' to cut'); ?> 6 |
  • 7 |
  • 8 | 9 | linkTo('tickets', 'index', $project, array('?t=releasing'), 'encoding task' . (($stats['checking'] != 1)? 's' : '') . ' to check'); ?> 10 |
  • 11 |
  • 12 | 13 | linkTo('tickets', 'index', $project, array('?failed'), (($stats['fixing'] != 1)? 'tickets' : 'ticket') . ' failed'); ?> 14 |
  • 15 |
-------------------------------------------------------------------------------- /src/Application/View/tickets/feed/entry.html.php: -------------------------------------------------------------------------------- 1 | getEventMessage(LogEntry::MESSAGE_TYPE_SINGLE); 4 | 5 | if ($message !== false) { 6 | $message = str_replace([ 7 | '{user_name}', 8 | '{id}' 9 | 10 | /*, 11 | '{tickets}'*/ 12 | ], [ 13 | $this->linkTo( 14 | 'tickets', 'index', $project, ['?u=' . $entry['handle_id']], 15 | $entry['handle_name'], 16 | ['data-handle' => $entry['handle_id']] 17 | ), 18 | $this->linkTo( 19 | 'tickets', 'view', ['id' => $entry['ticket_id']], $project, 20 | $entry['ticket_fahrplan_id'], 21 | [ 22 | 'data-ticket-id' => $entry['ticket_fahrplan_id'], 23 | 'aria-label' => $entry->Ticket->getTitle( 24 | $entry['parent_title'], 25 | $entry['encoding_profile_name'] 26 | ), 27 | 'data-tooltip' => true, 28 | ] 29 | ) 30 | /*, 31 | '' . ((isset($entry['children']))? (count($entry['children']) + 1) . ' tickets' : '1 ticket') . ''*/ 32 | ], $message); 33 | } 34 | 35 | $created = new DateTime($entry['created']); 36 | ?> 37 | 38 |
  • 43 | 44 | 45 | 46 | ' . Filter::specialChars($entry['comment_comment']) . '

    '; 49 | } else { 50 | $e .= $message; 51 | } */ ?> 52 | 53 | includesMessage()) { 54 | $lines = array_filter(explode("\n", h($entry['comment']))); 55 | 56 | echo '' . nl2br(implode('
    ', array_slice($lines, 0, 3))); 57 | 58 | if (count($lines) > 3) { 59 | echo ' ' . $this->linkTo( 60 | 'tickets', 'log', 61 | $entry->Ticket->toArray() + 62 | $project->toArray() + 63 | ['entry' => $entry['id'], '.txt'], 64 | 'more' 65 | ); 66 | } 67 | 68 | echo '
    '; 69 | } ?> 70 | 71 | 72 | 73 | 74 | 75 |
  • 76 | -------------------------------------------------------------------------------- /src/Application/View/tickets/feed/progress.html.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 |
    -------------------------------------------------------------------------------- /src/Application/View/tickets/index.html.php: -------------------------------------------------------------------------------- 1 | title('Tickets | '); ?> 2 | 3 |
    4 | 'tickets-filter']); ?> 5 |
      6 |
    • 7 |
    • 8 |
    • ">button('t', null, 'Recording', ['value' => 'recording']); ?>
    • 9 |
    • ">button('t', null, 'Cutting', ['value' => 'cutting']); ?>
    • 10 |
    • ">button('t', null, 'Encoding', ['value' => 'encoding']); ?>
    • 11 |
    • button('t', null, 'Releasing', ['value' => 'releasing']); ?>
    • 12 |
    • button('t', null, 'Released', ['value' => 'released']); ?>
    • 13 |
    • 14 |
    15 | 16 | 17 | render('tickets/index/_header'); ?> 18 |
    19 | 20 | 21 |
      22 | render('tickets/ticket', [ 24 | 'ticket' => $ticket 25 | ]); 26 | } ?> 27 |
    28 | 29 | -------------------------------------------------------------------------------- /src/Application/View/tickets/index.json.php: -------------------------------------------------------------------------------- 1 | render('tickets/ticket', [ 5 | 'ticket' => $ticket 6 | ]); 7 | } 8 | 9 | return $index; ?> -------------------------------------------------------------------------------- /src/Application/View/tickets/index/_header.html.php: -------------------------------------------------------------------------------- 1 | 'tickets-quicksearch']); ?> 2 |
      3 | 4 |
    • 5 | button('search', null, 'Search'); ?> 6 |
    • 7 | 8 | 9 |
    • 10 | 11 |
    • linkTo('tickets', 'create', $project, 'create', 'Create new ticket…'); ?>
    • 12 | 13 | 14 | 15 |
    • linkTo('import', 'index', $project, 'import', 'Import tickets…'); ?>
    • 16 | 17 |
    • 18 | 19 |
    20 | -------------------------------------------------------------------------------- /src/Application/View/tickets/search.html.php: -------------------------------------------------------------------------------- 1 | title('Ticket search | '); ?> 2 | 3 |
    4 |

    Search

    5 | 6 | render('tickets/index/_header'); ?> 7 |
    8 | 9 | 'tickets-search', 11 | 'data-search' => json_encode(($searchForm->wasSubmitted())? [ 12 | 'fields' => $fields, 13 | 'operators' => $operators, 14 | 'values' => $values 15 | ] : null), 16 | 'data-edit-url' => $this->Request->getRootURL() . 17 | Router::reverse('tickets', 'edit_multiple', [ 18 | 'tickets' => '{tickets}', 19 | 'project_slug' => $project['slug'] 20 | ]) 21 | ]); ?> 22 |
    23 |
      24 |
    • 25 | submit('Search'); ?> 26 |
    • 27 |
    28 |
    29 |
    30 | select('types', '', ['meta' => 'meta', 'recording' => 'recording', 'ingest' => 'ingest', 'encoding' => 'encoding'], null, ['id' => 'tickets-search-types']); ?> 31 | select('states', '', $states, null, ['id' => 'tickets-search-states']); ?> 32 | select('users', '', [ 33 | 'Users' => $users, 34 | 'Workers (assigned)' => $assignedWorkers, 35 | 'Workers (15 last seen)' => $workers 36 | ], null, ['id' => 'tickets-search-assignees']); ?> 37 | select('profiles', '', $profiles->toArray(), null, ['id' => 'tickets-search-profiles']); ?> 38 | select('days', '', $days->toArray(), null, ['id' => 'tickets-search-days']); ?> 39 | select('rooms', '', $rooms->toArray(), null, ['id' => 'tickets-search-rooms']); ?> 40 | select('languages', '', $languages->toArray(), null, ['id' => 'tickets-search-languages']); ?> 41 |
    42 | 43 | register('fields[]'); 44 | $f->register('operators[]'); 45 | $f->register('values[]'); ?> 46 | 47 | 0, 'recording' => 0, 'encoding' => 0, 'ingest' => 0]; ?> 49 |
      50 | render('tickets/ticket', [ 52 | 'ticket' => $ticket 53 | ]); 54 | 55 | $stats[$ticket['ticket_type']]++; 56 | } ?> 57 |
    58 | 59 |
    60 | The search matched 61 | $count) { 62 | if ($count > 0 and $type !== 'meta') { 63 | echo (($count === 1)? 'one' : $count) . ' ' . $type . 64 | ' ticket' . (($count !== 1)? 's' : '') . ', '; 65 | } 66 | } ?> 67 | 68 | meta ticket. 69 |
    70 | 71 | 72 | -------------------------------------------------------------------------------- /src/Application/View/tickets/ticket.html.php: -------------------------------------------------------------------------------- 1 | Project; 4 | } 5 | 6 | $attributes = [ 7 | 'data-id' => $ticket['id'], 8 | 'data-fahrplan-id' => $ticket['fahrplan_id'], 9 | 'data-ticket-type' => $ticket['ticket_type'] 10 | ]; 11 | 12 | if (empty($ticket['parent_id'])) { 13 | $attributes['data-title'] = $ticket['title']; 14 | } else { 15 | $attributes['class'] = ((!empty($simulateTickets))? 'no-properties' : 'child'); 16 | } 17 | 18 | if (!empty($filtered) and (!empty($filtered[$ticket['id']]) or !empty($filtered[$ticket['parent_id']]))) { 19 | if (isset($attributes['class'])) { 20 | $attributes['class'] .= ' filtered'; 21 | } else { 22 | $attributes['class'] = 'filtered'; 23 | } 24 | 25 | $filters = (!empty($filtered[$ticket['id']]))? 26 | $filtered[$ticket['id']] : $filtered[$ticket['parent_id']]; 27 | 28 | $attributes['title'] = "Ticket does not match the following properties from the worker group filter:\n"; 29 | 30 | foreach ($filters as $filter) { 31 | $attributes['title'] .= "\n" . $filter['property_key'] . ' = ' . $filter['property_value']; 32 | } 33 | } ?> 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 39)? ' aria-label="' . h($ticket['title']) . '" data-tooltip="true"' : ''; ?>> 46 | 47 | getTitleSuffix(); 48 | echo h(str_shorten(($suffix !== '')? $suffix : $ticket['title'], 39)); ?> 49 | 50 | 51 | 52 | '; 54 | echo '' . 55 | round((((float) $ticket['priority_product']) - 1) * 100 + 1, 2) . ''; 56 | } else { 57 | echo ''; 58 | 59 | if (empty($ticket['parent_id']) and isset($ticket['fahrplan_day'])) { 60 | echo (!empty($ticket['fahrplan_day']) or $ticket['fahrplan_day'] === '0')? ('Day ' . h($ticket['fahrplan_day'])) : '-'; 61 | } 62 | } ?> 63 | 64 | 65 | 66 | format('H:i')); 68 | } else { 69 | echo ' '; 70 | } ?> 71 | 72 | '; 74 | echo h($ticket['fahrplan_room']); 75 | } else { 76 | echo '>'; 77 | } ?> 78 | 79 | 80 | 81 | 82 | 83 | linkTo( 84 | 'tickets', 'index', $project, 85 | ['?u=' . $ticket['handle_id']], 86 | $ticket['handle_name'], 87 | null, 88 | [ 89 | 'data-handle' => $ticket['handle_id']/*, 90 | 'aria-label' => "Last seen: \nIdentitfication: 12392", 91 | 'data-tooltip' => true*/ 92 | ] 93 | ); ?> 94 | 95 | 96 | isEligibleAction('cut') and User::isAllowed('tickets', 'cut')) { 97 | echo $this->linkTo('tickets', 'cut', $ticket, $project, 'cut', 'Cut recording ticket…', ['class' => 'action']); 98 | } ?> 99 | 100 | isEligibleAction('check') and User::isAllowed('tickets', 'check')) { 101 | echo $this->linkTo('tickets', 'check', $ticket, $project, 'check', 'Check ticket…', ['class' => 'action']); 102 | } ?> 103 | 104 | linkTo('tickets', 'edit', $ticket, $project, 'edit', 'Edit ticket…', ['class' => 'edit']); 106 | } ?> 107 | 108 | 109 | linkTo( 111 | 'tickets', 'view', $ticket, $project, 112 | (isset($ticket['progress']))? ('' . (($ticket['progress'] != '0')? '' : '') . '') : '', 113 | (isset($ticket['progress']))? (round($ticket['progress']) . '%') : '', 114 | ['class' => 'progress'] 115 | ); 116 | } ?> 117 | 118 | -------------------------------------------------------------------------------- /src/Application/View/tickets/view.html.php: -------------------------------------------------------------------------------- 1 | title($ticket['fahrplan_id'] . ' | ' . $ticket['title'] . ' | '); 3 | } else { 4 | $this->title(mb_ucfirst($action) . ' lecture ' . $ticket['title'] . ' | '); 5 | } ?> 6 | 7 | render('tickets/view/_header', [ 8 | 'titlePrefix' => (isset($action))? 9 | h(mb_ucfirst($action)) . ' lecture ' : 10 | null, 11 | 'showDetails' => !isset($action), 12 | 'currentAction' => (isset($action))? $action : null 13 | ]); ?> 14 | 15 | render('tickets/view/_action'); 17 | } ?> 18 | 19 | 20 |

    Parent

    21 |
      22 | render('tickets/ticket', [ 23 | 'ticket' => $parent 24 | ]); ?> 25 |
    26 | 27 | 28 | getRows() > 0): ?> 29 |

    Children

    30 |
      31 | render('tickets/ticket', [ 33 | 'ticket' => $child, 34 | 'simulateTickets' => true 35 | ]); 36 | } ?> 37 |
    38 | 39 | 40 | 41 |

    Encoding profile

    42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 65 | 66 |
    NameVersion
    r 56 | linkTo('encodingprofiles', 'edit', $profile, 'edit profile'); 58 | } ?> 59 |
    67 | 68 | 69 |

    Properties

    70 | 71 | getRows() > 0 or ($ticket['ticket_type'] === 'encoding' and empty($action))) { 72 | echo $this->render( 73 | 'shared/properties', [ 74 | 'merged' => $ticket['ticket_type'] === 'encoding' and empty($action) 75 | ] 76 | ); 77 | } ?> 78 | 79 | 80 |
    81 | Last imported 82 | 83 | ' . h($import['version']) . ')') : 85 | ('' . h($import['url']) . ''); ?> 86 | by User['user_name']; ?>. 87 |
    88 | 89 | 90 | render('shared/properties', ['properties' => $parentProperties]); 92 | } 93 | 94 | if (isset($recordingProperties)) { 95 | echo $this->render('shared/properties', ['properties' => $recordingProperties]); 96 | } ?> 97 | 98 |
    99 |

    Timeline

    100 |
    101 |
      102 | 103 |
    • 104 | 105 |
      106 |
        107 |
      • textarea('text', null, null, array('class' => 'wide')); ?>
      • 108 |
      • 109 | checkbox('needs_attention', 'Ticket group needs attention', $ticket->needsAttention()); 110 | echo $f->submit('Comment'); ?> 111 |
      • 112 |
      113 |
      114 | 115 |
    • 116 | getIterator(); 119 | 120 | foreach ($comments as $comment) { 121 | while (strtotime($log->current()['created']) > strtotime($comment['created'])) { 122 | echo $this->render('tickets/view/_log_entry', ['entry' => $log->current()]); 123 | $log->next(); 124 | } 125 | 126 | echo $this->render('tickets/view/_comment', ['comment' => $comment]); 127 | } 128 | 129 | while ($log->current()) { 130 | echo $this->render('tickets/view/_log_entry', ['entry' => $log->current()]); 131 | $log->next(); 132 | } ?> 133 |
    134 |
    -------------------------------------------------------------------------------- /src/Application/View/tickets/view/_action.html.php: -------------------------------------------------------------------------------- 1 | 'ticket-action']); ?> 2 |
    3 |
      4 | 6 |
    • 7 |
    • 8 | 9 |

      10 | linkTo('tickets', 'index', $project, array('?u=' . $ticket['handle_id']), $ticket['handle_name']) . ' is ' . $state; 15 | } 16 | 17 | echo ' since ' . (new DateTime($ticket['modified']))->format('d.m.Y H:i:s'); ?>. 18 |

      19 |
    • 20 |
    • submit('Appropriate ticket', array('name' => 'appropriate')) . ' or ' . $this->linkTo('tickets', 'index', $project, 'leave ticket untouched'); ?>
    21 | 22 | render('tickets/view/action/_' . $action . '', ['f' => $f]); 26 | break; 27 | } ?> 28 | checkbox('jump', null, isset($_GET['jump']), []) . 29 | ''; 30 | 31 | echo ' or ' . $this->linkTo( 32 | 'tickets', 33 | 'un' . $action, 34 | $ticket, 35 | $project/* + (($referer)? array('?ref=' . $referer) : array())*/, 36 | 'leave and reset ticket', 37 | 'reset state and remove assignee' 38 | ); ?> 39 | 40 | 41 | 42 |
    43 | -------------------------------------------------------------------------------- /src/Application/View/tickets/view/_comment.html.php: -------------------------------------------------------------------------------- 1 |
  • 2 |

    3 | 4 |
    5 | 6 | by 7 | 8 | 9 | on 10 | linkTo('tickets', 'view', $comment->ReferencedTicket, $project, h($comment->ReferencedTicket->getTitleSuffix())); 14 | } ?> 15 | 16 | 17 | 18 | 19 | 20 | linkTo('tickets', 'delete_comment', $comment, $project, 'delete'); 22 | } ?> 23 | 24 | 25 |
    26 |
  • -------------------------------------------------------------------------------- /src/Application/View/tickets/view/_header.html.php: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | 4 | 50)? ' aria-label="' . h($ticket['title']) . '" data-tooltip="true"' : ''; ?>> 5 | linkTo('tickets', 'view', $ticket, $project, h(str_shorten($ticket['title'], 37)), null, array('aria-label' => $ticket['title'], 'data-tooltip' => true)); 7 | } else { 8 | echo h(str_shorten($ticket['title'], 50)); 9 | } ?> 10 | 11 |

    12 | 13 | 14 | 15 | last edited 16 | 17 | 18 | 19 |
    20 | 21 | 22 | failed 23 | 24 | 25 | 26 | 27 | 28 | needsAttention()): ?> 29 | needs attention 30 | 31 | 32 | 33 | assigned to linkTo( 34 | 'tickets', 'index', $project, ['?u=' . $ticket['handle_id']], 35 | ($ticket['handle_id'] == User::getCurrent()['id']) ? 'you' : $ticket['handle_name'], 36 | ['aria-label' => 'Last seen ' . timeRelativeDifference(new DateTime($ticket['handle_last_seen'])), 'data-tooltip' => true] 37 | ); ?> 38 | 39 |
    40 | 41 | 42 |
      43 |
    • 44 | 45 | render('tickets/view/_status'); ?> 46 | 47 | isEligibleAction($action)) { 49 | continue; 50 | } ?> 51 | 52 |
    • 53 | linkTo('tickets', $action, $ticket, $project, '' . $action . '', ucfirst($action) . '…'); 59 | break; 60 | /*case 'reset': 61 | echo $this->linkTo('tickets', 'reset', $ticket + $project, 'reset', 'Reset encoding task', ['data-dialog-confirm' => 'Are you sure you want to reset this encoding task?']); 62 | break;*/ 63 | case 'delete': 64 | echo $this->linkTo('tickets', 'delete', $ticket, $project, 'delete', 'Delete ticket', ['data-dialog-confirm' => 'Are you sure you want to permanently delete this ticket?']); 65 | break; 66 | } ?> 67 |
    • 68 | 69 |
    • 70 |
    71 | 72 |
    -------------------------------------------------------------------------------- /src/Application/View/tickets/view/_log_entry.html.php: -------------------------------------------------------------------------------- 1 |
  • 2 | getEventMessage()) !== false) { 3 | echo $message; 4 | } else { 5 | echo '' . $entry['event'] . ''; 6 | } ?> 7 | 8 | 9 | ', array_slice($lines, 0, 3))); 11 | 12 | if (count($lines) > 3) { 13 | echo ' ' . $this->linkTo('tickets', 'log', $ticket, ['entry' => $entry['id']], $project, array('.txt'), 'more'); 14 | } ?> 15 | 16 | 17 | 18 | by 19 | 20 | 21 | 22 | 23 | 24 |
  • -------------------------------------------------------------------------------- /src/Application/View/tickets/view/_status.html.php: -------------------------------------------------------------------------------- 1 | EncodingProfileVersion 8 | ->except(['fields']) 9 | ->select('tbl_project_encoding_profile.priority') 10 | ->where([ 11 | 'id' => $ticket['encoding_profile_version_id'] 12 | ]) 13 | ->first(); 14 | } 15 | 16 | if ($ticket->Parent['ticket_state'] !== 'staged') { 17 | $status = [ 18 | 'dependency', 19 | 'Parent is ' . $ticket->Parent['ticket_state'] . ' (staged required).' 20 | ]; 21 | // TODO: recording ticket < finalized (?) 22 | } elseif ( 23 | isset($projectEncodingProfile) and 24 | $projectEncodingProfile['priority'] === '0' 25 | ) { 26 | $status = [ 27 | 'disabled', 28 | 'Encoding profile is disabled.' 29 | ]; 30 | } elseif ($ticket['handle_id'] !== null) { 31 | $status = [ 32 | 'dependency', 33 | 'Ticket is assigned to ' . $ticket['handle_name'] . '.' 34 | ]; 35 | } elseif ( 36 | isset($projectEncodingProfile) and 37 | $ticket->isDependeeTicketMissing() === true 38 | ) { 39 | $status = [ 40 | 'dependency', 41 | "Encoding profile dependency not satisfied\n" . 42 | '(encoding ticket for dependee is missing).' 43 | ]; 44 | } elseif ( 45 | isset($projectEncodingProfile) and 46 | ($state = $ticket->getDependeeTicketState()) !== null and 47 | !$ticket->isDependeeTicketStateSatisfied() 48 | ) { 49 | $status = [ 50 | 'dependency', 51 | "Encoding profile dependency not satisfied\n" . 52 | '(supporting ticket is ' . $state . ').' 53 | ]; 54 | } else { 55 | $status = [ 56 | 'queue', 57 | 'Ticket is ' . ( 58 | ($ticket['ticket_state'] === 'scheduled')? 59 | 'scheduled' : 'queued' 60 | ) . 61 | ((isset($projectEncodingProfile))? 62 | ' (' . 63 | Ticket::$priorities[$projectEncodingProfile['priority']] . 64 | ' priority)' 65 | : '' 66 | ) . '.' 67 | ]; 68 | } 69 | // TODO: check worker group filter? 70 | // TODO: all matching worker groups paused? ?> 71 | 72 |
  • 73 | 74 |
  • -------------------------------------------------------------------------------- /src/Application/View/tickets/view/action/_check.html.php: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 |
  • 4 | 5 |

    6 | You've already cut this ticket, you may want to linkTo('tickets', 'uncheck', $ticket, $project, 'leave this to another user', 'reset state and remove assignee'); ?>. 7 |

    8 |
  • 9 | 10 | 11 |
  • checkbox('reset', 'Source material is flawed or cutting failed. Reset all encoding tasks.'); ?>
  • 12 |
  • checkbox('failed', 'This encoding failed or something is wrong with the metadata.'); ?>
  • 13 |
  • textarea('comment', 'Comment', null, ['class' => 'wide hidden']); ?>
  • 14 | 15 |
  • Recording language was set to 16 | 18 | . 19 |

  • 20 | 21 | 22 | hasEncodingProfilePublishingURL($ticket)): ?> 23 |
  • The file maybe available a($project->getEncodingProfilePublishingURL($ticket), 'as download'); ?>.

  • 24 | 25 | 26 |
  • submit('Everything\'s fine'); // Closing
  • is in parent template ?> -------------------------------------------------------------------------------- /src/Application/View/tickets/view/action/_cut.html.php: -------------------------------------------------------------------------------- 1 | getRows()): ?> 2 |
  • 3 | select('language', 'Language', array('' => '') + $languages->toArray()); ?> 4 | 5 | 6 | Fahrplan language is set as . 9 | 10 | Fahrplan language not set. 11 | 12 | 13 |
  • 14 | 15 | 16 |
  • checkbox('delay', 'There is a noticable audio delay.', !empty($properties['Record.AVDelay'])); ?>
  • 17 |
  • 18 | input('delay_by', 'Delay audio by', (!empty($properties['Record.AVDelay']))? delayToMilliseconds($properties['Record.AVDelay']['value']) : ''); ?> 19 | Delay is specified in milliseconds and can be negative.
    Note: values from mplayer can be used as is, values from VLC have to be negated first.
    20 |
  • 21 | 22 |
  • checkbox('expand', 'The content extends the given timeline.'); ?>
  • 23 |
  • select('expand_left', 'Expand left by', actionExpandOptions()); ?>
  • 24 |
  • select('expand_right', 'Expand right by', actionExpandOptions()); ?>
  • 25 | 26 |
  • checkbox('failed', 'I\'m unable to cut this lecture, because something is broken.', $ticket['failed']); ?>
  • 27 |
  • textarea('comment', 'Comment', null, array('class' => 'wide hidden')); ?>
  • 28 | 29 |
  • submit('I finished cutting'); // Closing
  • is in parent template ?> 30 | -------------------------------------------------------------------------------- /src/Application/View/user/edit.html.php: -------------------------------------------------------------------------------- 1 | title('Edit user ' . $user['name'] . ' | '); 3 | } else { 4 | $this->title('Add user | '); 5 | } ?> 6 | 7 | 8 | 9 |
    10 |

    11 | Edit user 12 |

    13 | 14 | 15 |
      16 |
    • 17 |
    • linkTo('user', 'delete', $user, 'delete', 'Delete user', ['data-dialog-confirm' => 'Are you sure you want to permanently delete this user?']); ?>
    • 18 |
    • 19 |
    20 | 21 |
    22 | 23 | 24 | 25 |
    26 | 27 |

    Add User

    28 | 29 |
      30 |
    • 31 | input('name', 'Name', $user['name']); ?> 32 |
    • 33 |
    • 34 | 35 | select('role', 'Role', [ 36 | 'engineer' => 'engineer' 37 | ], $user['role'], ['disabled' => true]); ?> 38 | 39 | select('role', 'Role', [ 40 | 'read only' => 'read only', 41 | 'restricted' => 'restricted', 42 | 'user' => 'user', 43 | 'restricted superuser' => 'restricted superuser', 44 | 'superuser' => 'superuser', 45 | 'admin' => 'admin' 46 | ], $user['role'], ['data-user-edit-role' => '']); ?> 47 | 48 |
    • 49 |
    50 |
    51 |
    52 | Password 53 |
      54 | 55 |
    • 56 | password('user_password', 'Your password'); ?> 57 | To change the users password first enter your password for confirmation. 58 |
    • 59 | 60 |
    • password('password', (!empty($user))? 'New user password' : 'Password'); ?>
    • 61 |
    62 |
    63 |
    64 | Project access 65 |
      66 |
    • 67 | checkbox('restrict_project_access', 'Restrict access to the following projects', (!empty($user))? $user['restrict_project_access'] : true, ['data-enable-restrictions' => '']); ?> 68 | Access restrictions are only available for non admin users. 69 |
    • 70 | 71 | $project): ?> 72 |
    • 73 | 74 | hidden('Project[' . $index . '][project_id]', $project['id'], ['data-project-index' => $index, 'data-project-destroy' => 'Project[' . $index . '][_destroy]']); ?> 75 | 76 | 77 | 78 |
    • 79 | 80 | 81 | 82 |
    • select('', '', ['' => ''] + $projects->toArray(), '', ['data-project-select' => '']); ?>
    • 83 | register('Project[][project_id]'); ?> 84 | register('Project[][_destroy]'); ?> 85 |
    86 |
    87 |
    88 |
      89 |
    • 90 | submit((!empty($user))? 'Save user' : 'Create user') . ' or '; 91 | echo $this->linkTo('user', 'index', 'discard changes', array('class' => 'reset')); ?> 92 |
    • 93 |
    94 |
    95 | 96 | -------------------------------------------------------------------------------- /src/Application/View/user/index.html.php: -------------------------------------------------------------------------------- 1 | title('Manage users | '); ?> 2 | 3 | 4 |
      5 |
    • 6 |
    • linkTo('user', 'create', 'create', 'Create new user'); ?>
    • 7 |
    • 8 |
    9 | 10 | 11 |
    12 |

    Users

    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 40 | 43 | 44 | 45 | 46 |
    NameRole   
    47 |
    48 | -------------------------------------------------------------------------------- /src/Application/View/user/login.html.php: -------------------------------------------------------------------------------- 1 | title('Login | '); ?> 2 | 3 |
    4 | 'user-login')); ?> 5 |
    6 | Login 7 |
      8 |
    • input('user', 'User', null, ['autofocus' => true]); ?>
    • 9 |
    • password('password', 'Password'); ?>
    • 10 |
    • checkbox('remember', 'Keep me logged in'); ?>
    • 11 |
    • submit('Login'); ?>
    • 12 |
    13 |
    14 | 15 |
    -------------------------------------------------------------------------------- /src/Application/View/user/settings.html.php: -------------------------------------------------------------------------------- 1 | title('Settings | '); ?> 2 | 3 |

    Settings

    4 | 5 | 6 |
    7 | Change Password 8 |
      9 |
    • password('current_password', 'Current password'); ?>
    • 10 |
    • password('password', 'New password'); ?>
    • 11 |
    • password('password_confirmation', 'Repeat password'); ?>
    • 12 |
    • submit('Change password') ?>
    • 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /src/Application/View/workers/group/edit.html.php: -------------------------------------------------------------------------------- 1 | title((isset($group))? ('Edit worker group ' . $group['title'] . ' | ') : 'Create new worker group | '); ?> 2 | 3 | 4 | 5 |
    6 |

    7 | 8 |
      9 |
    • 10 | 11 | 12 |
    • linkTo('workers', 'queue', $group, 'queue', 'Show worker group queue'); ?>
    • 13 | 14 | 15 |
    • linkTo('workers', 'edit_group', $group, 'edit', 'Edit worker group…'); ?>
    • 16 | 17 | 18 |
    • linkTo('workers', 'delete_group', $group, 'delete', 'Delete worker group', ['data-dialog-confirm' => 'Are you sure you want to permanently delete this worker group?']); ?>
    • 19 | 20 | 21 |
    • 22 |
    23 |
    24 | 25 | 26 | 27 |
    28 | 29 |

    Create new worker group

    30 | 31 | 32 |
      33 |
    • input('title', 'Title', $group['title']); ?>
    • 34 |
    35 |
    36 | 37 |
    38 | Authentication 39 |
      40 |
    • input('token', 'Token', $group['token'], array('readonly' => true, 'class' => 'wide'), false); ?>
    • 41 |
    • input('secret', 'Secret', $group['secret'], array('readonly' => true, 'class' => 'wide'), false); ?>
    • 42 |
    • checkbox('create_secret', 'Create new secret', false, array(), false); ?>
    • 43 |
    44 |
    45 | 46 |
    47 |
      48 |
    • submit((isset($group))? 'Save changes' : 'Create new worker group') . ' or '; 50 | echo $this->linkTo('workers', 'index', (isset($group))? 'discard changes' : 'discard worker group', array('class' => 'reset')); 51 | ?>
    • 52 |
    53 |
    54 | -------------------------------------------------------------------------------- /src/Application/View/workers/group/queue.html.php: -------------------------------------------------------------------------------- 1 | title($group['title'] . ' queue | ') ?> 2 | 3 |
    4 |

    queue

    5 | 6 |
      7 |
    • 8 | 9 |
    • linkTo('workers', 'queue', $group, 'queue', 'Show worker group queue'); ?>
    • 10 | 11 | 12 |
    • linkTo('workers', 'edit_group', $group, 'edit', 'Edit worker group…'); ?>
    • 13 | 14 | 15 |
    • linkTo('workers', 'delete_group', $group, 'delete', 'Delete worker group', ['data-dialog-confirm' => 'Are you sure you want to permanently delete this worker group?']); ?>
    • 16 | 17 | 18 |
    • 19 |
    20 |
    21 | 22 |
    23 | Your project access is restricted. This queue may contain more tickets from other projects. 24 |
    25 | 26 | 27 | 28 |
      29 | render('tickets/ticket', [ 31 | 'ticket' => $ticket 32 | ]); 33 | } ?> 34 |
    35 | -------------------------------------------------------------------------------- /src/Application/View/workers/index.html.php: -------------------------------------------------------------------------------- 1 | title('Worker groups | '); ?> 2 | 3 | 4 |
      5 |
    • 6 |
    • linkTo('workers', 'create_group', 'create', 'Create new worker group'); ?>
    • 7 |
    • 8 |
    9 | 10 | 11 |
    12 |

    Worker groups

    13 | 14 | 15 | 16 | 17 | 18 | 29 | 34 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Worker as $worker): ?> 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
    (paused)' : ''); ?> 19 | linkTo('workers', 'unpause', $group, 'continue'); 22 | } 23 | } else { 24 | if (User::isAllowed('workers', 'pause')) { 25 | echo $this->linkTo('workers', 'pause', $group, 'pause'); 26 | } 27 | } ?> 28 | 30 | linkTo('workers', 'queue', $group, 'show queue'); 32 | } ?> 33 | 35 | linkTo('workers', 'delete_group', $group, 'delete', ['data-dialog-confirm' => 'Are you sure you want to permanently delete this worker group?']); 37 | } ?> 38 | 40 | linkTo('workers', 'edit_group', $group, 'edit'); 42 | } ?> 43 |
    NameHostnameLast seen
    format('d.m.Y H:i:s'); ?>
    63 | 64 |
    -------------------------------------------------------------------------------- /src/Application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CRSTicketTracker", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "repository": "https://github.com/crs-tools/tracker", 6 | "devDependencies": { 7 | "autoprefixer": "^9.6.1", 8 | "copy-webpack-plugin": "^5.0.4", 9 | "css-loader": "^3.2.0", 10 | "mini-css-extract-plugin": "^0.8.2", 11 | "node-sass": "^4.13.0", 12 | "postcss-loader": "^3.0.0", 13 | "sass-loader": "^8.0.0", 14 | "webpack": "^4.41.0", 15 | "webpack-cli": "^3.3.9", 16 | "webpack-extraneous-file-cleanup-plugin": "^2.0.0" 17 | }, 18 | "scripts": { 19 | "build": "NODE_ENV=production webpack --progress", 20 | "watch": "NODE_ENV=production webpack --progress --watch", 21 | "build:debug": "webpack --debug --progress", 22 | "watch:debug": "webpack --debug --progress --watch" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 'off' */ 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const ExtraneousFileCleanupPlugin = require('webpack-extraneous-file-cleanup-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | const env = (process.env.NODE_ENV || 'development'); 9 | const isDevelopment = (env === 'development'); 10 | 11 | module.exports = { 12 | mode: env, 13 | context: path.resolve(__dirname), 14 | entry: { 15 | main: [ 16 | './Styles/index.scss' 17 | ], 18 | }, 19 | output: { 20 | // filename: 'js/[name].js', 21 | path: path.resolve(__dirname, '../Public'), 22 | publicPath: '' 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.scss$/, 28 | use: [ 29 | MiniCssExtractPlugin.loader, 30 | { 31 | loader: 'css-loader', 32 | options: { 33 | importLoaders: 1, 34 | sourceMap: true, 35 | url: false 36 | } 37 | }, 38 | { 39 | loader: 'postcss-loader', 40 | options: { 41 | plugins: () => [ 42 | require('autoprefixer')() 43 | ] 44 | } 45 | }, 46 | { 47 | loader: 'sass-loader', 48 | options: { 49 | prependData: `$DEBUG: ${JSON.stringify(isDevelopment)};` 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | plugins: [ 57 | new CopyWebpackPlugin([ 58 | {from: './Styles/images', to: 'images'} 59 | ]), 60 | new ExtraneousFileCleanupPlugin({ 61 | extensions: ['.js', '.js.map'] 62 | }), 63 | new MiniCssExtractPlugin({ 64 | filename: 'css/[name].css' 65 | }), 66 | new webpack.DefinePlugin({ 67 | DEBUG: JSON.stringify(isDevelopment), 68 | 'process.env.NODE_ENV': JSON.stringify(env) 69 | }), 70 | new webpack.LoaderOptionsPlugin({ 71 | minimize: !isDevelopment 72 | }) 73 | ], 74 | devtool: (!isDevelopment) ? 75 | 'nosources-source-map' : 76 | 'cheap-module-source-map' 77 | }; 78 | -------------------------------------------------------------------------------- /src/Config/AccessControl.php: -------------------------------------------------------------------------------- 1 | 77 | -------------------------------------------------------------------------------- /src/Config/Config.Default.php: -------------------------------------------------------------------------------- 1 | query( 16 | 'SET timezone = ' . Database::$Instance->quote(date_default_timezone_get()) 17 | ); 18 | 19 | requires('Cache/Adapter/APC'); 20 | Cache::setAdapter(new Cache_Adapter_APC()); 21 | 22 | session_set_cookie_params(0, '/', null, false, true); 23 | 24 | if (PHP_MAJOR_VERSION < 8) { 25 | libxml_disable_entity_loader(true); 26 | } 27 | 28 | ?> -------------------------------------------------------------------------------- /src/Config/Routes.php: -------------------------------------------------------------------------------- 1 | ['XMLRPC_Handler', 'default'], 3 | 4 | ':project_slug/tickets' => ['tickets', 'index'], 5 | ':project_slug/tickets/search' => ['tickets', 'search'], 6 | 7 | ':project_slug/ticket/create' => ['tickets', 'create'], 8 | 9 | ':project_slug/ticket/:id/comment' => ['tickets', 'comment'], 10 | ':project_slug/ticket/:ticket_id/comment/delete/:id' => ['tickets', 'delete_comment'], 11 | 12 | ':project_slug/ticket/:id/log/:entry' => ['tickets', 'log'], 13 | ':project_slug/ticket/:id' => ['tickets', 'view'], 14 | ':project_slug/ticket/:id/:action' => ['tickets'], 15 | 16 | ':project_slug/tickets/:tickets/edit' => ['tickets', 'edit_multiple'], 17 | 18 | ':project_slug/tickets/import' => ['import', 'index'], 19 | ':project_slug/tickets/import/create' => ['import', 'create'], 20 | ':project_slug/tickets/import/:id/continue' => ['import', 'continue_import'], 21 | ':project_slug/tickets/import/:id/rooms' => ['import', 'rooms'], 22 | ':project_slug/tickets/import/:id/review' => ['import', 'review'], 23 | ':project_slug/tickets/import/:id/apply' => ['import', 'apply'], 24 | ':project_slug/tickets/import/:id/repeat' => ['import', 'repeat'], 25 | ':project_slug/tickets/import/:id/schedule' => ['import', 'download'], 26 | 27 | ':project_slug/workers' => ['workers', 'project'], 28 | ':project_slug/services/hold' => ['services', 'hold'], 29 | ':project_slug/services/resume' => ['services', 'resume'], 30 | 31 | ':project_slug/settings' => ['projects', 'settings'], 32 | ':project_slug/settings/properties' => ['projects', 'properties'], 33 | ':project_slug/settings/encoding/profiles' => ['projects', 'profiles'], 34 | ':project_slug/settings/states' => ['projects', 'states'], 35 | ':project_slug/settings/worker' => ['projects', 'worker'], 36 | ':project_slug/settings/worker/filter/:id/edit' => ['projects', 'edit_filter'], 37 | 38 | 'encoding/profiles' => ['encodingprofiles', 'index'], 39 | 'encoding/profile/create' => ['encodingprofiles', 'create'], 40 | 'encoding/profile/:id/versions/compare' => ['encodingprofiles', 'compare'], 41 | 'encoding/profile/:id/versions' => ['encodingprofiles', 'view'], 42 | 'encoding/profile/:id/edit/:version?' => ['encodingprofiles', 'edit'], 43 | 'encoding/profile/:id/delete' => ['encodingprofiles', 'delete'], 44 | 45 | 'workers' => ['workers', 'index'], 46 | 'workers/group/create' => ['workers', 'create_group'], 47 | 'workers/group/:id/edit' => ['workers', 'edit_group'], 48 | 'workers/group/:id/delete' => ['workers', 'delete_group'], 49 | 'workers/group/:id/queue' => ['workers', 'queue'], 50 | 'workers/group/:id/pause' => ['workers', 'pause'], 51 | 'workers/group/:id/unpause' => ['workers', 'unpause'], 52 | 53 | 'users' => ['user', 'index'], 54 | 'user/create' => ['user', 'create'], 55 | 'user/:id/edit' => ['user', 'edit'], 56 | 'user/:id/delete' => ['user', 'delete'], 57 | 58 | 'user/switch/:id' => ['user', 'substitute'], 59 | 'user/exit' => ['user', 'changeback'], 60 | 61 | 'project/create' => ['projects', 'create'], 62 | ':project_slug/settings/general' => ['projects', 'edit'], 63 | ':project_slug/duplicate' => ['projects', 'duplicate'], 64 | ':project_slug/delete' => ['projects', 'delete'], 65 | 'projects' => ['projects', 'index'], 66 | 67 | 'api/v1/:project_slug/tickets/fahrplan' => ['API', 'tickets_fahrplan'], 68 | 'api/v1/:project_slug/tickets/released' => ['API', 'tickets_released'], 69 | 70 | 'login' => ['user', 'login'], 71 | 'logout' => ['user', 'logout'], 72 | 'settings' => ['user', 'settings'], 73 | 74 | ':project_slug?' => ['tickets', 'feed'], 75 | ]; ?> 76 | -------------------------------------------------------------------------------- /src/Public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond $1 !^index\.php 4 | RewriteRule ^(.*?)$ index.php/$1 [L] -------------------------------------------------------------------------------- /src/Public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/favicon.ico -------------------------------------------------------------------------------- /src/Public/images/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/background.gif -------------------------------------------------------------------------------- /src/Public/images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/main.png -------------------------------------------------------------------------------- /src/Public/images/main_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/main_2x.png -------------------------------------------------------------------------------- /src/Public/images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/progress.gif -------------------------------------------------------------------------------- /src/Public/images/wait.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/wait.gif -------------------------------------------------------------------------------- /src/Public/images/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crs-tools/tracker/9256f4f021a210c4b4a7e56bd67e95a37c31a58e/src/Public/images/warning.png -------------------------------------------------------------------------------- /src/Public/index.php: -------------------------------------------------------------------------------- 1 | getCode() > 400)? $e->getCode() : 500; 44 | echo Controller::renderTemplate( 45 | $c, 46 | ['exception' => $e], 47 | null, 48 | new Response($c) 49 | ); 50 | } 51 | 52 | ?> -------------------------------------------------------------------------------- /src/Public/javascript/jquery.cookie.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cookie plugin 3 | * 4 | * Copyright (c) 2010 Klaus Hartl (stilbuero.de) 5 | * Dual licensed under the MIT and GPL licenses: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * http://www.gnu.org/licenses/gpl.html 8 | * 9 | */ 10 | jQuery.cookie=function(e,b,a){if(arguments.length>1&&(b===null||typeof b!=="object")){a=jQuery.extend({},a);if(b===null)a.expires=-1;if(typeof a.expires==="number"){var d=a.expires,c=a.expires=new Date;c.setDate(c.getDate()+d)}return document.cookie=[encodeURIComponent(e),"=",a.raw?String(b):encodeURIComponent(String(b)),a.expires?"; expires="+a.expires.toUTCString():"",a.path?"; path="+a.path:"",a.domain?"; domain="+a.domain:"",a.secure?"; secure":""].join("")}a=b||{};c=a.raw?function(f){return f}: 11 | decodeURIComponent;return(d=RegExp("(?:^|; )"+encodeURIComponent(e)+"=([^;]*)").exec(document.cookie))?c(d[1]):null}; -------------------------------------------------------------------------------- /src/Public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /src/Public/xsl/jobstyle.xsl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 21 | 22 | 23 |

    CRS Tasks

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