What's New - SAP Sailing Race Manager
July 2019
+-
+
- The app no longer remembers the last entered duration when setting a protest time. + The protest duration can now be configured correctly in the app settings. + Please consider that a protest duration in the specific regatta configuration is preferred to + the protest duration in the default regatta configuration. +
May 2019
- The Course editor now works correctly when managing more than one regatta.
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java
index 9a2f2ace2be..3af2d3c950a 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java
@@ -357,7 +357,14 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint
ResultImportUrlsManagementPanel resultImportUrlsManagementPanel = new ResultImportUrlsManagementPanel(
getSailingService(), getUserService(), this, getStringMessages());
- panel.addToTabPanel(connectorsTabPanel, new DefaultRefreshableAdminConsolePanel
(resultImportUrlsManagementPanel), + panel.addToTabPanel(connectorsTabPanel, + new DefaultRefreshableAdminConsolePanel ( + resultImportUrlsManagementPanel) { + @Override + public void refreshAfterBecomingVisible() { + resultImportUrlsManagementPanel.refreshUrlList(); + } + }, getStringMessages().resultImportUrls(), SecuredDomainType.RESULT_IMPORT_URL.getPermission(DefaultActions.values())); StructureImportManagementPanel structureImportUrlsManagementPanel = new StructureImportManagementPanel( diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java index b41c919c03e..ce605b85230 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java @@ -107,7 +107,7 @@ public class ResultImportUrlsManagementPanel extends FlowPanel { return result; } - private void refreshUrlList() { + public void refreshUrlList() { String selectedProviderName = getSelectedProviderName(); if (selectedProviderName != null) { sailingService.getResultImportUrls(selectedProviderName, new AsyncCallback - >() {
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties
index 7747a105404..88c5081e6c2 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tiempo total de ceñida recorrido en esta carrera
noCustomGrouperScriptTextError=El script de grupo está vacío
noGrouperSelectedError=No se ha seleccionado ningún tipo de agrupador
dataMining=Minería de datos
+changeOrientation=Modificar orientación
hideToolbar=Ocultar barra de herramientas
showSeriesLeaderboards=Mostrar tablas de clasificación de las series
showOverallLeaderboard=Mostrar tabla de clasificación general
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Se ha importado el número siguiente de correcciones
timeToStart={0} hasta la salida
timeSinceStart={0} desde la salida
distanceToLine=Distancia hasta la línea de salida
-TWATooltip=Ángulo entre la dirección de los competidores y el viento
+TWATooltip=El ángulo entre el rumbo de los competidores sobre el fondo y el viento, muestra valores negativos que implican viento de babor, valores positivos que implican viento de estribor.
+absoluteTWATooltip=El ángulo positivo entre el rumbo de los competidores sobre el fondo y el viento, sin signo +/-
TWA=Ángulo real del viento
+absoluteTWA=Ángulo real absoluto del viento
showBoatClassChartsLabel=También puede visualizar el diagrama global para las clases de embarcación disponibles.
showDiagram=Mostrar diagrama
rerunQueryAfterRefreshTooltip=Vuelve a ejecutar la consulta después de que se hayan refrescado las tablas.
@@ -1927,6 +1930,7 @@ finishingTime=Hora de llegada
finishTimeString=Hora de fin
setFinishingAndFinishTime=Fijar hora de llegada y de fin
finishTimeMustBeAtOrAfterFinishingTime=Hora de fin debe ser a la hora de llegada o posterior
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Debe indicar la hora de llegada si indica la hora de fin
boatNameMustNotBeEmpty=Proporcione un nombre de embarcación que no esté en blanco
errorUpdatingExpeditionDeviceConfiguration=Error al actualizar la configuración del dispositivo con la expedición "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=¿Desea realmente eliminar la configuración del dispositivo con la expedición "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Tratar conexión SwissTiming
filterSwissTimingConnections=Filtrar conexiones SwissTiming
addSwissTimingConnection=Añadir conexión SwissTiming
updateURL=Actualizar URL
+startProcedure=Iniciar procedimiento
+startMode=Iniciar modo
+gateOpeningTime=Horario de apertura de puerta
+protestTimeInMinutes=Tiempo para protestar en minutos
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties
index 297a661a664..d8113a3ef33 100755
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Durée totale de navigation dans le lit du vent dans cette
noCustomGrouperScriptTextError=Script de regroupement vide
noGrouperSelectedError=Aucun type de regroupement sélectionné
dataMining=Exploration de données
+changeOrientation=Modifier orientation
hideToolbar=Masquer barre d''outils
showSeriesLeaderboards=Afficher palmarès de la série
showOverallLeaderboard=Afficher palmarès général
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Le nombre suivant de points vent ont été importés
timeToStart={0} jusqu''au départ
timeSinceStart={0} depuis le départ
distanceToLine=Distance jusqu''à la ligne de départ
-TWATooltip=L''angle entre la direction du concurrent et le vent.
+TWATooltip=L''angle entre la route fond des concurrents et le vent, accompagné d''un signe : les valeurs négatives indiquent que le vent vient de bâbord, les valeurs positives indiquent que le vent vient de tribord.
+absoluteTWATooltip=L''angle positif entre la route fond des concurrents et le vent, sans signe +/-.
TWA=Angle du vent réel
+absoluteTWA=Angle du vent réel absolu
showBoatClassChartsLabel=Vous pouvez également afficher le diagramme global pour les catégories de bateau disponibles.
showDiagram=Afficher diagramme
rerunQueryAfterRefreshTooltip=Réexécute la requête après l''actualisation des tables.
@@ -1927,6 +1930,7 @@ finishingTime=Heure d''arrivée du premier
finishTimeString=Heure de fermeture de la ligne d''arrivée
setFinishingAndFinishTime=Définir heure d''arrivée du premier et de fermeture de la ligne d''arrivée
finishTimeMustBeAtOrAfterFinishingTime=L''heure de fermeture de la ligne d''arrivée doit être égale ou ultérieure à l''heure d''arrivée du premier.
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Vous devez saisir une heure d''arrivée du premier si vous saisissez une heure de fermeture de la ligne d''arrivée.
boatNameMustNotBeEmpty=Veuillez indiquer un nom de bateau.
errorUpdatingExpeditionDeviceConfiguration=Erreur lors de la mise à jour de la configuration du terminal Expedition "{0}" : {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Voulez-vous vraiment supprimer la configuration du terminal Expedition "{0}" ?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Modifier connexion SwissTiming
filterSwissTimingConnections=Filtrer connexions SwissTiming
addSwissTimingConnection=Ajouter connexion SwissTiming
updateURL=Mettre à jour URL
+startProcedure=Procédure de départ
+startMode=Mode de départ
+gateOpeningTime=Heure d''ouverture de la porte
+protestTimeInMinutes=Durée de réclamation en minutes
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_it.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_it.properties
index d8f2c943a40..6bb5df9ba72 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_it.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_it.properties
@@ -339,7 +339,7 @@ notAvailable=Non disponibile
noGroupSelected=Nessun gruppo selezionato
combinedWindSourceTypeName=Combinato
legMiddleWindSourceTypeName=Metà della tratta
-courseBasedWindSourceTypeName=Progettazione rotta
+courseBasedWindSourceTypeName=Progettazione della rotta
trackBasedEstimationWindSourceTypeName=Stima
windSensorWindSourceTypeName=Misurato
webWindSourceTypeName=Manuale
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tempo totale di navigazione di bolina in questa gara
noCustomGrouperScriptTextError=Script membro vuoto
noGrouperSelectedError=Nessun tipo di membro selezionato
dataMining=Estrazione dati
+changeOrientation=Modifica orientamento
hideToolbar=Nascondi barra degli strumenti
showSeriesLeaderboards=Mostra classifiche serie
showOverallLeaderboard=Mostra classifica generale
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Per le gare elencate è stato importato il seguente n
timeToStart={0} all’inizio
timeSinceStart={0} dall’inizio
distanceToLine=Distanza fino alla linea di partenza
-TWATooltip=L’angolo tra la direzione dei concorrenti e il vento
+TWATooltip=L’angolo tra la rotta effettiva al fondo dei concorrenti e il vento tracciato, dove i valori negativi indicano il vento da sinistra e quelli positivi il vento da destra
+absoluteTWATooltip=L''angolo positivo tra la rotta effettiva al fondo dei concorrenti e il vento, senza segno positivo o negativo
TWA=Angolo del vento reale
+absoluteTWA=Angolo assoluto del vento reale
showBoatClassChartsLabel=È anche possibile visualizzare il diagramma complessivo delle classi di barca disponibili.
showDiagram=Mostra diagramma
rerunQueryAfterRefreshTooltip=Riesegue la query dopo che le tabelle sono state aggiornate.
@@ -1927,6 +1930,7 @@ finishingTime=Ora conclusione
finishTimeString=Ora di fine
setFinishingAndFinishTime=Imposta ora conclusione e ora di fine
finishTimeMustBeAtOrAfterFinishingTime=L’ora di fine deve essere uguale o successiva all’ora di conclusione
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Se si inserisce un''ora di fine occorre indicare un''ora di conclusione
boatNameMustNotBeEmpty=Fornisci un nome barca non vuoto
errorUpdatingExpeditionDeviceConfiguration=Errore nell’aggiornamento della configurazione del dispositivo Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Confermare l''eliminazione della configurazione del dispositivo Expedition "{0}"?
@@ -2001,9 +2005,9 @@ raceNameIsRequired=Deve essere indicato un nome per la gara suddivisa.
raceNameIsAlreadyUsed=Il nome indicato è già in uso
errorWhilePreparingToSliceARace=Errore nella preparazione della suddivisione gara
errorWhileSlicingARace=Errore nella suddivisione della gara
-windFinderWindSourceTypeName=
+windFinderWindSourceTypeName=Windfinder
windFinderWindSourceTypeTooltip=Vento misurato da uno o più punti di www.windfinder.com
-windFinder=
+windFinder=Windfinder
enterTagsForTheVideo=Inserisci i tag per il video
enterIdOfWindFinderReviewedSpotCollection=Inserisci l''ID di una collezione di punti WindFinder rivista, es. "schilksee"
windFinderSpotCollectionsList=Collezioni punti WindFinder
@@ -2128,7 +2132,7 @@ automaticResizeNeedsStorageService=Per il ridimensionamento automatico occorre u
qrCodeBoatInviteTitle=Sei invitato a tracciare la barca {0} per la regata {1}
qrCodeMarkInviteTitle=Sei invitato a tracciare il segnale {0} per la regata {1}
qrCodeCompetitorInviteTitle=Benvenuto {0} alla regata {1}
-qrCodeTitleBouy=Benvenuto come {0}
+qrCodeTitleBouy=Benvenuto come posaboe per la regata {0}
qrCodeTitle=Benvenuto alla regata {0}
qrCodeSubtitle={0} - {1}
qrCodeTitleOpenRegatta=Benvenuto alla regata pubblica {0}
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Modifica connessione SwissTiming
filterSwissTimingConnections=Filtra connessioni SwissTiming
addSwissTimingConnection=Aggiungi connessione SwissTiming
updateURL=Aggiorna URL
+startProcedure=Procedura di partenza
+startMode=Modalità di partenza
+gateOpeningTime=Ora di apertura del cancello
+protestTimeInMinutes=Tempo per protesta (minuti)
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties
index c23bdaadcaf..4eccc94d08a 100755
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=このレースでアップウィンドで移動した合
noCustomGrouperScriptTextError=分類スクリプトが空です
noGrouperSelectedError=分類タイプが選択されていません
dataMining=データマイニング
+changeOrientation=方向の変更
hideToolbar=ツールバー非表示
showSeriesLeaderboards=シリーズリーダーボード表示
showOverallLeaderboard=全体リーダーボード表示
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=一覧されたレースに対して次の数のフ
timeToStart=スタートまで {0}
timeSinceStart=スタートから {0}
distanceToLine=スタートラインまでの距離
-TWATooltip=競技者の方向と風の間のアングル
+TWATooltip=競技者の対地コースと風の間のアングル (符号あり、マイナスの値は左舷からの風、プラスの値は右舷からの風を意味)
+absoluteTWATooltip=競技者の対地コースと風の間のアングル (+/- 符号のないプラスの値)
TWA=真の風角度
+absoluteTWA=絶対真風角度
showBoatClassChartsLabel=利用可能な艇種の全体図も表示することができます。
showDiagram=ダイアグラム表示
rerunQueryAfterRefreshTooltip=テーブルがリフレッシュされた後にクエリを再実行します。
@@ -1927,6 +1930,7 @@ finishingTime=フィニッシュ予定時刻
finishTimeString=フィニッシュ時刻
setFinishingAndFinishTime=フィニッシュ予定時刻とフィニッシュ時刻の設定
finishTimeMustBeAtOrAfterFinishingTime=フィニッシュ時刻はフィニッシュ予定時刻以降であることが必要
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=フィニッシュ時刻を入力する場合、フィニッシュ予定時刻を入力することが必要
boatNameMustNotBeEmpty=空でない艇名を指定してください
errorUpdatingExpeditionDeviceConfiguration=Expedition デバイス設定 "{0}" の更新でエラーが発生: {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Expedition デバイス設定 "{0}" を削除しますか
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=SwissTiming 接続の編集
filterSwissTimingConnections=SwissTiming 接続のフィルタリング
addSwissTimingConnection=SwissTiming 接続の追加
updateURL=URL 更新
+startProcedure=スタート手順
+startMode=スタートモード
+gateOpeningTime=ゲートオープニング時刻
+protestTimeInMinutes=抗議受付時間 (分)
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties
index d7de762d16d..8f820d69441 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties
@@ -807,7 +807,7 @@ explainNoConnectionsFromReplicas=Nenhuma conexão de uma réplica encontrada. Vo
stopConnectionToMaster=Parar replicação
asLink=Como link
noStarttimeAnnouncedYet=Pendente
-raceIsRunning=Em execução
+raceIsRunning=Em competição
raceIsFinishing=Concluindo
raceIsFinished=Concluído
raceIsScheduled=Planejado
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Tempo total navegado a barlavento nesta corrida
noCustomGrouperScriptTextError=O script do sistema de agrupamento está vazio
noGrouperSelectedError=Nenhum tipo de sistema de agrupamento selecionado
dataMining=Data mining
+changeOrientation=Modificar orientação
hideToolbar=Ocultar barra de ferramentas
showSeriesLeaderboards=Visualizar painéis de classificação da série
showOverallLeaderboard=Visualizar painel de classificação geral
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=O número seguinte de pontos fixos foi importado para
timeToStart={0} até partida
timeSinceStart={0} desde partida
distanceToLine=Distância para linha de partida
-TWATooltip=O ângulo entre a direção dos competidores e o vento
+TWATooltip=O ângulo entre o percurso no fundo dos competidores e o vento, assinalado, sendo os valores negativos vento de bombordo e os valores positivos vento de boreste
+absoluteTWATooltip=O ângulo positivo entre o percurso no fundo dos competidores e o vento, sem sinal positivo/negativo
TWA=Ângulo do vento verdadeiro
+absoluteTWA=Ângulo do vento verdadeiro absoluto
showBoatClassChartsLabel=Você também pode ver o diagrama geral para as classes de barcos disponíveis.
showDiagram=Visualizar diagrama
rerunQueryAfterRefreshTooltip=Executa novamente a consulta após a atualização das tabelas.
@@ -1775,7 +1778,7 @@ couldNotSaveCompetitorRegistrations=Não foi possível gravar registros de compe
searchTagForImportedCompetitorPlaceholder=Utilizar, por exemplo, corrida + nome da flotilha
version=Versão: {0}
replicables=Replicando objetos
-set=Definir
+set=Curso
eventRegattaRacesLegendWind=# sensores de vento
eventRegattaRacesLegendVideo=# fluxos de vídeo
eventRegattaRacesLegendAudio=# fluxos de áudio
@@ -1927,6 +1930,7 @@ finishingTime=Hora de chegada
finishTimeString=Hora de conclusão
setFinishingAndFinishTime=Definir hora de chegada e conclusão
finishTimeMustBeAtOrAfterFinishingTime=A hora de conclusão deve ser igual ou posterior à hora de chegada
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=Você precisa inserir uma hora de chegada se inserir uma hora de conclusão
boatNameMustNotBeEmpty=Forneça um nome de barco não vazio
errorUpdatingExpeditionDeviceConfiguration=Erro ao atualizar configuração de dispositivo Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Você quer realmente remover a configuração do dispositivo Expedition "{0}"?
@@ -2114,7 +2118,7 @@ gender=Sexo
manage2SailEventIdBox=ID de evento Manage2Sail
manage2SailEventURLBox=URL de evento Manage2Sail (json)
manage2SailEventIdBoxTooltip=ID de evento ou um URL que contenha o ID de evento do Manage2Sail
-manage2SailPort=Porta
+manage2SailPort=Bombordo
audioFiles=Faixas de áudio
selectMedia=Selecionar mídia
swissTimingUpdateURL=Atualizar URL (por exemplo, para hora de partida e feedback de mudança de percurso)
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Processar conexão SwissTiming
filterSwissTimingConnections=Filtrar conexões SwissTiming
addSwissTimingConnection=Adicionar conexão SwissTiming
updateURL=Atualizar URL
+startProcedure=Procedimento de partida
+startMode=Modo de partida
+gateOpeningTime=Hora de abertura do portão
+protestTimeInMinutes=Tempo de protesto em minutos
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties
index 6ecb7727ea7..21e7c591fc3 100755
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=Общее время хода против ветра в
noCustomGrouperScriptTextError=Скрипт группирования пуст
noGrouperSelectedError=Не выбран тип группирования
dataMining=Добыча данных
+changeOrientation=Изменить ориентацию
hideToolbar=Скрыть панель инструментов
showSeriesLeaderboards=Показать таблицы лидеров серии
showOverallLeaderboard=Показать итоговую таблицу лидеров
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=Для перечисленных гонок импо
timeToStart={0} до старта
timeSinceStart={0} от старта
distanceToLine=Дистанция до линии старта
-TWATooltip=Угол между направлением участников и ветром
+TWATooltip=Угол между курсом участников относительно грунта и ветром, со знаком, отрицательные значения означают ветер по левому борту, положительные - ветер по правому борту.
+absoluteTWATooltip=участников относительно грунта и ветром, без знака +/-
TWA=Угол истинного ветра
+absoluteTWA=Абсолютный угол истинного ветра
showBoatClassChartsLabel=Также можно просмотреть общую диаграмму для доступных классов лодок.
showDiagram=Показать диаграмму
rerunQueryAfterRefreshTooltip=Повторно выполняет запрос после обновления таблиц.
@@ -1927,6 +1930,7 @@ finishingTime=Время финиширования
finishTimeString=Время финиша
setFinishingAndFinishTime=Указать время финиширования и финиша
finishTimeMustBeAtOrAfterFinishingTime=Время финиша должно совпадать или быть после времени финиширования
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=При вводе времени финиша необходимо ввести время финиширования
boatNameMustNotBeEmpty=Укажите непустое имя лодки
errorUpdatingExpeditionDeviceConfiguration=Ошибка при обновлении конфигурации устройства Expedition "{0}": {1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=Действительно удалить конфигурацию устройства Expedition "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=Редактировать соединение Swiss
filterSwissTimingConnections=Фильтр соединений SwissTiming
addSwissTimingConnection=Добавить соединение SwissTiming
updateURL=Обновить URL
+startProcedure=Процедура старта
+startMode=Режим старта
+gateOpeningTime=Время открытия ворот
+protestTimeInMinutes=Времени протеста в минутах
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties
index c43ab88387a..8cb1492b656 100755
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties
@@ -696,9 +696,9 @@ speedOverGroundFiveSecondsBeforeStartTooltip=起航前五秒时的实际速度
speedOverGroundWhenPassingStartTooltip=参赛队越过起航线时的实际速度
startTack=开始迎风转向
startTackTooltip=参赛队开始迎风转向
-portTack=端口
+portTack=左舷
starboardTack=右舷
-portSide=端口
+portSide=左舷
starboardSide=右舷
pleaseEnterANumber=请输入数字。
competitorsFilter=参赛队筛选器
@@ -948,6 +948,7 @@ raceTimeUpwindTooltip=本轮次比赛迎风航行的总时间
noCustomGrouperScriptTextError=群集器脚本为空
noGrouperSelectedError=未选择群集器类型
dataMining=数据挖掘
+changeOrientation=更改方向
hideToolbar=隐藏工具栏
showSeriesLeaderboards=显示系列积分榜
showOverallLeaderboard=显示总积分榜
@@ -1051,8 +1052,10 @@ resultFromIgtimiWindImport=已为所列的比赛轮次导入以下修复: {0}
timeToStart={0} - 开始
timeSinceStart={0} - 开始后
distanceToLine=与起航线的距离
-TWATooltip=赛队航行的方向和风的夹角
+TWATooltip=参赛队对地航向和风的夹角,设有符号标示,负值表示风来自左舷,正值表示风来自右舷
+absoluteTWATooltip=参赛队对地航向和风的正向夹角,不带 +/- 符号
TWA=实际风向角
+absoluteTWA=绝对实际风向角
showBoatClassChartsLabel=还可以查看可用船只级别的总直直方图。
showDiagram=显示直方图
rerunQueryAfterRefreshTooltip=在刷新表后,重新运行查询。
@@ -1927,6 +1930,7 @@ finishingTime=结束时间
finishTimeString=终点线时间
setFinishingAndFinishTime=设置结束和终点线时间
finishTimeMustBeAtOrAfterFinishingTime=终点线时间必须与结束时间相同或在其之后
+youHaveToEnterAFinishingTimeIfYouEnterAFinishedTime=如果输入终点线时间,则必须输入结束时间。
boatNameMustNotBeEmpty=请提供非空船只名称
errorUpdatingExpeditionDeviceConfiguration=更新导航图设备配置 "{0}" 出错:{1}
doYouReallyWantToRemoveExpeditionDeviceConfiguration=是否确定移除导航图设备配置 "{0}"?
@@ -2255,3 +2259,7 @@ editSwissTimingConnections=编辑 SwissTiming 连接
filterSwissTimingConnections=筛选 SwissTiming 连接
addSwissTimingConnection=添加 SwissTiming 连接
updateURL=更新 URL
+startProcedure=起航程序
+startMode=起航模式
+gateOpeningTime=门标开启时间
+protestTimeInMinutes=声明时间(分钟)
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java
index 8395fc3b387..e5c7f382d7c 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java
@@ -5209,19 +5209,13 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet
@Override
public List
- for the regatta owner (or any user with update permission on the regatta) to add existing +competitors to the regatta
- and for any user to register himself to an regatta that is explicitly open for self-registrations.
+All slashes contained within the {regattaname} parameter must be replaced by "__" (double underscore). +
++
+ +Back to Web Service Overview + + diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsDeleteDoc.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsDeleteDoc.html new file mode 100644 index 00000000000..e9c169db61c --- /dev/null +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsDeleteDoc.html @@ -0,0 +1,52 @@ + + + + + + + ++ +Webservice Type: +POST ++ +Output format: +None ++ +Mandatory parameters: ++ +regattaname the name of the regatta to register a competitor for+competitorid the UUID of the competitor to register++ +Additional mandatory parameters for user self-registrations on open regattas: ++ +secret secret string required for self registration validation purpose++ +Request method: +POST +SAP Sailing Analytics Webservices API Version 1.0 + + +SAP Sailing Analytics Webservices API Version 1.0
+URL: /api/v1/trackedevents/{eventId}/{leaderboardName}
+ + Description: +Delete a tracked event for the current user.
+
++
+ + Back to Web Service Overview + + diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsGetDoc.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsGetDoc.html new file mode 100644 index 00000000000..99e065311df --- /dev/null +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsGetDoc.html @@ -0,0 +1,53 @@ + + + + + + + ++ +Webservice Type: +REST ++ +Output format: +Json ++ +Mandatory parameters: +Path parameter eventId: Identifier of the associated event +
+ Path parameter leaderboardName: Name of the associated leaderboard ++ +Optional parameters: +None ++ +Request method: +DELETE ++ +Example: +Request: + http://www.sapsailing.com/sailingserver/api/v1/trackedevents?eventId=71fe232b-19b8-455f-ac91-23a53a5c557a&leaderbaordName=TestRegattaLeaderboard
+ Status 202
+SAP Sailing Analytics Webservices API Version 1.0 + + +SAP Sailing Analytics Webservices API Version 1.0
+URL: /api/v1/trackedevents
+ + Description: +Gets all tracked events with the associated + device-to-competitor/boat/mark elements for the current user.
+
++
+ + Back to Web Service Overview + + diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPostDoc.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPostDoc.html new file mode 100644 index 00000000000..e8f2522e2db --- /dev/null +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPostDoc.html @@ -0,0 +1,55 @@ + + + + + + + ++ +Webservice Type: +REST ++ +Output format: +Json ++ +Mandatory parameters: +includeArchived: Should be set to true, if the archived + elements should be returned as well. Default value false returns + only non-archived elements. ++ +Optional parameters: +None ++ +Request method: +GET ++ +Example: +http://www.sapsailing.com/sailingserver/api/v1/trackedevents?includeArchived=false +
+ Status 200SAP Sailing Analytics Webservices API Version 1.0 + + +SAP Sailing Analytics Webservices API Version 1.0
+URL: /api/v1/trackedevents
+ + Description: +Update the archived status of a tracked event for the current + user.
+
++
+ + Back to Web Service Overview + + diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPutDoc.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPutDoc.html new file mode 100644 index 00000000000..0ebae75b94f --- /dev/null +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/trackedeventsPutDoc.html @@ -0,0 +1,58 @@ + + + + + + + ++ +Webservice Type: +REST ++ +Output format: +Json ++ +Mandatory parameters: +eventId (Path parameter): Identifier of the associated event +
+ leaderboardName (Path parameter): Name of the associated leaderboard
+ isArchived (Form parameter): True or False depending on whether the + event should be set to archived or non-archived. ++ +Optional parameters: +None ++ +Request method: +POST ++ +Example: +Request: + http://www.sapsailing.com/sailingserver/api/v1/trackedevents/71fe232b-19b8-455f-ac91-23a53a5c557a/TestRegattaLeaderboard
+ form: isArchived=true
Status 202
+SAP Sailing Analytics Webservices API Version 1.0 + + +SAP Sailing Analytics Webservices API Version 1.0
+URL: /api/v1/trackedevents/{eventId}/{leaderboardName}
+ + Description: +Create or update a tracked event for the current user and set (overwrite) all tracked elements.
+
++
+ + Back to Web Service Overview + + diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index 61841b312ae..6a049d99f6c 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -46,6 +46,7 @@ import com.sap.sailing.server.impl.preferences.model.BoatClassNotificationPrefer import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences; import com.sap.sailing.server.impl.preferences.model.SailorProfilePreferences; import com.sap.sailing.server.impl.preferences.model.StoredDataMiningQueryPreferences; +import com.sap.sailing.server.impl.preferences.model.TrackedEventPreferences; import com.sap.sailing.server.interfaces.RacingEventService; import com.sap.sailing.server.notification.impl.SailingNotificationServiceImpl; import com.sap.sailing.server.security.SailingViewerRole; @@ -153,12 +154,15 @@ public class Activator implements BundleActivator { properties)); properties.put(PreferenceConverter.KEY_PARAMETER_NAME, StoredDataMiningQueryPreferences.PREF_NAME); registrations.add(context.registerService(PreferenceConverter.class, - new GenericJSONPreferenceConverter<>(() -> new StoredDataMiningQueryPreferences()), properties)); + new GenericJSONPreferenceConverter<>(StoredDataMiningQueryPreferences::new), properties)); properties.put(PreferenceConverter.KEY_PARAMETER_NAME, SailorProfilePreferences.PREF_NAME); registrations.add(context.registerService(PreferenceConverter.class, new GenericJSONPreferenceConverter<>( () -> new SailorProfilePreferences(racingEventService.getCompetitorAndBoatStore())), properties)); + properties.put(PreferenceConverter.KEY_PARAMETER_NAME, TrackedEventPreferences.PREF_NAME); + registrations.add(context.registerService(PreferenceConverter.class, + new GenericJSONPreferenceConverter<>(TrackedEventPreferences::new), properties)); } public static BundleContext getContext() { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java index 236c6f087a8..a089e5c2a29 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/SailingPreferences.java @@ -7,4 +7,5 @@ package com.sap.sailing.server.impl.preferences.model; public interface SailingPreferences { public static final String NOTIFICATION_PREFERENCES_PREFIX = "sailing.notifications."; public static final String STORED_DATAMINING_QUERY_PREFERENCES = "sailing.datamining.storedqueries"; + public static final String TRACKED_EVENTS_PREFERENCES = "sailing.profile.trackedevents"; } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedElementWithDeviceId.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedElementWithDeviceId.java new file mode 100644 index 00000000000..aea9b17eb8c --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedElementWithDeviceId.java @@ -0,0 +1,62 @@ +package com.sap.sailing.server.impl.preferences.model; + +import java.util.UUID; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.StringSetting; +import com.sap.sse.common.settings.generic.UUIDSetting; + +public class TrackedElementWithDeviceId extends AbstractGenericSerializableSettings { + + private static final long serialVersionUID = 234711768869820003L; + + private transient StringSetting deviceId; + private transient UUIDSetting trackedCompetitorId; + private transient UUIDSetting trackedBoatId; + private transient UUIDSetting trackedMarkId; + + public TrackedElementWithDeviceId() { + } + + /** copy constructor */ + public TrackedElementWithDeviceId(TrackedElementWithDeviceId other) { + this(); + this.deviceId.setValue(other.getDeviceId()); + this.trackedBoatId.setValue(other.getTrackedBoatId()); + this.trackedCompetitorId.setValue(other.getTrackedCompetitorId()); + this.trackedMarkId.setValue(other.getTrackedMarkId()); + } + + public TrackedElementWithDeviceId(String deviceId, UUID trackedBoatId, UUID trackedCompetitorId, + UUID trackedMarkId) { + this(); + this.deviceId.setValue(deviceId); + this.trackedBoatId.setValue(trackedBoatId); + this.trackedCompetitorId.setValue(trackedCompetitorId); + this.trackedMarkId.setValue(trackedMarkId); + } + + @Override + protected void addChildSettings() { + deviceId = new StringSetting("deviceId", this); + trackedCompetitorId = new UUIDSetting("trackedCompetitorId", this); + trackedBoatId = new UUIDSetting("trackedBoatId", this); + trackedMarkId = new UUIDSetting("trackedMarkId", this); + } + + public String getDeviceId() { + return deviceId.getValue(); + } + + public UUID getTrackedBoatId() { + return trackedBoatId.getValue(); + } + + public UUID getTrackedCompetitorId() { + return trackedCompetitorId.getValue(); + } + + public UUID getTrackedMarkId() { + return trackedMarkId.getValue(); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreference.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreference.java new file mode 100644 index 00000000000..b91e26c82d1 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreference.java @@ -0,0 +1,110 @@ +package com.sap.sailing.server.impl.preferences.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.UUID; + +import com.sap.sse.common.Util; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.BooleanSetting; +import com.sap.sse.common.settings.generic.SettingsList; +import com.sap.sse.common.settings.generic.StringSetting; +import com.sap.sse.common.settings.generic.UUIDSetting; + +public class TrackedEventPreference extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = 234711768869820003L; + + private transient UUIDSetting eventId; + private transient StringSetting leaderboardName; + private transient StringSetting baseUrl; + private transient BooleanSetting isArchived; + private transient StringSetting regattaSecret; + + private transient SettingsList+ +Webservice Type: +REST ++ +Output format: +Json ++ +Mandatory parameters: +Path Parameter eventId: Identifier of the associated event +
+ Path Parameter leaderboardName: Name of the associated leaderboard ++ +Optional parameters: +None ++ +Request method: +PUT ++ +Example: +Request: + http://www.sapsailing.com/sailingserver/api/v1/trackedevents/61fe232b-19b8-455f-ac91-23a53a5c557a/TestRegattaLeaderboard
+
Body (device-to-competitor):
+ {"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","competitorId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}
+
Body(device-to-boat):
+ {"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","boatId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}
+
Body (device-to-mark):
+ {"url":"http://www.sapsailing.com","regattaSecret":"71fe232b-19b8-455f-ac91-23a53a5c557a","trackedElements":[{"deviceId":"301eda00-68ad-402d-9734-4a7d5b8df406","markId":"6204e6d2-2d6c-4891-b077-d83117c69513"}],"isArchived":false}
+ Status 202
+trackedElements; + + public TrackedEventPreference() { + } + + /** copy constructor */ + public TrackedEventPreference(TrackedEventPreference other) { + this(); + eventId.setValue(other.getEventId()); + leaderboardName.setValue(other.getLeaderboardName()); + trackedElements.setValues(other.getTrackedElements()); + baseUrl.setValue(other.getBaseUrl()); + isArchived.setValue(other.getIsArchived()); + this.regattaSecret.setValue(other.getRegattaSecret()); + } + + /** copy constructor with new archived state */ + public TrackedEventPreference(TrackedEventPreference other, boolean isArchived) { + this(); + eventId.setValue(other.getEventId()); + leaderboardName.setValue(other.getLeaderboardName()); + trackedElements.setValues(other.getTrackedElements()); + baseUrl.setValue(other.getBaseUrl()); + this.isArchived.setValue(isArchived); + this.regattaSecret.setValue(other.getRegattaSecret()); + } + + /** copy constructor with additional tracked event */ + public TrackedEventPreference(TrackedEventPreference other, TrackedElementWithDeviceId trackedElement) { + this(); + eventId.setValue(other.getEventId()); + leaderboardName.setValue(other.getLeaderboardName()); + final Collection trackedElements = new HashSet<>(); + Util.addAll(other.getTrackedElements(), trackedElements); + trackedElements.add(trackedElement); + this.trackedElements.setValues(trackedElements); + baseUrl.setValue(other.getBaseUrl()); + this.isArchived.setValue(other.getIsArchived()); + this.regattaSecret.setValue(other.getRegattaSecret()); + } + + public TrackedEventPreference(UUID eventId, String leaderboardName, + Iterable trackedElements, String baseUrl, boolean isArchived, + String regattaSecret) { + this(); + this.eventId.setValue(eventId); + this.leaderboardName.setValue(leaderboardName); + this.trackedElements.setValues(trackedElements); + this.baseUrl.setValue(baseUrl); + this.isArchived.setValue(isArchived); + this.regattaSecret.setValue(regattaSecret); + } + + @Override + protected void addChildSettings() { + eventId = new UUIDSetting("eventId", this); + leaderboardName = new StringSetting("leaderboardName", this); + trackedElements = new SettingsList ("trackedElements", this, + () -> new TrackedElementWithDeviceId()); + baseUrl = new StringSetting("baseUrl", this); + isArchived = new BooleanSetting("isArchived", this); + regattaSecret = new StringSetting("regattaSecret", this); + } + + public UUID getEventId() { + return eventId.getValue(); + } + + public String getLeaderboardName() { + return leaderboardName.getValue(); + } + + public Iterable getTrackedElements() { + return trackedElements.getValues(); + } + + public String getBaseUrl() { + return baseUrl.getValue(); + } + + public boolean getIsArchived() { + return isArchived.getValue(); + } + + public String getRegattaSecret() { + return regattaSecret.getValue(); + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreferences.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreferences.java new file mode 100644 index 00000000000..d4745dee645 --- /dev/null +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/preferences/model/TrackedEventPreferences.java @@ -0,0 +1,30 @@ +package com.sap.sailing.server.impl.preferences.model; + +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.SettingsList; + +public class TrackedEventPreferences extends AbstractGenericSerializableSettings { + + private static final long serialVersionUID = -290209497308646446L; + + // TODO: setting preference title + public static final String PREF_NAME = SailingPreferences.TRACKED_EVENTS_PREFERENCES; + + private transient SettingsList trackedEvents; + + public TrackedEventPreferences() { + } + + @Override + protected void addChildSettings() { + trackedEvents = new SettingsList<>("trackedEvents", this, TrackedEventPreference::new); + } + + public Iterable getTrackedEvents() { + return trackedEvents.getValues(); + } + + public void setTrackedEvents(Iterable trackedEvents) { + this.trackedEvents.setValues(trackedEvents); + } +} diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index c2d29440fa0..53bc9063be4 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -21,6 +21,12 @@ Release Notes - Administration Console
+July 2019
+-
+
- Result and competitor import URLs are now only considered when the requesting user + has the permission to read that URL. +
June 2019
- Added new API endpoints for creating, reading, updating and deleting roles under /restsecurity/role. @@ -35,7 +41,12 @@
- Blue flag up/down times can be cleared in the AdminConsole now
- Users can now create media tracks if they have the permissions based on the ownership that will result from the creation process. +
- A couple of new API end points for managing tracked events were added under /api/v1/trackedevents/. + The new end points support creation, reading, updating and deleting tracked events and their + associated device-competitor/boat/mark elements for the current user. +
May 2019
- Ranks (finishing orders) entered by the Race Manager app user are no longer
diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/impl/SecondsDurationImpl.java b/java/com.sap.sse.common/src/com/sap/sse/common/impl/SecondsDurationImpl.java
new file mode 100644
index 00000000000..900d1824a2c
--- /dev/null
+++ b/java/com.sap.sse.common/src/com/sap/sse/common/impl/SecondsDurationImpl.java
@@ -0,0 +1,114 @@
+package com.sap.sse.common.impl;
+
+import com.sap.sse.common.Duration;
+
+/**
+ * Uses double for internal representation. Therefore this implementation achieves a higher accuracy but uses more
+ * memory in comparison to {@link MillisecondsDurationImpl}.
+ *
+ * @author Daniel Lisunkin (i505543)
+ *
+ */
+public class SecondsDurationImpl implements Duration {
+ private static final long serialVersionUID = 7575912908654324216L;
+ private final double seconds;
+
+ public SecondsDurationImpl(double seconds) {
+ super();
+ this.seconds = seconds;
+ }
+
+ @Override
+ public int compareTo(Duration o) {
+ double diff = seconds - o.asSeconds();
+ return diff > 0. ? 1 : diff < 0. ? -1 : 0;
+ }
+
+ @Override
+ public long asMillis() {
+ return Math.round(seconds / Duration.ONE_MILLISECOND.asSeconds());
+ }
+
+ @Override
+ public double asSeconds() {
+ return seconds;
+ }
+
+ @Override
+ public double asMinutes() {
+ return seconds / Duration.ONE_MINUTE.asSeconds();
+ }
+
+ @Override
+ public double asHours() {
+ return seconds / Duration.ONE_HOUR.asSeconds();
+ }
+
+ @Override
+ public double asDays() {
+ return seconds / Duration.ONE_DAY.asSeconds();
+ }
+
+ @Override
+ public Duration abs() {
+ return seconds >= 0 ? this : new SecondsDurationImpl(-seconds);
+ }
+
+ @Override
+ public Duration divide(long divisor) {
+ return new SecondsDurationImpl(seconds / divisor);
+ }
+
+ @Override
+ public Duration divide(double divisor) {
+ return new SecondsDurationImpl(seconds / divisor);
+ }
+
+ @Override
+ public double divide(Duration duration) {
+ return (seconds / duration.asSeconds());
+ }
+
+ @Override
+ public Duration times(long factor) {
+ return new SecondsDurationImpl(seconds * factor);
+ }
+
+ @Override
+ public Duration times(double factor) {
+ return new SecondsDurationImpl(seconds * factor);
+ }
+
+ @Override
+ public Duration minus(Duration duration) {
+ return new SecondsDurationImpl(seconds - duration.asSeconds());
+ }
+
+ @Override
+ public Duration minus(long milliseconds) {
+ return new SecondsDurationImpl(seconds - (double) milliseconds / 1000.);
+ }
+
+ @Override
+ public Duration plus(long milliseconds) {
+ return new SecondsDurationImpl(seconds + milliseconds / 1000.);
+ }
+
+ @Override
+ public Duration plus(Duration duration) {
+ return new SecondsDurationImpl(seconds + duration.asSeconds());
+ }
+
+ @Override
+ public Duration mod(Duration d) {
+ return new SecondsDurationImpl(seconds % d.asSeconds());
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer result = new StringBuffer();
+ result.append(asSeconds()).append("s");
+ return result.toString();
+ }
+
+}
diff --git a/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/AbstractUserGroupImpl.java b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/AbstractUserGroupImpl.java
index a1c84ec0f55..57f28d706c4 100644
--- a/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/AbstractUserGroupImpl.java
+++ b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/AbstractUserGroupImpl.java
@@ -4,6 +4,8 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import com.sap.sse.common.Util;
+
/**
* A group of users; equality and hash code are based solely on the {@link #getId() ID}.
*/
@@ -45,7 +47,6 @@ public abstract class AbstractUserGroupImpltrue
protestTimeDuration 90 -protestTimeDurationCustom -protestTimeDurationCustomEditable -true allow_demo false diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppPreferences.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppPreferences.java index 88081708281..3c3c607a367 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppPreferences.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/AppPreferences.java @@ -523,33 +523,12 @@ public class AppPreferences { context.getResources().getInteger(R.integer.preference_protest_time_duration_default)); } - public int getProtestTimeDurationInMinutesCustom() { - return helper.getInt(context.getString(R.string.preference_protest_time_duration_custom_key), -1); - } - - public boolean isDefaultProtestTimeCustomEditable() { - return helper.getBoolean(context.getString(R.string.preference_protest_time_duration_custom_editable_key), - context.getResources().getBoolean(R.bool.preference_protest_time_duration_custom_editable_default)); - } - public void setDefaultProtestTimeDurationInMinutes(int protestTimeInMinutes) { helper.getEditor() .putInt(context.getString(R.string.preference_protest_time_duration_key), protestTimeInMinutes) .commit(); } - public void setDefaultProtestTimeDurationInMinutesCustom(int protestTimeInMinutest) { - helper.getEditor() - .putInt(context.getString(R.string.preference_protest_time_duration_custom_key), protestTimeInMinutest) - .commit(); - } - - public void setDefaultProtestTimeDurationInMinutesCustomEditable(boolean editable) { - helper.getEditor() - .putBoolean(context.getString(R.string.preference_protest_time_duration_custom_editable_key), editable) - .commit(); - } - public void setAccessToken(String accessToken) { helper.getEditor().putString(context.getString(R.string.preference_access_token_key), accessToken).commit(); } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/configuration/impl/PreferencesRegattaConfigurationLoader.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/configuration/impl/PreferencesRegattaConfigurationLoader.java index ee70bfe07f2..6761a04c23a 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/configuration/impl/PreferencesRegattaConfigurationLoader.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/domain/configuration/impl/PreferencesRegattaConfigurationLoader.java @@ -1,8 +1,5 @@ package com.sap.sailing.racecommittee.app.domain.configuration.impl; -import java.util.ArrayList; -import java.util.HashSet; - import com.sap.sailing.android.shared.logging.ExLog; import com.sap.sailing.domain.base.configuration.ConfigurationLoader; import com.sap.sailing.domain.base.configuration.RacingProcedureConfiguration; @@ -23,6 +20,9 @@ import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.racecommittee.app.AppPreferences; import com.sap.sse.common.Duration; +import java.util.ArrayList; +import java.util.HashSet; + public class PreferencesRegattaConfigurationLoader implements ConfigurationLoader{ private static final String TAG = PreferencesRegattaConfigurationLoader.class.getSimpleName(); @@ -113,9 +113,6 @@ public class PreferencesRegattaConfigurationLoader implements ConfigurationLoade if (configuration.getDefaultProtestTimeDuration() != null) { preferences.setDefaultProtestTimeDurationInMinutes( (int) configuration.getDefaultProtestTimeDuration().asMinutes()); - preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(false); - } else { - preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(true); } if (configuration.getRRS26Configuration() != null) { diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/LoginActivity.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/LoginActivity.java index 29ffac51c1d..ecec4e0db61 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/LoginActivity.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/activities/LoginActivity.java @@ -401,7 +401,6 @@ public class LoginActivity extends BaseActivity implements EventSelectedListener public void onLoadFailed(Exception reason) { dismissProgressSpinner(); - preferences.setDefaultProtestTimeDurationInMinutesCustomEditable(true); if (reason instanceof FileNotFoundException) { Toast.makeText(getApplicationContext(), getString(R.string.loading_configuration_not_found), Toast.LENGTH_LONG).show(); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java index 0ed8e6ca2a7..606b9b19d86 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/dialogs/ProtestTimeDialogFragment.java @@ -319,8 +319,7 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements TimePoint startTime = TimeUtils.getTime(mTimePicker); TimePoint now = MillisecondsTimePoint.now(); for (ManagedRace race : selectedRaces) { - Duration duration = Duration.ONE_MINUTE - .times(AppPreferences.on(getActivity()).getProtestTimeDurationInMinutes()); + Duration duration = Duration.ONE_MINUTE.times(mDuration); TimeRange protestTime = new TimeRangeImpl(startTime, startTime.plus(duration)); race.getState().setProtestTime(now, protestTime); } @@ -404,12 +403,6 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements private void updateProtestRange() { if (mDuration == null) { mDuration = mPreferences.getProtestTimeDurationInMinutes(); - if (mPreferences.isDefaultProtestTimeCustomEditable()) { - int custom = mPreferences.getProtestTimeDurationInMinutesCustom(); - if (custom >= 0) { - mDuration = custom; - } - } } if (mProtestDuration != null) { mProtestDuration.setText(getString(R.string.protest_duration, mDuration)); @@ -468,9 +461,9 @@ public class ProtestTimeDialogFragment extends AttachedDialogFragment implements builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - mDuration = Integer.parseInt(duration.getText().toString()); - if (mPreferences.isDefaultProtestTimeCustomEditable()) { - mPreferences.setDefaultProtestTimeDurationInMinutesCustom(mDuration); + try { + mDuration = Integer.parseInt(duration.getText().toString()); + } catch (NumberFormatException ignored) { } updateProtestRange(); } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/preference/GeneralPreferenceFragment.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/preference/GeneralPreferenceFragment.java index c0fcc295200..5b3b404dd8d 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/preference/GeneralPreferenceFragment.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/preference/GeneralPreferenceFragment.java @@ -33,6 +33,7 @@ import com.sap.sailing.racecommittee.app.utils.autoupdate.AutoUpdater; import java.io.FileNotFoundException; import java.util.Objects; +import java.util.Set; import java.util.UUID; public class GeneralPreferenceFragment extends BasePreferenceFragment { @@ -200,6 +201,7 @@ public class GeneralPreferenceFragment extends BasePreferenceFragment { //Update all the other preferences, e.g. course areas, mail recipient final SharedPreferences.OnSharedPreferenceChangeListener listener = new SharedPreferences.OnSharedPreferenceChangeListener() { + @SuppressWarnings("unchecked") @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { final Preference preference = findPreference(key); @@ -211,6 +213,9 @@ public class GeneralPreferenceFragment extends BasePreferenceFragment { if (listener != null) { listener.onPreferenceChange(preference, newValue); } + if (preference instanceof EditSetPreference) { + preference.persistStringSet((Set ) newValue); + } } }; diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/views/IntegerEditTextPreference.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/views/IntegerEditTextPreference.java index 33ca650fb8f..5984b00a015 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/views/IntegerEditTextPreference.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/views/IntegerEditTextPreference.java @@ -25,6 +25,10 @@ public class IntegerEditTextPreference extends EditTextPreference { @Override protected boolean persistString(String value) { - return persistInt(Integer.valueOf(value)); + try { + return persistInt(Integer.valueOf(value)); + } catch (NumberFormatException e) { + return false; + } } }
getResultImportUrls(String resultProviderName) { - List result = new ArrayList (); + final List result = new ArrayList (); ResultUrlProvider urlBasedScoreCorrectionProvider = getUrlBasedScoreCorrectionProvider(resultProviderName); ResultUrlRegistry resultUrlRegistry = getResultUrlRegistry(); if (urlBasedScoreCorrectionProvider != null) { - Iterable allUrls = resultUrlRegistry.getResultUrls(resultProviderName); - final Subject subject = SecurityUtils.getSubject(); - for (URL url : allUrls) { - if (subject - .isPermitted(SecuredDomainType.RESULT_IMPORT_URL.getStringPermissionForTypeRelativeIdentifier( - DefaultActions.READ, new TypeRelativeObjectIdentifier( - urlBasedScoreCorrectionProvider.getName(), url.toString())))) { - result.add(url.toString()); - } + Iterable allUrlsReadableBySubject = resultUrlRegistry.getResultUrls(resultProviderName); + for (URL url : allUrlsReadableBySubject) { + result.add(url.toString()); } } return result; diff --git a/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF b/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF index d1660d4b80e..c56c59907a5 100644 --- a/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.resultimport/META-INF/MANIFEST.MF @@ -12,8 +12,12 @@ Export-Package: com.sap.sailing.resultimport, com.sap.sailing.resultimport.impl Import-Package: com.sap.sailing.competitorimport, com.sap.sailing.domain.common, + com.sap.sailing.domain.common.security, com.sap.sailing.domain.persistence, com.sap.sailing.server.interfaces, + com.sap.sse.security.shared, + org.apache.shiro;version="1.2.2", + org.apache.shiro.subject;version="1.2.2", org.osgi.framework;version="1.5.0", org.osgi.util.tracker;version="1.5.1" Require-Bundle: com.sap.sse.common diff --git a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/ResultUrlRegistry.java b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/ResultUrlRegistry.java index 73243174f3e..7641c1e152a 100644 --- a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/ResultUrlRegistry.java +++ b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/ResultUrlRegistry.java @@ -2,14 +2,43 @@ package com.sap.sailing.resultimport; import java.net.URL; +import org.apache.shiro.subject.Subject; + +import com.sap.sailing.domain.common.security.SecuredDomainType; +import com.sap.sse.security.shared.HasPermissions.DefaultActions; + /** * A registry for result URL's of score correction providers * @author Frank */ public interface ResultUrlRegistry { + /** + * Registers the URL {@code url} for the result provider named {@code resultProviderName}. Unlike the + * {@link #getResultUrls(String)} method, here no security check is performed. The caller is expected to assert that + * the calling {@link Subject} has the permission {@link SecuredDomainType#RESULT_IMPORT_URL + * RESULT_IMPORT_URL}.{@link DefaultActions#CREATE CREATE} after setting the ownership. Implementing this + * check properly is done best with the {@code SecurityService} which is not visible from this bundle. + */ void registerResultUrl(String resultProviderName, URL url); + + /** + * Registers the URL {@code url} from the result provider named {@code resultProviderName}. Unlike the + * {@link #getResultUrls(String)} method, here no security check is performed. The caller is expected to assert that + * the calling {@link Subject} has the permission {@link SecuredDomainType#RESULT_IMPORT_URL + * RESULT_IMPORT_URL}.{@link DefaultActions#DELETE DELETE} after setting the ownership. Implementing this + * check properly is done best with the {@code SecurityService} which is not visible from this bundle. + */ void unregisterResultUrl(String resultProviderName, URL url); + /** + * Obtains the result import URLs registered for the provider named {@code resultProviderName}. If no such URLs are + * found, an empty iterable is returned. The URLs returned are filtered by the calling {@link Subject}'s permission + * to The URLs returned are filtered by the calling {@link Subject}'s permission + * to {@link SecuredDomainType#RESULT_IMPORT_URL RESULT_IMPORT_URL}.{@link DefaultActions#READ READ} them. them. + * + * @return an always valid, non-{@code null} iterable + */ Iterable getResultUrls(String resultProviderName); + Iterable getResultProviderNames(); } diff --git a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryImpl.java b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryImpl.java index b57ee771e9f..3273b01beb5 100644 --- a/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryImpl.java +++ b/java/com.sap.sailing.resultimport/src/com/sap/sailing/resultimport/impl/ResultUrlRegistryImpl.java @@ -1,14 +1,22 @@ package com.sap.sailing.resultimport.impl; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.Map; import java.util.Set; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; + +import com.sap.sailing.domain.common.security.SecuredDomainType; import com.sap.sailing.domain.persistence.DomainObjectFactory; import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.resultimport.ResultUrlRegistry; +import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.shared.TypeRelativeObjectIdentifier; public class ResultUrlRegistryImpl implements ResultUrlRegistry { private final Map > resultUrls; @@ -41,9 +49,18 @@ public class ResultUrlRegistryImpl implements ResultUrlRegistry { @Override public Iterable getResultUrls(String resultProviderName) { - Iterable result; - if(resultUrls.containsKey(resultProviderName)) { - result = resultUrls.get(resultProviderName); + final Subject subject = SecurityUtils.getSubject(); + final Iterable result; + if (resultUrls.containsKey(resultProviderName)) { + result = new ArrayList<>(resultUrls.get(resultProviderName)); + for (final Iterator urlIterator = result.iterator(); urlIterator.hasNext();) { + final URL url = urlIterator.next(); + if (!subject.isPermitted(SecuredDomainType.RESULT_IMPORT_URL + .getStringPermissionForTypeRelativeIdentifier(DefaultActions.READ, + new TypeRelativeObjectIdentifier(resultProviderName, url.toString())))) { + urlIterator.remove(); + } + } } else { result = Collections.emptySet(); } diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/event/TrackedEventsApi.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/event/TrackedEventsApi.java new file mode 100644 index 00000000000..1483d46e4b5 --- /dev/null +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/event/TrackedEventsApi.java @@ -0,0 +1,172 @@ +package com.sap.sailing.selenium.api.event; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.sap.sailing.selenium.api.core.ApiContext; +import com.sap.sailing.selenium.api.core.JsonWrapper; +import com.sap.sse.common.Util.Triple; + +public class TrackedEventsApi { + + private static final String TRACKED_EVENTS_URL = "/api/v1/trackedevents/"; + private static final String KEY_TRACKED_EVENTS = "trackedEvents"; + + private static final String KEY_LEADERBOARD_NAME = "leaderboardName"; + private static final String KEY_EVENT_ID = "eventId"; + private static final String KEY_EVENT_IS_ARCHIVED = "isArchived"; + private static final String KEY_EVENT_NAME = "name"; + private static final String KEY_EVENT_START = "start"; + private static final String KEY_EVENT_END = "end"; + private static final String KEY_EVENT_BASE_URL = "url"; + private static final String KEY_EVENT_IS_OWNER = "isOwner"; + private static final String KEY_EVENT_REGATTA_SECRET = "regattaSecret"; + + private static final String KEY_EVENT_TRACKED_ELEMENTS = "trackedElements"; + + private static final String KEY_TRACKED_ELEMENT_DEVICE_ID = "deviceId"; + private static final String KEY_TRACKED_ELEMENT_COMPETITOR_ID = "competitorId"; + private static final String KEY_TRACKED_ELEMENT_BOAT_ID = "boatId"; + private static final String KEY_TRACKED_ELEMENT_MARK_ID = "markId"; + + private static final String KEY_QUERY_INCLUDE_ARCHIVED = "includeArchived"; + + public TrackedEvents getTrackedEvents(ApiContext ctx, boolean isArchived) { + final Map queryParams = new HashMap<>(); + queryParams.put(KEY_QUERY_INCLUDE_ARCHIVED, Boolean.toString(isArchived)); + return new TrackedEvents(ctx.get(TRACKED_EVENTS_URL, queryParams)); + } + + public void updateOrCreateTrackedEvent(ApiContext ctx, String eventId, String leaderboardName, String eventBaseUrl, + String deviceId, Set > trackedIds, String regattaSecret) { + final JSONObject json = new JSONObject(); + json.put(KEY_EVENT_BASE_URL, eventBaseUrl); + + final JSONArray elements = new JSONArray(); + for (Triple trackedId : trackedIds) { + final JSONObject elem = new JSONObject(); + elem.put(KEY_TRACKED_ELEMENT_DEVICE_ID, deviceId); + if (trackedId.getA() != null) { + elem.put(KEY_TRACKED_ELEMENT_COMPETITOR_ID, trackedId.getA()); + } else if (trackedId.getB() != null) { + elem.put(KEY_TRACKED_ELEMENT_BOAT_ID, trackedId.getB()); + } else if (trackedId.getC() != null) { + elem.put(KEY_TRACKED_ELEMENT_MARK_ID, trackedId.getC()); + } + elements.add(elem); + } + + json.put(KEY_EVENT_REGATTA_SECRET, regattaSecret); + json.put(KEY_EVENT_TRACKED_ELEMENTS, elements); + json.put(KEY_EVENT_IS_ARCHIVED, false); + ctx.put(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName, new HashMap<>(), json); + } + + public void setArchived(ApiContext ctx, String eventId, String leaderboardName, boolean archived) { + final HashMap formParams = new HashMap<>(); + formParams.put(KEY_QUERY_INCLUDE_ARCHIVED, Boolean.toString(archived)); + ctx.post(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName, new HashMap (), formParams); + } + + public void deleteEventTrackings(ApiContext ctx, String eventId, String leaderboardName) { + ctx.delete(TRACKED_EVENTS_URL + eventId + "/" + leaderboardName); + } + + public class TrackedElement extends JsonWrapper { + public TrackedElement(JSONObject json) { + super(json); + } + + public String getCompetitorId() { + return get(KEY_TRACKED_ELEMENT_COMPETITOR_ID); + } + + public String getBoatId() { + return get(KEY_TRACKED_ELEMENT_BOAT_ID); + } + + public String getMarkId() { + return get(KEY_TRACKED_ELEMENT_MARK_ID); + } + + public String getDeviceId() { + return get(KEY_TRACKED_ELEMENT_DEVICE_ID); + } + } + + public class TrackedEvents extends JsonWrapper { + public TrackedEvents(JSONObject json) { + super(json); + } + + public Iterable getEvents() { + final Collection events = new ArrayList<>(); + final JSONArray array = get(KEY_TRACKED_EVENTS); + for (final Object json : array) { + events.add(new TrackedEvent((JSONObject) json)); + } + return events; + } + } + + public class TrackedEvent extends JsonWrapper { + + public TrackedEvent(JSONObject json) { + super(json); + } + + public String getEventId() { + return get(KEY_EVENT_ID); + } + + public String getEventName() { + return get(KEY_EVENT_NAME); + } + + public String getEventStart() { + return get(KEY_EVENT_START); + } + + public String getEventEnd() { + return get(KEY_EVENT_END); + } + + public String getEventBaseUrl() { + return get(KEY_EVENT_BASE_URL); + } + + public boolean isArchived() { + return get(KEY_EVENT_IS_ARCHIVED); + } + + public boolean isOwner() { + return get(KEY_EVENT_IS_OWNER); + } + + public Iterable getTrackedElements() { + final JSONArray array = get(KEY_EVENT_TRACKED_ELEMENTS); + final Collection col = new ArrayList<>(); + if (array != null) { + for (final Object w : array) { + col.add(new TrackedElement((JSONObject) w)); + } + } + return col; + } + + public String getLeaderboardName() { + return get(KEY_LEADERBOARD_NAME); + } + + public String getRegattaSecret() { + return get(KEY_EVENT_REGATTA_SECRET); + } + } + +} diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/regatta/RegattaApi.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/regatta/RegattaApi.java index 1e9fee55738..ad892d30766 100644 --- a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/regatta/RegattaApi.java +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/regatta/RegattaApi.java @@ -14,7 +14,9 @@ public class RegattaApi { private static final String REGATTAS = "/api/v1/regattas"; private static final String LIST_REGATTA_RACES = "/races"; - private static final String COMPETITOR_CREATE_AND_ADD_WITH_BOAT = "/competitors/createandadd"; + private static final String COMPETITORS = "/competitors"; + private static final String COMPETITOR_CREATE_AND_ADD_WITH_BOAT = COMPETITORS + "/createandadd"; + private static final String ADD = "/add"; private static final String ADD_RACE_COLUMN_URL = "/addracecolumns"; private static final String LIST_COMPETITORS = "/competitors"; @@ -87,5 +89,12 @@ public class RegattaApi { } return result; } + + public void addCompetitor(ApiContext ctx, String regattaName, UUID competitorId, Optional secret) { + final String url = REGATTAS + "/" + regattaName + COMPETITORS + "/" + competitorId + ADD; + final Map queryParams = new TreeMap<>(); + queryParams.put("secret", secret.orElse(null)); + ctx.post(url, queryParams); + } } diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/RegattaApiTest.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/RegattaApiTest.java index c6030a7845e..ba2bfa470db 100644 --- a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/RegattaApiTest.java +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/RegattaApiTest.java @@ -3,39 +3,54 @@ package com.sap.sailing.selenium.api.test; import static com.sap.sailing.domain.common.CompetitorRegistrationType.CLOSED; import static com.sap.sailing.selenium.api.core.ApiContext.SERVER_CONTEXT; import static com.sap.sailing.selenium.api.core.ApiContext.createAdminApiContext; +import static com.sap.sailing.selenium.api.core.ApiContext.createApiContext; +import static com.sap.sailing.selenium.pages.adminconsole.AdminConsolePage.goToPage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.util.Optional; +import java.util.UUID; + import org.json.simple.JSONArray; import org.json.simple.JSONObject; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import com.sap.sailing.domain.common.CompetitorRegistrationType; import com.sap.sailing.selenium.api.core.ApiContext; import com.sap.sailing.selenium.api.event.EventApi; +import com.sap.sailing.selenium.api.event.EventApi.Event; import com.sap.sailing.selenium.api.event.LeaderboardApi; +import com.sap.sailing.selenium.api.event.SecurityApi; +import com.sap.sailing.selenium.api.event.UserGroupApi; +import com.sap.sailing.selenium.api.event.UserGroupApi.UserGroup; import com.sap.sailing.selenium.api.regatta.Competitor; import com.sap.sailing.selenium.api.regatta.RaceColumn; import com.sap.sailing.selenium.api.regatta.Regatta; import com.sap.sailing.selenium.api.regatta.RegattaApi; import com.sap.sailing.selenium.api.regatta.RegattaRaces; +import com.sap.sailing.selenium.pages.adminconsole.AdminConsolePage; import com.sap.sailing.selenium.test.AbstractSeleniumTest; public class RegattaApiTest extends AbstractSeleniumTest { private static String EVENT_NAME = " loggingsession"; + private static String OTHER_EVENT_NAME = " loggingsession"; private static String BOAT_CLASS = "75QMNATIONALEKREUZER"; private final EventApi eventApi = new EventApi(); private final RegattaApi regattaApi = new RegattaApi(); private final LeaderboardApi leaderboardApi = new LeaderboardApi(); + private final UserGroupApi usergroupApi = new UserGroupApi(); @Before public void setUp() { clearState(getContextRoot(), /* headless */ true); + super.setUp(); } @Test @@ -73,8 +88,8 @@ public class RegattaApiTest extends AbstractSeleniumTest { RegattaRaces regattaRaces = regattaApi.getRegattaRaces(ctx, EVENT_NAME); assertEquals("read: regatta is different", EVENT_NAME, regattaRaces.getRegattaName()); - // assertEquals("read: reagtta.series should have 0 entries", 0, regattaRaces.getRaces().length); - + //assertEquals("read: reagtta.series should have 0 entries", 0, regattaRaces.getRaces().length); + RaceColumn[] raceColumns = regattaApi.addRaceColumn(ctx, EVENT_NAME, "R", 1); leaderboardApi.startRaceLogTracking(ctx, EVENT_NAME, raceColumns[0].getRaceName(), "Default"); regattaRaces = regattaApi.getRegattaRaces(ctx, EVENT_NAME); @@ -136,4 +151,81 @@ public class RegattaApiTest extends AbstractSeleniumTest { // assertEquals("read: racecolumn.seriesname is different", "Default", result[4].getSeriesName()); // assertEquals("read: racecolumn.racename is different", "T5", result[4].getRaceName()); } + + @Test + public void testRegisterExistingCompetitorWithSecret() { + final AdminConsolePage adminConsole = goToPage(getWebDriver(), getContextRoot()); + adminConsole.goToLocalServerPanel().setSelfServiceServer(true); + + SecurityApi securityApi = new SecurityApi(); + final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT); + securityApi.createUser(adminSecurityCtx, "donald", "Donald Duck", null, "daisy0815"); + final ApiContext userWithCompetitorCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "donald", "daisy0815"); + final ApiContext userWithCompetitorAndSecurityCtx = createApiContext(getContextRoot(), + ApiContext.SECURITY_CONTEXT, "donald", "daisy0815"); + securityApi.createUser(adminSecurityCtx, "dagobert", "Dagobert Duck", null, "daisy1337"); + final ApiContext userOwningEventCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "dagobert", "daisy1337"); + + // TODO we currently can't just create a competitor using the API. This is only possible by using a temporary Event/Regatta. + eventApi.createEvent(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, CLOSED, "default"); + Competitor competitor = regattaApi.createAndAddCompetitor(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, null, "donald", "USA"); + + // ensure the reader can actually see the competitor + UserGroup group = usergroupApi.getUserGroupByName(userWithCompetitorAndSecurityCtx, "donald-tenant"); + usergroupApi.addRoleToGroup(userWithCompetitorAndSecurityCtx, group.getGroupId(), + UUID.fromString(/* sailing viewer role id */"c42948df-517b-45cb-9fa9-d1e79f18e115"), true); + + // This is the event to register the existing competitor for + Event eventToRegisterExistingCompetitor = eventApi.createEvent(userOwningEventCtx, OTHER_EVENT_NAME, BOAT_CLASS, CompetitorRegistrationType.OPEN_UNMODERATED, "default"); + regattaApi.addCompetitor(userWithCompetitorCtx, OTHER_EVENT_NAME, competitor.getId(), Optional.of(eventToRegisterExistingCompetitor.getSecret())); + + Competitor[] competitors = regattaApi.getCompetitors(userOwningEventCtx, OTHER_EVENT_NAME); + + assertEquals(1, competitors.length); + assertEquals(competitor.getId(), competitors[0].getId()); + } + + @Test + public void testRegisterExistingCompetitorWithBadSecret() { + final AdminConsolePage adminConsole = goToPage(getWebDriver(), getContextRoot()); + adminConsole.goToLocalServerPanel().setSelfServiceServer(true); + + SecurityApi securityApi = new SecurityApi(); + final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT); + securityApi.createUser(adminSecurityCtx, "donald", "Donald Duck", null, "daisy0815"); + final ApiContext userWithCompetitorCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "donald", + "daisy0815"); + securityApi.createUser(adminSecurityCtx, "dagobert", "Dagobert Duck", null, "daisy1337"); + final ApiContext userOwningEventCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "dagobert", + "daisy1337"); + + final ApiContext userWithCompetitorAndSecurityCtx = createApiContext(getContextRoot(), + ApiContext.SECURITY_CONTEXT, "donald", "daisy0815"); + + // TODO we currently can't just create a competitor using the API. This is only possible by using a temporary + // Event/Regatta. + eventApi.createEvent(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, CLOSED, "default"); + Competitor competitor = regattaApi.createAndAddCompetitor(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, null, + "donald", "USA"); + + // ensure the reader can actually see the competitor + UserGroup group = usergroupApi.getUserGroupByName(userWithCompetitorAndSecurityCtx, "donald-tenant"); + usergroupApi.addRoleToGroup(userWithCompetitorAndSecurityCtx, group.getGroupId(), + UUID.fromString(/* sailing viewer role id */"c42948df-517b-45cb-9fa9-d1e79f18e115"), true); + + // This is the event to register the existing competitor for + Event eventToRegisterExistingCompetitor = eventApi.createEvent(userOwningEventCtx, OTHER_EVENT_NAME, BOAT_CLASS, + CompetitorRegistrationType.OPEN_UNMODERATED, "default"); + try { + regattaApi.addCompetitor(userWithCompetitorCtx, OTHER_EVENT_NAME, competitor.getId(), + Optional.of(eventToRegisterExistingCompetitor.getSecret() + "bad")); + Assert.fail("Expected error because of bad secret."); + } catch (RuntimeException e) { + Assert.assertTrue("Exepcted HTTP 401 - Unauthorized", e.getMessage().contains("rc=401")); + } + + Competitor[] competitors = regattaApi.getCompetitors(userOwningEventCtx, OTHER_EVENT_NAME); + + assertEquals(0, competitors.length); + } } diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/TrackedEventsResourceApiTest.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/TrackedEventsResourceApiTest.java new file mode 100644 index 00000000000..8fb509279a9 --- /dev/null +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/api/test/TrackedEventsResourceApiTest.java @@ -0,0 +1,332 @@ +package com.sap.sailing.selenium.api.test; + +import static com.sap.sailing.selenium.api.core.ApiContext.SERVER_CONTEXT; +import static com.sap.sailing.selenium.api.core.ApiContext.createAdminApiContext; +import static com.sap.sailing.selenium.api.core.ApiContext.createApiContext; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.common.CompetitorRegistrationType; +import com.sap.sailing.selenium.api.core.ApiContext; +import com.sap.sailing.selenium.api.event.EventApi; +import com.sap.sailing.selenium.api.event.EventApi.Event; +import com.sap.sailing.selenium.api.event.SecurityApi; +import com.sap.sailing.selenium.api.event.TrackedEventsApi; +import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedElement; +import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedEvent; +import com.sap.sailing.selenium.api.event.TrackedEventsApi.TrackedEvents; +import com.sap.sailing.selenium.test.AbstractSeleniumTest; +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Triple; + +public class TrackedEventsResourceApiTest extends AbstractSeleniumTest { + + private final TrackedEventsApi trackedEventsApi = new TrackedEventsApi(); + + private final EventApi eventApi = new EventApi(); + private final SecurityApi securityApi = new SecurityApi(); + + @Before + public void setUp() { + clearState(getContextRoot()); + } + + @Test + public void testGetTrackedEventsEmpty() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true); + Assert.assertNotNull("Tracked Events element should not be null", trackedEvents); + Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents())); + } + + @Test + public void testCreateMultipleTrackingEventsAndEditOne() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + final String competitorId = UUID.randomUUID().toString(); + final String boatId = UUID.randomUUID().toString(); + final String markId = UUID.randomUUID().toString(); + + final String eventBaseUrl = "testUrl"; + final String deviceId = UUID.randomUUID().toString(); + + final Event evt = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(), + "75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId = evt.getId(); + final String leaderboardName = evt.getName(); + final String regattaSecret = evt.getSecret(); + + final Event evt2 = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(), + "75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId2 = evt2.getId(); + final String leaderboardName2 = evt2.getName(); + final String regattaSecret2 = evt.getSecret(); + + final Set > trackedIds = new HashSet<>(); + trackedIds.add(new Triple<>(competitorId, null, null)); + trackedIds.add(new Triple<>(null, boatId, null)); + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId, + trackedIds, regattaSecret); + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId2, leaderboardName2, eventBaseUrl, deviceId, + new HashSet<>(Arrays.asList(new Triple<>(null, null, markId))), regattaSecret2); + + trackedEventsApi.setArchived(adminCtx, evt.getId(), leaderboardName, true); + trackedEventsApi.setArchived(adminCtx, evt2.getId(), leaderboardName2, false); + // check if created event is still there + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, false); + + int cntEvents = 0; + for (final TrackedEvent event : trackedEvents.getEvents()) { + cntEvents++; + Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl()); + if (eventId.equals(event.getEventId())) { + Assert.assertEquals("Unexpected event ID", eventId, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret()); + Assert.fail("Expected event 1 to not be shown anymore."); + } else if (eventId2.equals(event.getEventId())) { + Assert.assertEquals("Unexpected event ID", eventId2, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName2, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret2, event.getRegattaSecret()); + } else { + Assert.fail("Invalid event id."); + } + + int cntElements = 0; + for (final TrackedElement elem : event.getTrackedElements()) { + cntElements++; + final boolean correctBoatId = boatId.equals(elem.getBoatId()); + final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId()); + final boolean correctMarkId = markId.equals(elem.getMarkId()); + + Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId()); + Assert.assertTrue("More than one or zero items tracked in this element", + correctBoatId ^ correctCompetitorId ^ correctMarkId); + } + Assert.assertEquals("Invalid numer of elements in this event", 1, cntElements); + } + + Assert.assertEquals("Expected 1 events", 1, cntEvents); + } + + @Test + public void testCreateMultipleTrackingEvents() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + final String competitorId = UUID.randomUUID().toString(); + final String boatId = UUID.randomUUID().toString(); + final String markId = UUID.randomUUID().toString(); + + final String eventBaseUrl = "testUrl"; + final String deviceId = UUID.randomUUID().toString(); + + final Event evt = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(), + "75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId = evt.getId(); + final String leaderboardName = evt.getName(); + final String regattaSecret = evt.getSecret(); + + final Event evt2 = eventApi.createEvent(adminCtx, "TestEvent-" + UUID.randomUUID().toString(), + "75QMNATIONALEKREUZER", CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId2 = evt2.getId(); + final String leaderboardName2 = evt2.getName(); + final String regattaSecret2 = evt2.getSecret(); + + final Set > trackedIds = new HashSet<>(); + trackedIds.add(new Triple<>(competitorId, null, null)); + trackedIds.add(new Triple<>(null, boatId, null)); + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId, + trackedIds, regattaSecret); + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId2, leaderboardName2, eventBaseUrl, deviceId, + new HashSet<>(Arrays.asList(new Triple<>(null, null, markId))), regattaSecret2); + + // check if created event is still there + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true); + + int cntEvents = 0; + for (final TrackedEvent event : trackedEvents.getEvents()) { + cntEvents++; + Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl()); + if (eventId.equals(event.getEventId())) { + Assert.assertEquals("Unexpected event ID", eventId, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret()); + } else if (eventId2.equals(event.getEventId())) { + Assert.assertEquals("Unexpected event ID", eventId2, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName2, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret2, event.getRegattaSecret()); + } else { + Assert.fail("Invalid event id."); + } + + int cntElements = 0; + for (final TrackedElement elem : event.getTrackedElements()) { + cntElements++; + final boolean correctBoatId = boatId.equals(elem.getBoatId()); + final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId()); + final boolean correctMarkId = markId.equals(elem.getMarkId()); + + Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId()); + Assert.assertTrue("More than one or zero items tracked in this element", + correctBoatId ^ correctCompetitorId ^ correctMarkId); + } + Assert.assertEquals("Invalid numer of elements in this event", eventId.equals(event.getEventId()) ? 2 : 1, + cntElements); + } + + Assert.assertEquals("Expected 2 events", 2, cntEvents); + } + + @Test + public void testCreateMultipleTrackingsForSameEvent() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + final String eventName = "TestEvent-" + UUID.randomUUID().toString(); + final String competitorId = UUID.randomUUID().toString(); + final String boatId = UUID.randomUUID().toString(); + final String markId = UUID.randomUUID().toString(); + + final String eventBaseUrl = "testUrl"; + final String deviceId = UUID.randomUUID().toString(); + + final Event evt = eventApi.createEvent(adminCtx, eventName, "75QMNATIONALEKREUZER", + CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId = evt.getId(); + final String leaderboardName = evt.getName(); + final String regattaSecret = evt.getSecret(); + + final Set > trackedIds = new HashSet<>(); + trackedIds.add(new Triple<>(competitorId, null, null)); + trackedIds.add(new Triple<>(null, boatId, null)); + trackedIds.add(new Triple<>(null, null, markId)); + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId, + trackedIds, regattaSecret); + + // check if created event is still there + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true); + + int cntEvents = 0; + for (final TrackedEvent event : trackedEvents.getEvents()) { + cntEvents++; + Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl()); + Assert.assertEquals("Unexpected event ID", eventId, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret()); + + int cntElements = 0; + for (final TrackedElement elem : event.getTrackedElements()) { + cntElements++; + final boolean correctBoatId = boatId.equals(elem.getBoatId()); + final boolean correctCompetitorId = competitorId.equals(elem.getCompetitorId()); + final boolean correctMarkId = markId.equals(elem.getMarkId()); + + Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId()); + Assert.assertTrue("More than one or zero items tracked in this element", + correctBoatId ^ correctCompetitorId ^ correctMarkId); + } + Assert.assertEquals("Expected 3 event elements", 3, cntElements); + } + + Assert.assertEquals("Expected exactly 1 event", 1, cntEvents); + } + + @Test + public void testCreateTrackedEventWithCompetitorTracking() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + testCreateTrackedEventWithTracking(UUID.randomUUID().toString(), null, null, adminCtx, + "TestEvent-" + UUID.randomUUID().toString()); + } + + @Test + public void testCreateTrackedEventWithBoatTracking() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + testCreateTrackedEventWithTracking(null, UUID.randomUUID().toString(), null, adminCtx, + "TestEvent-" + UUID.randomUUID().toString()); + } + + @Test + public void testCreateTrackedEventWithMarkTracking() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + testCreateTrackedEventWithTracking(null, null, UUID.randomUUID().toString(), adminCtx, + "TestEvent-" + UUID.randomUUID().toString()); + } + + private String testCreateTrackedEventWithTracking(final String competitorId, final String boatId, + final String markId, final ApiContext adminCtx, final String eventName) { + final String eventBaseUrl = "testUrl"; + final String deviceId = UUID.randomUUID().toString(); + + final Event evt = eventApi.createEvent(adminCtx, eventName, "75QMNATIONALEKREUZER", + CompetitorRegistrationType.CLOSED, "Mannheim"); + final String eventId = evt.getId(); + final String leaderboardName = evt.getName(); + final String regattaSecret = evt.getSecret(); + + trackedEventsApi.updateOrCreateTrackedEvent(adminCtx, eventId, leaderboardName, eventBaseUrl, deviceId, + new HashSet<>(Arrays.asList(new Triple<>(competitorId, boatId, markId))), regattaSecret); + + // check if created event is still there + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true); + + boolean hasEvents = false; + for (final TrackedEvent event : trackedEvents.getEvents()) { + hasEvents = true; + Assert.assertEquals("Unexpected event base url", eventBaseUrl, event.getEventBaseUrl()); + Assert.assertEquals("Unexpected event ID", eventId, event.getEventId()); + Assert.assertEquals("Unexpected leaderboard name", leaderboardName, event.getLeaderboardName()); + Assert.assertEquals("Unexpected regatta secret", regattaSecret, event.getRegattaSecret()); + + boolean hasElements = false; + for (final TrackedElement elem : event.getTrackedElements()) { + hasElements = true; + Assert.assertEquals("Unexpected boat ID", boatId, elem.getBoatId()); + Assert.assertEquals("Unexpected competitor ID", competitorId, elem.getCompetitorId()); + Assert.assertEquals("Unexpected device ID", deviceId, elem.getDeviceId()); + Assert.assertEquals("Unexpected mark ID", markId, elem.getMarkId()); + } + Assert.assertTrue("Expected at least one element", hasElements); + } + + Assert.assertTrue("Expected at least one event", hasEvents); + return eventId; + } + + @Test + public void testDeleteEvent() { + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + final String leaderboardName = "TestEvent-" + UUID.randomUUID().toString(); + final String eventId = testCreateTrackedEventWithTracking(UUID.randomUUID().toString(), null, null, adminCtx, + leaderboardName); + + trackedEventsApi.deleteEventTrackings(adminCtx, eventId, leaderboardName); + + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(adminCtx, true); + Assert.assertNotNull("Tracked Events element should not be null", trackedEvents); + Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents())); + } + + @Test + public void testMultipleUsers() { + + // create test user + final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT); + securityApi.createUser(adminSecurityCtx, "tuser", "Test User", null, "testuser"); + final ApiContext userCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "tuser", "testuser"); + + final ApiContext adminCtx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT); + + // add an event in the user context of admin user + testCreateTrackedEventWithTracking(null, UUID.randomUUID().toString(), null, adminCtx, + "TestEvent-" + UUID.randomUUID().toString()); + + // check if event is invisible to the other user + final TrackedEvents trackedEvents = trackedEventsApi.getTrackedEvents(userCtx, true); + Assert.assertNotNull("Tracked Events element should not be null", trackedEvents); + Assert.assertTrue("Expected empty list of tracked events", Util.isEmpty(trackedEvents.getEvents())); + } + +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java index 9c4d4653eb5..37b72572b01 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java @@ -349,13 +349,17 @@ public class RegattasResource extends AbstractSailingServerResource { @Produces("application/json;charset=UTF-8") @Path("{regattaname}/competitors/{competitorid}/add") public Response addCompetitor(@PathParam("regattaname") String regattaName, - @PathParam("competitorid") String competitorIdAsString) { + @PathParam("competitorid") String competitorIdAsString, + @QueryParam("secret") String registrationLinkSecret) { Response response; Regatta regatta = findRegattaByName(regattaName); if (regatta == null) { response = getBadRegattaErrorResponse(regattaName); } else { - getSecurityService().checkCurrentUserUpdatePermission(regatta); + boolean skipPermissionCheck = getService().skipChecksDueToCorrectSecret(regattaName, registrationLinkSecret); + if (!skipPermissionCheck) { + getSecurityService().checkCurrentUserUpdatePermission(regatta); + } Serializable competitorId; try { competitorId = UUID.fromString(competitorIdAsString); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java index 083cd0942be..66469183084 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java @@ -35,6 +35,7 @@ public class RestApiApplication extends Application { classes.add(TagsResource.class); classes.add(UserGroupsResource.class); classes.add(StatusResource.class); + classes.add(TrackedEventsResource.class); // Exception Mappers classes.add(ShiroAuthorizationExceptionTo401ResponseMapper.class); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/TrackedEventsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/TrackedEventsResource.java new file mode 100644 index 00000000000..1c13c74af26 --- /dev/null +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/TrackedEventsResource.java @@ -0,0 +1,314 @@ +package com.sap.sailing.server.gateway.jaxrs.api; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; + +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.impl.Helpers; +import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource; +import com.sap.sailing.server.impl.preferences.model.SailingPreferences; +import com.sap.sailing.server.impl.preferences.model.TrackedElementWithDeviceId; +import com.sap.sailing.server.impl.preferences.model.TrackedEventPreference; +import com.sap.sailing.server.impl.preferences.model.TrackedEventPreferences; +import com.sap.sse.security.shared.impl.User; + +@Path("/v1/trackedevents/") +public class TrackedEventsResource extends AbstractSailingServerResource { + + private static final String KEY_QUERY_INCLUDE_ARCHIVED = "includeArchived"; + + private static final String KEY_LEADERBOARD_NAME = "leaderboardName"; + private static final String KEY_EVENT_ID = "eventId"; + private static final String KEY_EVENT_IS_ARCHIVED = "isArchived"; + private static final String KEY_EVENT_TRACKED_ELEMENTS = "trackedElements"; + private static final String KEY_EVENT_BASE_URL = "url"; + private static final String KEY_EVENT_REGATTA_SECRET = "regattaSecret"; + private static final String KEY_TRACKED_ELEMENT_DEVICE_ID = "deviceId"; + private static final String KEY_TRACKED_ELEMENT_COMPETITOR_ID = "competitorId"; + private static final String KEY_TRACKED_ELEMENT_BOAT_ID = "boatId"; + private static final String KEY_TRACKED_ELEMENT_MARK_ID = "markId"; + private static final String KEY_TRACKED_EVENTS = "trackedEvents"; + + @GET + @Produces("application/json;charset=UTF-8") + public Response getTrackedEvents(@QueryParam(KEY_QUERY_INCLUDE_ARCHIVED) String includeArchivedStr) { + + final User currentUser = getSecurityService().getCurrentUser(); + final ResponseBuilder builder; + + // check if user logged in + if (currentUser != null) { + + // load tracked events from storage + final TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(), + SailingPreferences.TRACKED_EVENTS_PREFERENCES); + + final JSONArray result = new JSONArray(); + final boolean includeArchived = Boolean.parseBoolean(includeArchivedStr); + + if (prefs != null) { + // iterate all stored tracked events + for (final TrackedEventPreference pref : prefs.getTrackedEvents()) { + + if (!includeArchived && pref.getIsArchived()) { + // skip, if event is archived and should be filtered out + continue; + } + + final JSONObject jsonEvent = new JSONObject(); + jsonEvent.put(KEY_EVENT_ID, pref.getEventId().toString()); + jsonEvent.put(KEY_EVENT_REGATTA_SECRET, pref.getRegattaSecret()); + jsonEvent.put(KEY_EVENT_BASE_URL, pref.getBaseUrl()); + jsonEvent.put(KEY_EVENT_IS_ARCHIVED, pref.getIsArchived()); + jsonEvent.put(KEY_LEADERBOARD_NAME, pref.getLeaderboardName()); + jsonEvent.put(KEY_EVENT_TRACKED_ELEMENTS, trackedElementsToJson(pref)); + result.add(jsonEvent); + } + } + + // storage was empty -> respond with empty list + final JSONObject resultEvents = new JSONObject(); + resultEvents.put(KEY_TRACKED_EVENTS, result); + final String jsonString = resultEvents.toJSONString(); + builder = Response.ok(jsonString).header("Content-Type", MediaType.APPLICATION_JSON + ";charset=UTF-8"); + } else { + builder = Response.status(Status.UNAUTHORIZED); + } + return builder.build(); + } + + private JSONArray trackedElementsToJson(final TrackedEventPreference pref) { + final JSONArray deviceIdsWithTrackedElementJson = new JSONArray(); + for (final TrackedElementWithDeviceId trackedElement : pref.getTrackedElements()) { + final JSONObject trackedElementJson = new JSONObject(); + trackedElementJson.put(KEY_TRACKED_ELEMENT_DEVICE_ID, trackedElement.getDeviceId()); + if (trackedElement.getTrackedCompetitorId() != null) { + trackedElementJson.put(KEY_TRACKED_ELEMENT_COMPETITOR_ID, + trackedElement.getTrackedCompetitorId().toString()); + } else if (trackedElement.getTrackedBoatId() != null) { + trackedElementJson.put(KEY_TRACKED_ELEMENT_BOAT_ID, trackedElement.getTrackedBoatId().toString()); + } else if (trackedElement.getTrackedMarkId() != null) { + trackedElementJson.put(KEY_TRACKED_ELEMENT_MARK_ID, trackedElement.getTrackedMarkId().toString()); + } + deviceIdsWithTrackedElementJson.add(trackedElementJson); + } + return deviceIdsWithTrackedElementJson; + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Path("{eventId}/{leaderboardName}") + public Response updateOrCreateTrackedEvent(@PathParam(KEY_EVENT_ID) String eventId, + @PathParam(KEY_LEADERBOARD_NAME) String leaderboardName, String jsonBody) { + ResponseBuilder responseBuilder = null; + final User currentUser = getSecurityService().getCurrentUser(); + + if (currentUser != null) { + + try { + final Object requestBody = JSONValue.parseWithException(jsonBody); + final JSONObject requestObject = Helpers.toJSONObjectSafe(requestBody); + final Boolean archived = (Boolean) requestObject.get(KEY_EVENT_IS_ARCHIVED); + final String baseUrl = (String) requestObject.get(KEY_EVENT_BASE_URL); + final String regattaSecret = (String) requestObject.get(KEY_EVENT_REGATTA_SECRET); + try { + final UUID uuidEvent = UUID.fromString(eventId); + final boolean isArchived = archived; + + TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(), + SailingPreferences.TRACKED_EVENTS_PREFERENCES); + + if (prefs == null) { + prefs = new TrackedEventPreferences(); + } + + final JSONArray trackedElementsJson = (JSONArray) requestObject.get(KEY_EVENT_TRACKED_ELEMENTS); + + // check that exactly one trackedElement is in JSON + if (trackedElementsJson == null || trackedElementsJson.size() == 0) { + // too few children + responseBuilder = Response.status(Status.BAD_REQUEST) + .entity("Invalid JSON body in request: Tracked element is missing."); + } else { + + // Copy tracked event preference objects to new list while removing the changed + // event/leaderboard node + final Collection prefsNew = // + StreamSupport.stream(prefs.getTrackedEvents().spliterator(), false) + .filter(pref -> !(pref.getEventId().equals(uuidEvent) + && pref.getLeaderboardName().equals(leaderboardName))) + .collect(Collectors.toList()); + + // Create new tracked elements for this event/leaderboard from JSON body + final Collection trackedElements = new ArrayList<>(); + + for (int i = 0; i < trackedElementsJson.size(); i++) { + final JSONObject jsonTrackedElement = (JSONObject) trackedElementsJson.get(i); + + // parse IDs + final String deviceId = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_DEVICE_ID); + final String competitorIdStr = (String) jsonTrackedElement + .get(KEY_TRACKED_ELEMENT_COMPETITOR_ID); + final String boatIdStr = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_BOAT_ID); + final String markIdStr = (String) jsonTrackedElement.get(KEY_TRACKED_ELEMENT_MARK_ID); + + // parse UUIDs of tracked element + final UUID competitorId = parseUUID(competitorIdStr); + final UUID boatId = parseUUID(boatIdStr); + final UUID markId = parseUUID(markIdStr); + + if (boatId != null ^ markId != null ^ competitorId != null) { + + // create TrackedElementWithID holder + final TrackedElementWithDeviceId newPrefElem = new TrackedElementWithDeviceId(deviceId, + boatId, competitorId, markId); + trackedElements.add(newPrefElem); + + } else { + // no boatId, competitorId or markId were specified + responseBuilder = Response.status(Status.BAD_REQUEST) + .entity("Invalid JSON body in request."); + } + + } + + final TrackedEventPreference newPreference = new TrackedEventPreference(uuidEvent, + leaderboardName, trackedElements, baseUrl, isArchived, regattaSecret); + prefsNew.add(newPreference); + + prefs.setTrackedEvents(prefsNew); + + getSecurityService().setPreferenceObject(currentUser.getName(), + SailingPreferences.TRACKED_EVENTS_PREFERENCES, prefs); + responseBuilder = Response.status(Status.ACCEPTED); + } + + } catch (IllegalArgumentException | ClassCastException e) { + responseBuilder = Response.status(Status.BAD_REQUEST) + .entity("Invalid or missing attributes in JSON body."); + } + } catch (ParseException | JsonDeserializationException e) { + responseBuilder = Response.status(Status.BAD_REQUEST).entity("Invalid JSON body in request."); + } + } else { + responseBuilder = Response.status(Status.UNAUTHORIZED); + } + return responseBuilder.build(); + } + + private UUID parseUUID(String potentialUUID) { + UUID uuid; + if (potentialUUID != null && !potentialUUID.isEmpty()) { + + try { + uuid = UUID.fromString(potentialUUID); + } catch (IllegalArgumentException e) { + uuid = null; + } + } else { + uuid = null; + } + return uuid; + } + + @POST + @Path("{eventId}/{leaderboardName}") + public Response updateTrackedEventsArchivedStatus(@PathParam(KEY_EVENT_ID) String eventId, + @PathParam(KEY_LEADERBOARD_NAME) String leaderboardName, + @FormParam(KEY_QUERY_INCLUDE_ARCHIVED) String archived) { + final boolean isArchived = Boolean.parseBoolean(archived); + return applyOnEventByIdAndLeadboardName(eventId, leaderboardName, + pref -> new TrackedEventPreference(pref, isArchived)); + } + + @DELETE + @Path("{eventId}/{leaderboardName}") + public Response deleteTrackedEvents(@PathParam(KEY_EVENT_ID) String eventId, + @PathParam(KEY_LEADERBOARD_NAME) String leaderboardName) { + return applyOnEventByIdAndLeadboardName(eventId, leaderboardName, pref -> null); + } + + /** Finds the tracked event with the corresponding eventId and applies the function to it. */ + private Response applyOnEventByIdAndLeadboardName(String eventId, String leaderboardName, + Function function) { + ResponseBuilder responseBuilder; + final User currentUser = getSecurityService().getCurrentUser(); + + if (currentUser != null) { + + final TrackedEventPreferences prefs = getSecurityService().getPreferenceObject(currentUser.getName(), + SailingPreferences.TRACKED_EVENTS_PREFERENCES); + try { + // parse tracked event UUID + final UUID eventUuid = UUID.fromString(eventId); + + if (prefs == null) { + // storage is empty + responseBuilder = Response.status(Status.NOT_FOUND) + .entity("No tracked events with this eventId found."); + } else { + + // iterate stored preferences until tracked event with corresponding eventId is found + boolean found = false; + final Collection newPrefs = new HashSet<>(); + for (final TrackedEventPreference pref : prefs.getTrackedEvents()) { + if (pref.getEventId().equals(eventUuid) && pref.getLeaderboardName().equals(leaderboardName)) { + // tracked event found, apply function on it and remove it if result of function execution + // is null + found = true; + final TrackedEventPreference updatedPreference = function.apply(pref); + if (updatedPreference != null) { + newPrefs.add(updatedPreference); + } + } else { + newPrefs.add(pref); + } + } + if (!found) { + // tracked event was not found -> respond with error + responseBuilder = Response.status(Status.NOT_FOUND) + .entity("No tracked events with this eventId found."); + } else { + // update and store preferences + prefs.setTrackedEvents(newPrefs); + getSecurityService().setPreferenceObject(currentUser.getName(), + SailingPreferences.TRACKED_EVENTS_PREFERENCES, prefs); + responseBuilder = Response.status(Status.ACCEPTED); + } + + } + } catch (IllegalArgumentException e) { + // eventId parameter could not be parsed + responseBuilder = Response.status(Status.BAD_REQUEST).entity("Invalid eventId."); + } + } else { + responseBuilder = Response.status(Status.UNAUTHORIZED); + } + return responseBuilder.build(); + } +} diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html index b36bfcfac64..82a2df2eab7 100644 --- a/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/index.html @@ -154,7 +154,7 @@ name to make sure you get to the event you're interested in. Creates a competitor and adds it to the regatta - /api/v1/regattas/{regattaname}/competitors/{competitorId}/add +/api/v1/regattas/{regattaname}/competitors/{competitorId}/add Registers a competitor with a regatta (POST) @@ -340,6 +340,22 @@ name to make sure you get to the event you're interested in. +/api/v1/status Returns a status description for the "RacingEventService" core service + +/api/v1/trackedevents +Gets all tracked events with the associated device-to-competitor/boat/mark elements for the current user. ++ +/api/v1/trackedevents +Create or update a tracked event for the current user. ++ +/api/v1/trackedevents +Update the archived status of a tracked event for the current user. ++ diff --git a/java/com.sap.sailing.server.gateway/webservices/api/v1/regattasCompetitorAdd.html b/java/com.sap.sailing.server.gateway/webservices/api/v1/regattasCompetitorAdd.html new file mode 100644 index 00000000000..1d833f24430 --- /dev/null +++ b/java/com.sap.sailing.server.gateway/webservices/api/v1/regattasCompetitorAdd.html @@ -0,0 +1,50 @@ + + + + + + + +/api/v1/trackedevents +Delete a tracked event for the current user. +SAP Sailing Analytics Webservices API Version 1.0 + + +SAP Sailing Analytics Webservices API Version 1.0
+URL: /api/v1/regattas/{regattaname}/competitors/{competitorId}/add
+ +Description: +Registers an existing competitor with the regatta.
+This provides functionality