Compare commits
62 Commits
master
...
fradrive/m
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d045739be | |||
| 7a5c11e457 | |||
| 507c383410 | |||
| aebf6ec914 | |||
| bccdb2494f | |||
| 36b4beb908 | |||
| 228a6254bc | |||
| 7294b9731c | |||
| 97a3845f6d | |||
| 0adc12c828 | |||
| 28c3ee5be1 | |||
| 8c91d6d37a | |||
| 0d6346ef2c | |||
| fddbf8a30b | |||
| 3d63c88c75 | |||
| c92ddb9081 | |||
| 1b71137295 | |||
| 6fcfe56626 | |||
| 030ddcac66 | |||
| 36a0bd9edc | |||
| 06fa34c938 | |||
| d4d511a02f | |||
| ec2b09b20b | |||
| 7d57a30be7 | |||
| 01c4225da4 | |||
| 4fc6f54b32 | |||
| 8506c4d7e0 | |||
| ed44edc199 | |||
| ab46577b7e | |||
| be7fc2e540 | |||
| 3960931bb5 | |||
| 56c2be7b79 | |||
| 4e171a7a1a | |||
| f642b9cccf | |||
| 72b2b6876b | |||
| c9ecb30542 | |||
| 8ddf38b904 | |||
| 21592347b4 | |||
| e625dca6ea | |||
| f17d89c21e | |||
| 5c7b4cff93 | |||
| 83fe750b15 | |||
| e29e6f3db8 | |||
| 6dd27eb848 | |||
| 4c2baa4e9f | |||
| 384c39b9ec | |||
| a262921a7d | |||
| 05638c2b51 | |||
| 3d7df8066d | |||
| 6c9d92475e | |||
| 78c645cf21 | |||
| e8b276851c | |||
| e16baedfce | |||
| d19266e918 | |||
| 53c68638da | |||
| 6e3dd1c1f3 | |||
| ba0fd21c8f | |||
| d0eb3ddf92 | |||
| 5307350b0b | |||
| 1a954e037f | |||
| faaaa18247 | |||
| 2e0455a154 |
@ -1,65 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: releaseTag
|
||||
type: string
|
||||
- name: releaseEndpoint
|
||||
type: string
|
||||
default: 'devfra'
|
||||
values:
|
||||
- 'devfra'
|
||||
- 'prodfra'
|
||||
|
||||
jobs:
|
||||
- job: Release
|
||||
displayName: Release ${{ parameters.releaseTag }}
|
||||
container:
|
||||
image: devfra.azurecr.io/de.fraport.build/tools:1.1.0
|
||||
endpoint: devfra
|
||||
steps:
|
||||
|
||||
# Download required artifacts from pipeline
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download FraDrive binaries
|
||||
inputs:
|
||||
artifactName: Build_backend
|
||||
patterns: 'Build_backend/bin/*'
|
||||
targetPath: '$(Build.Repository.LocalPath)'
|
||||
|
||||
- task: Docker@2
|
||||
displayName: Login to container registry
|
||||
inputs:
|
||||
command: login
|
||||
containerRegistry: '${{ parameters.releaseEndpoint }}'
|
||||
- task: Bash@3
|
||||
displayName: Build FraDrive container
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
cp docker/fradrive/Dockerfile .
|
||||
docker build \
|
||||
--tag $(buildImageUpstream)/fradrive:$(Build.BuildNumber) \
|
||||
--tag $(buildImageUpstream)/fradrive:${{parameters.releaseTag}} \
|
||||
--build-arg FROM_IMG=devfra.azurecr.io/de.fraport.trusted/ubuntu \
|
||||
--build-arg FROM_TAG=20.04 \
|
||||
--build-arg PROJECT_DIR=$(Build.Repository.LocalPath) \
|
||||
--build-arg IN_CI=true \
|
||||
--build-arg IN_CONTAINER=true \
|
||||
--build-arg HTTPS_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg HTTP_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg NO_PROXY='localhost,127.0.0.1,*.docker.internal,*.azmk8s.io,devfra.azurecr.io,devfra.westeurope.data.azurecr.io' \
|
||||
--build-arg FRAPORT_NOPROXY=dev.azure.com,*.dev.azure.com,*.fraport.de,*.frankfurt-airport.de \
|
||||
.
|
||||
- task: Docker@2
|
||||
displayName: Push container to registry
|
||||
inputs:
|
||||
command: push
|
||||
repository: 'de.fraport.fradrive.build/fradrive'
|
||||
tags: '$(Build.BuildNumber),${{parameters.releaseTag}}'
|
||||
- task: Docker@2
|
||||
displayName: Logout from container registry
|
||||
inputs:
|
||||
command: logout
|
||||
containerRegistry: '${{ parameters.releaseEndpoint }}'
|
||||
@ -1,61 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: serviceName
|
||||
type: string
|
||||
- name: dependenciesCaches
|
||||
type: object
|
||||
default: []
|
||||
- name: dependenciesBuildPool
|
||||
type: string
|
||||
default: 'Prod Private Agent Pool'
|
||||
values:
|
||||
- 'Prod Private Agent Pool'
|
||||
- 'Prod Private Agent Pool DS2'
|
||||
- 'Prod Private Agent Pool DS3'
|
||||
- name: dependenciesBuildCores
|
||||
type: number
|
||||
default: 1
|
||||
- name: dependenciesBuildTimeout
|
||||
type: number
|
||||
default: 60
|
||||
|
||||
jobs:
|
||||
- job: SetupDependencies_${{parameters.serviceName}}
|
||||
displayName: Install ${{parameters.serviceName}} dependencies
|
||||
dependsOn: SetupImage_${{parameters.serviceName}}
|
||||
${{ if eq(variables.setupImages, true) }}:
|
||||
condition: succeeded()
|
||||
${{ else }}:
|
||||
condition: always()
|
||||
pool: '${{parameters.dependenciesBuildPool}}'
|
||||
timeoutInMinutes: ${{parameters.dependenciesBuildTimeout}}
|
||||
container:
|
||||
${{ if variables.setupImages }}:
|
||||
image: $(buildImageUpstream)/${{parameters.serviceName}}:$(Build.BuildNumber)
|
||||
${{ else }}:
|
||||
image: $(buildImageUpstream)/${{parameters.serviceName}}:latest
|
||||
endpoint: devfra
|
||||
env:
|
||||
PROJECT_DIR: $(Build.Repository.LocalPath)
|
||||
IN_CONTAINER: true
|
||||
IN_CI: true
|
||||
steps:
|
||||
# Restore previously-built dependencies from caches
|
||||
- ${{ each cache in parameters.dependenciesCaches }}:
|
||||
- template: ./../../steps/cache.yaml
|
||||
parameters:
|
||||
cacheIdent: '${{parameters.serviceName}}-dependencies'
|
||||
cacheKeys: '${{cache.key}}'
|
||||
cachePath: '${{cache.path}}'
|
||||
|
||||
# Compile dependencies
|
||||
- template: ./../../steps/make.yaml
|
||||
parameters:
|
||||
makeJob: dependencies
|
||||
makeService: ${{parameters.serviceName}}
|
||||
makeVars: 'CPU_CORES=${{parameters.dependenciesBuildCores}} STACK_CORES=-j${{parameters.dependenciesBuildCores}}'
|
||||
|
||||
# (Note: a post-job for updating the dependency cache is automatically created, so no further step is due here.)
|
||||
@ -1,72 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: imageName
|
||||
type: string
|
||||
- name: imageBase
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- job: SetupImage_${{parameters.imageName}}
|
||||
displayName: Build ${{parameters.imageName}} image
|
||||
condition: eq(variables.setupImages, true)
|
||||
container:
|
||||
image: devfra.azurecr.io/de.fraport.build/tools:1.1.0
|
||||
endpoint: devfra
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: Login to container registry
|
||||
inputs:
|
||||
command: login
|
||||
containerRegistry: devfra
|
||||
- task: Bash@3
|
||||
displayName: Build ${{parameters.imageName}} image
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
cp docker/${{parameters.imageName}}/Dockerfile .
|
||||
docker build \
|
||||
--tag $(buildImageUpstream)/${{parameters.imageName}}:$(Build.BuildNumber) \
|
||||
--build-arg FROM_IMG=${{parameters.imageBase.image}} \
|
||||
--build-arg FROM_TAG=${{parameters.imageBase.tag}} \
|
||||
--build-arg HTTPS_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg HTTP_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg NO_PROXY='localhost,127.0.0.1,*.docker.internal,*.azmk8s.io,devfra.azurecr.io,devfra.westeurope.data.azurecr.io' \
|
||||
--build-arg FRAPORT_NOPROXY=dev.azure.com,*.dev.azure.com,*.fraport.de,*.frankfurt-airport.de \
|
||||
--build-arg PROJECT_DIR=$(Build.Repository.LocalPath) \
|
||||
--build-arg IN_CI=true \
|
||||
--build-arg IN_CONTAINER=true \
|
||||
.
|
||||
- task: Bash@3
|
||||
displayName: Push ${{parameters.imageName}} image
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
docker push $(buildImageUpstream)/${{parameters.imageName}}:$(Build.BuildNumber)
|
||||
- task: Bash@3
|
||||
displayName: Update latest ${{parameters.imageName}} image
|
||||
condition: or(eq(variables.forcePushLatest, true), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
docker tag $(buildImageUpstream)/${{parameters.imageName}}:$(Build.BuildNumber) $(buildImageUpstream)/${{parameters.imageName}}:latest
|
||||
docker push $(buildImageUpstream)/${{parameters.imageName}}:latest
|
||||
- task: Bash@3
|
||||
displayName: Save image for publication
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
docker image save --output=$(Build.ArtifactStagingDirectory)/${{parameters.imageName}}.tar $(buildImageUpstream)/${{parameters.imageName}}:$(Build.BuildNumber)
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish image as artifact
|
||||
inputs:
|
||||
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
ArtifactName: Image_${{parameters.imageName}}
|
||||
publishLocation: 'Container'
|
||||
- task: Docker@2
|
||||
displayName: Logout from container registry
|
||||
inputs:
|
||||
command: logout
|
||||
containerRegistry: devfra
|
||||
@ -1,141 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: serviceName
|
||||
type: string
|
||||
default: serviceName
|
||||
- name: serviceBase
|
||||
type: object
|
||||
default:
|
||||
image: baseImage
|
||||
tag: baseImageTag
|
||||
- name: servicePool
|
||||
type: string
|
||||
default: 'Prod Private Agent Pool'
|
||||
- name: serviceTimeout
|
||||
type: number
|
||||
default: 60
|
||||
# extraBuildOptions: ''
|
||||
- name: serviceDependsOn
|
||||
type: object
|
||||
default: []
|
||||
- name: serviceRequiredArtifacts
|
||||
type: object
|
||||
default: []
|
||||
- name: serviceArtifacts
|
||||
type: string
|
||||
default: ''
|
||||
- name: buildSteps
|
||||
type: object
|
||||
|
||||
stages:
|
||||
- stage: ${{ parameters.serviceName }}
|
||||
dependsOn: ${{ parameters.serviceDependsOn }}
|
||||
pool: '${{ parameters.servicePool }}'
|
||||
jobs:
|
||||
- job: ImageBuild_${{parameters.serviceName}}
|
||||
displayName: Build ${{parameters.serviceName}} image
|
||||
condition: or(eq(variables.forcePushLatest, true), eq(variables.onMasterBranch, true), eq(variables.onUpdateBranch, true))
|
||||
container:
|
||||
image: devfra.azurecr.io/de.fraport.build/tools:1.1.0
|
||||
endpoint: devfra
|
||||
steps:
|
||||
- checkout: self
|
||||
- task: Docker@2
|
||||
displayName: Login to container registry
|
||||
inputs:
|
||||
command: login
|
||||
containerRegistry: devFra
|
||||
- script: |
|
||||
ls -a .
|
||||
pwd
|
||||
find .
|
||||
- task: Bash@3
|
||||
displayName: Build ${{parameters.serviceName}} image
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
cp docker/${{parameters.serviceName}}/Dockerfile .
|
||||
docker build \
|
||||
--tag $(buildImageUpstream)/${{parameters.serviceName}}:$(Build.BuildNumber) \
|
||||
--build-arg FROM_IMG=${{parameters.serviceBase.image}} \
|
||||
--build-arg FROM_TAG=${{parameters.serviceBase.tag}} \
|
||||
--build-arg HTTPS_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg HTTP_PROXY=http://proxy.frankfurt-airport.de:8080 \
|
||||
--build-arg NO_PROXY='localhost,127.0.0.1,*.docker.internal,*.azmk8s.io,devfra.azurecr.io,devfra.westeurope.data.azurecr.io' \
|
||||
--build-arg FRAPORT_NOPROXY=dev.azure.com,*.dev.azure.com,*.fraport.de,*.frankfurt-airport.de \
|
||||
--build-arg PROJECT_DIR=$(Build.Repository.LocalPath) \
|
||||
--build-arg IN_CI=true \
|
||||
--build-arg IN_CONTAINER=true \
|
||||
.
|
||||
- task: Bash@3
|
||||
displayName: Push ${{ parameters.serviceName }} image
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
docker push $(buildImageUpstream)/${{parameters.serviceName}}:$(Build.BuildNumber)
|
||||
- task: Bash@3
|
||||
displayName: Update latest ${{parameters.serviceName}} image
|
||||
condition: or(eq(variables.forcePushLatest, true), eq(variables.onMasterBranch, true))
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
docker tag $(buildImageUpstream)/${{parameters.serviceName}}:$(Build.BuildNumber) $(buildImageUpstream)/${{parameters.serviceName}}:latest
|
||||
docker push $(buildImageUpstream)/${{parameters.serviceName}}:latest
|
||||
- task: Docker@2
|
||||
displayName: Logout from container registry
|
||||
inputs:
|
||||
command: logout
|
||||
containerRegistry: devFra
|
||||
|
||||
- job: Build_${{parameters.serviceName}}
|
||||
displayName: Build ${{parameters.serviceName}}
|
||||
dependsOn:
|
||||
- ImageBuild_${{parameters.serviceName}}
|
||||
condition: in(dependencies.ImageBuild_${{parameters.serviceName}}.result, 'Succeeded', 'Skipped')
|
||||
timeoutInMinutes: ${{ parameters.serviceTimeout }}
|
||||
container:
|
||||
# TODO: use BuildNumber instead of latest in update branches
|
||||
# image: devfra.azurecr.io/de.fraport.fradrive.build/frontend:$(Build.BuildNumber)
|
||||
image: $(buildImageUpstream)/${{parameters.serviceName}}:latest
|
||||
endpoint: devfra
|
||||
env:
|
||||
PROJECT_DIR: $(Build.Repository.LocalPath)
|
||||
IN_CONTAINER: true
|
||||
IN_CI: true
|
||||
steps:
|
||||
- checkout: self
|
||||
- ${{ each dependency in parameters.serviceRequiredArtifacts }}:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download artifacts from ${{ dependency.name }} dependency
|
||||
continueOnError: ${{ dependency.continueOnError }}
|
||||
condition: ${{ dependency.condition }}
|
||||
inputs:
|
||||
artifactName: ${{ dependency.artifact }}
|
||||
source: ${{ dependency.source }}
|
||||
project: 'Fahrerausbildung'
|
||||
pipeline: $(System.DefinitionId)
|
||||
buildVersionToDownload: '${{ dependency.version }}'
|
||||
tags: '${{ dependency.artifact }}'
|
||||
allowPartiallySucceededBuilds: true
|
||||
allowFailedBuilds: true
|
||||
patterns: '${{ dependency.patterns }}'
|
||||
targetPath: '$(Build.Repository.LocalPath)'
|
||||
- ${{ each buildStep in parameters.buildSteps }}:
|
||||
- template: ./service/build-step.yaml
|
||||
parameters:
|
||||
service: ${{ parameters.serviceName }}
|
||||
buildStep: ${{ buildStep }}
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy ${{parameters.serviceName}} artifacts
|
||||
inputs:
|
||||
Contents: ${{ parameters.serviceArtifacts }}
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish ${{parameters.serviceName}} artifacts
|
||||
inputs:
|
||||
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
ArtifactName: '${{parameters.serviceName}}'
|
||||
publishLocation: 'Container'
|
||||
@ -1,15 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: artifactName
|
||||
type: string
|
||||
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download artifacts from ${{parameters.artifactName}}
|
||||
inputs:
|
||||
source: 'current'
|
||||
artifactName: '${{parameters.artifactName}}'
|
||||
targetPath: '$(Build.Repository.LocalPath)'
|
||||
@ -1,18 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: cacheIdent
|
||||
type: string
|
||||
- name: cacheKeys
|
||||
type: string
|
||||
- name: cachePath
|
||||
type: string
|
||||
|
||||
steps:
|
||||
- task: Cache@2
|
||||
displayName: Restore ${{parameters.cacheIdent}} cache
|
||||
inputs:
|
||||
key: '"${{parameters.cacheIdent}}" | ${{parameters.cacheKeys}}'
|
||||
path: '${{parameters.cachePath}}'
|
||||
@ -1,35 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
parameters:
|
||||
- name: makeJob
|
||||
type: string
|
||||
values:
|
||||
- dependencies
|
||||
- compile
|
||||
- lint
|
||||
- test
|
||||
- name: makeService
|
||||
type: string
|
||||
values:
|
||||
- frontend
|
||||
- backend
|
||||
- name: makeVars
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
- task: Bash@3
|
||||
name: ${{parameters.makeJob}}_${{parameters.makeService}}
|
||||
displayName: make ${{parameters.makeJob}}-${{parameters.makeService}}
|
||||
env:
|
||||
HTTPS_PROXY: http://proxy.frankfurt-airport.de:8080
|
||||
HTTP_PROXY: http://proxy.frankfurt-airport.de:8080
|
||||
NO_PROXY: 'localhost,127.0.0.1,*.docker.internal,*.azmk8s.io,devfra.azurecr.io,devfra.westeurope.data.azurecr.io'
|
||||
FRAPORT_NOPROXY: 'dev.azure.com,*.dev.azure.com,*.fraport.de,*.frankfurt-airport.de'
|
||||
PROJECT_DIR: $(Build.Repository.LocalPath)
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
make -- --${{parameters.makeJob}}-${{parameters.makeService}} IN_CONTAINER=true IN_CI=true PROJECT_DIR=${PROJECT_DIR} ${{parameters.makeVars}}
|
||||
3
.babelrc.license
Normal file
3
.babelrc.license
Normal file
@ -0,0 +1,3 @@
|
||||
SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
3
.eslintrc.json.license
Normal file
3
.eslintrc.json.license
Normal file
@ -0,0 +1,3 @@
|
||||
SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@ -2,12 +2,9 @@
|
||||
dist*
|
||||
develop
|
||||
node_modules/
|
||||
.npm/
|
||||
.node_repl_history
|
||||
**/assets/icons
|
||||
**/assets/favicons
|
||||
assets/icons
|
||||
assets/favicons
|
||||
bin/
|
||||
assets/fonts/
|
||||
*.hi
|
||||
*.o
|
||||
*.sqlite3
|
||||
@ -42,19 +39,19 @@ src/Handler/Assist.bak
|
||||
src/Handler/Course.SnapCustom.hs
|
||||
*.orig
|
||||
/instance
|
||||
backend/instance
|
||||
.stack-work-*
|
||||
.stack-work.lock
|
||||
.directory
|
||||
tags
|
||||
test.log
|
||||
*.dump-splices
|
||||
/.stack-work.lock
|
||||
/.npmrc
|
||||
/.npm/
|
||||
/config/manifest.json
|
||||
tunnel.log
|
||||
static
|
||||
well-known
|
||||
.well-known-cache
|
||||
manifest.json
|
||||
/static
|
||||
/well-known
|
||||
/.well-known-cache
|
||||
/.nix-well-known
|
||||
/**/tmp-*
|
||||
/testdata/bigAlloc_*.csv
|
||||
@ -68,4 +65,3 @@ manifest.json
|
||||
**/result-*
|
||||
.develop.cmd
|
||||
/.vscode
|
||||
backend/.ghc/ghci_history
|
||||
|
||||
@ -6,11 +6,8 @@ use warnings;
|
||||
use Data::Dumper;
|
||||
|
||||
# Version changes:
|
||||
# [x].[y].[z] -- Main version number
|
||||
# XXX old
|
||||
# [x].[y].[z]-test-[branchstring]-[num] -- test/branch/devel version number
|
||||
# XXX new
|
||||
# [x].[y].[z]-[num]+[branchname]
|
||||
# v[x].[y].[z] -- Main version number
|
||||
# v[x].[y].[z]-test-[branchstring]-num -- test/branch/devel version number
|
||||
# on main/master: Biggest version so far, increment by occuring changes
|
||||
# on other branches: find version; be it branch string, old format or main version number;
|
||||
# increments from there. Increment version number, but on global conflict use new version number
|
||||
@ -55,12 +52,12 @@ my %parKinds = (
|
||||
},
|
||||
autokind=>{
|
||||
arity=>1,
|
||||
def=>'release/prod=v,release/*=t,*=t',
|
||||
def=>'main=v,master=v,test=t,*=t',
|
||||
help=>'determine the tag kind from branch name instead of fixed value; use the first fitting glob',
|
||||
},
|
||||
change=>{
|
||||
arity=>1,
|
||||
def=>'chore=patch,feat=minor,feature=minor,fix=patch,BREAK=major,perf=patch,refactor=patch,test=patch,style=patch,revert=patch,docs=patch,build=patch,ci=patch',
|
||||
def=>'chore=patch,feat=minor,feature=minor,fix=patch,BREAK=major,perf=patch,refactor=patch,test=patch,style=patch,revert=null,docs=patch,build=null,ci=null',
|
||||
help=>'how to react on which commit type; can be partially given. Actions are: "null", "major", "minor", "patch" or state "invalid" for removing this type',
|
||||
},
|
||||
changelog=>{
|
||||
@ -136,14 +133,11 @@ if($par{'h'}) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
my $branchNameEscaped = `$par{vcsbranch}`;
|
||||
chomp $branchNameEscaped;
|
||||
if($par{autokind}) {
|
||||
my $branch = $branchNameEscaped;
|
||||
my $branch = `$par{vcsbranch}`;
|
||||
my @rules = split /,/, $par{autokind};
|
||||
RULES: {
|
||||
for my $r(@rules) {
|
||||
warn "$0: Processing autokind rule '$r'\n" if $par{v};
|
||||
if($r!~m#(.*)=(.*)#) {
|
||||
die "$0: Bad rule in autokind: $r\n";
|
||||
}
|
||||
@ -156,18 +150,17 @@ if($par{autokind}) {
|
||||
warn "$0: No autokind rule matches; leaving the kind unchanged.\n"
|
||||
}
|
||||
}
|
||||
$branchNameEscaped =~ s/[^0-9a-zA-Z]+/-/g;
|
||||
|
||||
|
||||
if($par{'v'}) {
|
||||
warn "VERBOSE: Parameters\n";
|
||||
print "VERBOSE: Parameters\n";
|
||||
for my $k(sort keys %par) {
|
||||
warn " $k: $par{$k}\n"
|
||||
print " $k: $par{$k}\n"
|
||||
}
|
||||
}
|
||||
|
||||
my %typeReact = ();
|
||||
for my $as(split /,/, $par{change}) {
|
||||
warn "$0: processing change parameter '$as'\n" if $par{v};
|
||||
if($as=~m#(.*)=(.*)#) {
|
||||
$typeReact{$1} = $2;
|
||||
} else {
|
||||
@ -231,36 +224,47 @@ sub parseVersion {
|
||||
warn "$0: internal error (parseVersion called on undef at $c)\n";
|
||||
return undef
|
||||
}
|
||||
my %cap = ();
|
||||
if(
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)$# ||
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)\.(?<mi>[0-9]+)$# ||
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)\.(?<mi>[0-9]+)\.(?<p>[0-9]+)$# ||
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)\.(?<mi>[0-9]+)\.(?<p>[0-9]+)-test-(?<sp>(?<brn>[a-z]+)-?(?<brv>[0-9\.]+))$# ||
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)\.(?<mi>[0-9]+)\.(?<p>[0-9]+)-(?<sp>(?<brv>[0-9\.]+)\+(?<brn>[0-9A-Za-z\-]+))$# || # [x].[y].[z]-[num]+[branchname]
|
||||
$v=~m#^(?<pre>[a-z]*)(?<ma>[0-9]+)\.(?<mi>[0-9]+)\.(?<p>[0-9]+)-(?<sp>.*)$#
|
||||
) {
|
||||
%cap = %+
|
||||
# my ($pre,$ma,$mi,$p,$sp,$brn,$brv) = ();
|
||||
my ($pre,$ma,$mi,$p,$sp,$brn,$brv) = ();
|
||||
if($v=~m#^([a-z]*)([0-9]+)$#) {
|
||||
$pre = $1;
|
||||
$ma = $2;
|
||||
} elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)$#) {
|
||||
$pre = $1;
|
||||
$ma = $2;
|
||||
$mi = $3
|
||||
} elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)$#) {
|
||||
$pre = $1;
|
||||
$ma = $2;
|
||||
$mi = $3;
|
||||
$p = $4;
|
||||
} elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)-test-([a-z]+)-([0-9\.]+)$#) {
|
||||
$pre = $1;
|
||||
$ma = $2;
|
||||
$mi = $3;
|
||||
$p = $4;
|
||||
$sp = $5;
|
||||
$brn = $6;
|
||||
$brv = $7;
|
||||
} elsif($v=~m#^([a-z]*)([0-9]+)\.([0-9]+)\.([0-9]+)-(.*)$#) {
|
||||
$pre = $1;
|
||||
$ma = $2;
|
||||
$mi = $3;
|
||||
$p = $4;
|
||||
$sp = $5;
|
||||
} else {
|
||||
warn "$0: unexpected old version number: $v\n" if $par{v};
|
||||
return undef
|
||||
}
|
||||
$cap{pre} = 'v' if '' eq $cap{pre};
|
||||
my %ret = (
|
||||
prefix=>$cap{pre},
|
||||
major=>$cap{ma},
|
||||
minor=>$cap{mi},
|
||||
patch=>$cap{p},
|
||||
subpatch=>$cap{sp},
|
||||
branchname=>$cap{brn},
|
||||
branchversion=>$cap{brv},
|
||||
);
|
||||
if($par{v}) {
|
||||
my $parsed = join '; ', map { "$_=>".($ret{$_}//'') } sort keys %ret;
|
||||
warn "Version '$v' was parsed to '$parsed'\n"
|
||||
$pre = 'v' if '' eq $pre;
|
||||
return {
|
||||
prefix=>$pre,
|
||||
major=>$ma,
|
||||
minor=>$mi,
|
||||
patch=>$p,
|
||||
subpatch=>$sp,
|
||||
branchname=>$brn,
|
||||
branchversion=>$brv,
|
||||
}
|
||||
return \%ret
|
||||
}
|
||||
|
||||
#@oldVersions = sort {
|
||||
@ -294,18 +298,18 @@ sub vsCompare {
|
||||
#for($v, $w) {
|
||||
# $_ = parseVersion($_) unless ref $_;
|
||||
#}
|
||||
if($v->{prefix}=~m/^v?$/ and $w->{prefix}=~m/^v?$/) {
|
||||
if('v' eq $v->{prefix} and 'v' eq $w->{prefix}) {
|
||||
return(
|
||||
($v->{major} // 0) <=> ($w->{major} // 0) ||
|
||||
($v->{minor} // 0) <=> ($w->{minor} // 0) ||
|
||||
($v->{patch} // 0) <=> ($w->{patch} // 0) ||
|
||||
($v->{branchname} // '') cmp ($w->{branchname} // '') ||
|
||||
($v->{branchversion} // 0) <=> ($w->{branchversion} // 0) ||
|
||||
($v->{branchversion} // '') <=> ($w->{branchversion} // '') ||
|
||||
($v->{subpatch} // '') cmp ($w->{subpatch} // '')
|
||||
)
|
||||
} elsif($v->{prefix}=~m/^v?$/ and !$w->{prefix}=~m/^v?$/) {
|
||||
} elsif('v' eq $v->{prefix} and 'v' ne $w->{prefix}) {
|
||||
return 1;
|
||||
} elsif(!$v->{prefix}=~m/^v?$/ and $w->{prefix}=~m/^v?$/) {
|
||||
} elsif('v' ne $v->{prefix} and 'v' eq $w->{prefix}) {
|
||||
return -1;
|
||||
} else {
|
||||
return vsStringDebug($v) cmp vsStringDebug($w)
|
||||
@ -345,21 +349,13 @@ sub vsJustVersion {
|
||||
|
||||
sub vsTestVersion {
|
||||
my $v = shift;
|
||||
# [x].[y].[z]-[num]+[branchname]
|
||||
my $ret =
|
||||
'v' .
|
||||
($v->{major} // 0) . "." .
|
||||
($v->{minor} // 0) . "." .
|
||||
($v->{patch} // 0) . "-" .
|
||||
($v->{branchversion} // '0.0.0') . "+" .
|
||||
$branchNameEscaped;
|
||||
# old version format
|
||||
#my $ret =
|
||||
#'v' .
|
||||
#($v->{major} // 0) . "." .
|
||||
#($v->{minor} // 0) . "." .
|
||||
#($v->{patch} // 0) . "-test-" .
|
||||
#($v->{branchname} // 'a') .
|
||||
#($v->{branchversion} // '0.0.0');
|
||||
($v->{patch} // 0) . "-test-" .
|
||||
($v->{branchname} // 'a') .
|
||||
($v->{branchversion} // '0.0.0');
|
||||
return $ret
|
||||
}
|
||||
|
||||
@ -376,7 +372,6 @@ if('-' eq $par{vcslog}) {
|
||||
}
|
||||
my @versions = ();
|
||||
for my $v(@versionsOrig) {
|
||||
warn "$0: Processing orig version (part 1): '$v'\n" if $par{v};
|
||||
if($v=~m#^(.*?\S)\s*::::\s*(.*?)\s*::::\s*(.*)#) {
|
||||
push @versions, {
|
||||
hash => $1,
|
||||
@ -394,14 +389,12 @@ my $tag = undef;
|
||||
my @versionPast = ();
|
||||
|
||||
VERSION: for my $v(@versions) {
|
||||
warn "$0: Processing version (part 2): $v\n" if $par{v};
|
||||
#if($v->{meta}=~m#tag\s*:\s*\Q$par{kind}\E(.*)\)#) {
|
||||
# $tag=$1;
|
||||
# last VERSION
|
||||
#}
|
||||
if($v->{meta}=~m#tag\s*:\s*((?:[vtd]|db|)[0-9\.]+(?:[a-zA-Z\-\+0-9\.]*)?)[\),]#) {
|
||||
if($v->{meta}=~m#tag\s*:\s*([vtd]b?[0-9\.]+(?:-.*)?)\)#) {
|
||||
$v->{version} = $1;
|
||||
warn "$0: Found version number in log: '$v->{version}'\n" if $par{v};
|
||||
push @versionPast, $v->{version}
|
||||
}
|
||||
next if $v->{subject}=~m#^\s*(?:Merge (?:branch|remote)|Revert )#;
|
||||
@ -424,7 +417,6 @@ VERSION: for my $v(@versions) {
|
||||
#$tag = parseVersion($tag);
|
||||
|
||||
for my $r(reverse @change) {
|
||||
warn "$0: Processing change: $r\n" if $par{v};
|
||||
if('major' eq $r->{react}) {
|
||||
$tag->{major}++;
|
||||
$tag->{minor}=0;
|
||||
@ -459,11 +451,8 @@ for my $r(reverse @change) {
|
||||
|
||||
my @allVersions = split /\n/, `$par{vcstags}`;
|
||||
|
||||
#my @sortAll = sort {vsCompare($b, $a)} @allVersions;
|
||||
#my @sortSee = sort {vsCompare($b, $a)} @versionPast;
|
||||
# we want the latest version and do not sort
|
||||
my @sortAll = @allVersions;
|
||||
my @sortSee = @versionPast;
|
||||
my @sortAll = sort {vsCompare($b, $a)} @allVersions;
|
||||
my @sortSee = sort {vsCompare($b, $a)} @versionPast;
|
||||
#print "all: $sortAll[0] -- see: $sortSee[0]\n";
|
||||
#
|
||||
#print vsString($tag), "\n";
|
||||
@ -474,7 +463,6 @@ my $highStart = $mainVersion ? $sortAll[0] : $sortSee[0];
|
||||
my $highSee = $sortSee[0];
|
||||
my %reactCollect = ();
|
||||
SEARCHVERSION: for my $v(@versions) {
|
||||
warn "$0: search for version: '$v'\n" if $par{v};
|
||||
next unless $v->{version};
|
||||
next unless $v->{react};
|
||||
$reactCollect{$v->{react}} = 1;
|
||||
@ -486,18 +474,16 @@ SEARCHVERSION: for my $v(@versions) {
|
||||
sub justVersionInc {
|
||||
my ($v, $react) = @_;
|
||||
my $vv = parseVersion($v);
|
||||
$vv->{patch}++; # if $react->{patch}; # in principal a good idea to increase only when a patch action happend, but we need a new version, even if nothing happend, so we always increase patch; if there are other changes as well, it is overwritten anyways
|
||||
$vv->{patch}++ if $react->{patch};
|
||||
do {$vv->{minor}++; $vv->{patch}=0} if $react->{minor};
|
||||
do {$vv->{major}++; $vv->{minor}=0; $vv->{patch}=0} if $react->{major};
|
||||
my $ret = vsJustVersion($vv);
|
||||
warn "$0: version inc from '$v' to $ret\n" if $par{v};
|
||||
return $ret
|
||||
return vsJustVersion($vv);
|
||||
}
|
||||
|
||||
my $newVersion = undef;
|
||||
|
||||
if($mainVersion) {
|
||||
$newVersion = justVersionInc($highStart, \%reactCollect);
|
||||
$newVersion = "v" . justVersionInc($highStart, \%reactCollect);
|
||||
} else {
|
||||
my $v = parseVersion($highStart);
|
||||
if(exists $v->{branchname}) {
|
||||
@ -514,7 +500,6 @@ for(@allVersions) {
|
||||
$allVersions{$_} = 1
|
||||
}
|
||||
while(exists $allVersions{$newVersion}) {
|
||||
warn "$0: Version conflict, so we try another version, '$newVersion' exists already\n" if $par{v};
|
||||
if($mainVersion) {
|
||||
die "$0: probably internal error (collision in main version)\n"
|
||||
}
|
||||
@ -544,7 +529,6 @@ if($par{changelog}) {
|
||||
my %seen = ();
|
||||
my @sects = ([]);
|
||||
for(@changelog) {
|
||||
warn "$0: Changelog processing: '$_'\n" if $par{v};
|
||||
push @sects, [] if m/^## /;
|
||||
push @{$sects[-1]}, $_;
|
||||
if(m#/commit/([a-f0-9]+)\s*\)\s*\)\s*$#) {
|
||||
@ -558,7 +542,6 @@ if($par{changelog}) {
|
||||
shift @sects;
|
||||
}
|
||||
for my $s(@sects) {
|
||||
warn "$0: Changelog processing, section search: '$s'\n" if $par{v};
|
||||
my $hh = $s->[0];
|
||||
chomp $hh;
|
||||
my $cnt = @$s;
|
||||
@ -583,7 +566,6 @@ if($par{changelog}) {
|
||||
'feature' => 'Features',
|
||||
);
|
||||
SELECTCHANGELOG: for my $v(@versions) {
|
||||
warn "$0: Changelog processing, version selection: '$v'\n" if $par{v};
|
||||
last SELECTCHANGELOG if $seen{$v->{hash}};
|
||||
next unless $v->{subject}=~m#^\s*([a-z]+)\s*(!?)\s*((?:\(.*?\))?)\s*:\s*(.*?)\s*$#i;
|
||||
my ($kind, $break, $context, $msg) = ($1, $2, $3, $4);
|
||||
@ -607,7 +589,7 @@ if($par{changelog}) {
|
||||
my $preVersion = '';
|
||||
if(defined $sects[0] and defined $sects[0][0] and $sects[0][0]=~m/^##\s*\[([^\]\[]+)\]\(/) {
|
||||
$preVersion = $1;
|
||||
# $preVersion =~ s#^v?#v#;
|
||||
$preVersion =~ s#^v?#v#;
|
||||
}
|
||||
my $today = do {
|
||||
my @time = localtime;
|
||||
@ -636,3 +618,11 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -7,33 +7,33 @@ const standardVersionUpdaterYaml = require.resolve('standard-version-updater-yam
|
||||
module.exports = {
|
||||
scripts: {
|
||||
// postbump: './sync-versions.hs && git add -- package.yaml', // moved to bumpFiles
|
||||
postchangelog: 'sed \'s/^### \\[/## [/g\' -i CHANGELOG.md',
|
||||
postchangelog: 'sed \'s/^### \\[/## [/g\' -i CHANGELOG.md'
|
||||
},
|
||||
packageFiles: ['package.json', 'package.yaml'],
|
||||
bumpFiles: [
|
||||
{
|
||||
filename: 'package.json',
|
||||
type: 'json',
|
||||
type: 'json'
|
||||
},
|
||||
{
|
||||
filename: 'package-lock.json',
|
||||
type: 'json',
|
||||
type: 'json'
|
||||
},
|
||||
{
|
||||
filename: 'package.yaml',
|
||||
updater: standardVersionUpdaterYaml,
|
||||
updater: standardVersionUpdaterYaml
|
||||
},
|
||||
{
|
||||
filename: 'nix/docker/version.json',
|
||||
type: 'json',
|
||||
type: 'json'
|
||||
},
|
||||
{
|
||||
filename: 'nix/docker/demo-version.json',
|
||||
type: 'json',
|
||||
},
|
||||
type: 'json'
|
||||
}
|
||||
],
|
||||
commitUrlFormat: 'https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/{{hash}}',
|
||||
compareUrlFormat: 'https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/{{previousTag}}...{{currentTag}}',
|
||||
issueUrlFormat: 'https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/{{id}}',
|
||||
userUrlFormat: 'https://gitlab2.rz.ifi.lmu.de/{{user}}',
|
||||
userUrlFormat: 'https://gitlab2.rz.ifi.lmu.de/{{user}}'
|
||||
};
|
||||
|
||||
243
CHANGELOG.md
243
CHANGELOG.md
@ -2,72 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [27.4.59-0.0.20+145-build-system-rewrite](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/27.4.59-0.0.19+145-build-system-rewrite...27.4.59-0.0.20+145-build-system-rewrite) (2025-03-19)
|
||||
|
||||
## [27.4.59-0.0.19+145-build-system-rewrite](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/27.4.59-0.0.18+145-build-system-rewrite...27.4.59-0.0.19+145-build-system-rewrite) (2025-03-17)
|
||||
|
||||
## [27.4.59-0.0.18+145-build-system-rewrite](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-g0.0.17...27.4.59-0.0.18+145-build-system-rewrite) (2025-03-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **static:** fix addStaticContent by using memcached again to supply static files ([570cfc2](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive/commit/570cfc238bdccd3438124f96290b9272c8e82f0f))
|
||||
|
||||
## [v27.4.59-test-g0.0.17](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-f0.0.17...v27.4.59-test-g0.0.17) (2025-02-18)
|
||||
|
||||
## [v27.4.59-test-f0.0.17](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-g0.0.16...v27.4.59-test-f0.0.17) (2025-02-17)
|
||||
|
||||
## [v27.4.59-test-g0.0.16](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-f0.0.16...v27.4.59-test-g0.0.16) (2025-02-16)
|
||||
|
||||
## [v27.4.59-test-f0.0.16](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-f0.0.15...v27.4.59-test-f0.0.16) (2025-02-16)
|
||||
|
||||
## [v27.4.59-test-f0.0.15](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-e0.0.15...v27.4.59-test-f0.0.15) (2025-02-15)
|
||||
|
||||
## [v27.4.59-test-e0.0.15](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-f0.0.14...v27.4.59-test-e0.0.15) (2025-02-14)
|
||||
|
||||
## [v27.4.59-test-f0.0.14](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-e0.0.14...v27.4.59-test-f0.0.14) (2025-02-14)
|
||||
|
||||
## [v27.4.59-test-e0.0.14](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-e0.0.13...v27.4.59-test-e0.0.14) (2025-02-13)
|
||||
|
||||
## [v27.4.59-test-e0.0.13](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-e0.0.12...v27.4.59-test-e0.0.13) (2025-02-12)
|
||||
|
||||
## [v27.4.59-test-e0.0.12](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-d0.0.12...v27.4.59-test-e0.0.12) (2025-02-12)
|
||||
|
||||
## [v27.4.59-test-d0.0.12](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-d0.0.11...v27.4.59-test-d0.0.12) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-d0.0.11](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-c0.0.11...v27.4.59-test-d0.0.11) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-c0.0.11](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-b0.0.11...v27.4.59-test-c0.0.11) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-b0.0.11](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-c0.0.10...v27.4.59-test-b0.0.11) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-c0.0.10](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-b0.0.10...v27.4.59-test-c0.0.10) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-b0.0.10](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.10...v27.4.59-test-b0.0.10) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-a0.0.10](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.9...v27.4.59-test-a0.0.10) (2025-02-11)
|
||||
|
||||
## [v27.4.59-test-a0.0.9](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.8...v27.4.59-test-a0.0.9) (2025-02-10)
|
||||
|
||||
## [v27.4.59-test-a0.0.8](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.7...v27.4.59-test-a0.0.8) (2025-02-10)
|
||||
|
||||
## [v27.4.59-test-a0.0.7](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.6...v27.4.59-test-a0.0.7) (2025-02-10)
|
||||
|
||||
## [v27.4.59-test-a0.0.6](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.5...v27.4.59-test-a0.0.6) (2025-02-08)
|
||||
|
||||
## [v27.4.59-test-a0.0.5](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.4...v27.4.59-test-a0.0.5) (2025-02-07)
|
||||
|
||||
## [v27.4.59-test-a0.0.4](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.3...v27.4.59-test-a0.0.4) (2025-02-07)
|
||||
|
||||
## [v27.4.59-test-a0.0.3](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.2...v27.4.59-test-a0.0.3) (2025-02-06)
|
||||
|
||||
## [v27.4.59-test-a0.0.2](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.1...v27.4.59-test-a0.0.2) (2025-02-05)
|
||||
|
||||
## [v27.4.59-test-a0.0.1](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59-test-a0.0.0...v27.4.59-test-a0.0.1) (2025-02-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ghci:** ghci works now as expected ([c3117db](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive/commit/c3117dbdcd1de9ef9f0751afa45018e2ebce2c42))
|
||||
|
||||
## [v27.4.59-test-a0.0.0](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive//compare/v27.4.59...v27.4.59-test-a0.0.0) (2024-10-25)
|
||||
|
||||
### Features
|
||||
@ -84,6 +18,183 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
* **Makefile:** add missing dependency on well-known for backend-builds ([a09dc59](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive/commit/a09dc59f260843f8815c382576bb5254d21104bf))
|
||||
* **frontend:** fixed icon colour in table headers ([4c4571d](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive/commit/4c4571d2d0879e89f2572eba6015d34a7f4794c8))
|
||||
* **doc:** minor haddock problems ([d4f8a6c](https://fraport@dev.azure.com/fraport/Fahrerausbildung/_git/FRADrive/commit/d4f8a6c77b2a4a4540935f7f0beca0d0605508c8))
|
||||
## [27.4.76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.75...v27.4.76) (2024-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ap:** disambiguate action message ([8b0466e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8b0466e74e36e1d0d07518fd317d46b00ab53eff))
|
||||
* **avs:** fix [#173](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/173) by not using firm superior email as display email ([43f5c5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43f5c5f4854d1ab2af27b479e72a58e2818a5696))
|
||||
* **avs:** towards [#117](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/117) update if current value is Nothing even if oldval == newval ([d1fa01f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1fa01fcc5125c4adee8849f9c944884926f78ad))
|
||||
* **avs:** using firm superior as UserEmail is a no-go due to uniqueness constraints ([507a7e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/507a7e02fc68476d01031dc9f9ee1a669a453ed1))
|
||||
* **build:** linter likes it ([f929e03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f929e03129378e08c8a08ed4bd6f8e8716401813))
|
||||
* **course:** fix [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150) course edit for associated qualifications requires school admin or lecturer rights ([5b6e4e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b6e4e60e7d2957fbce93ee2e2d6d3464b4e3db7))
|
||||
* **course:** fix [#148](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/148) course qualification ordering ([cfd2534](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfd25348ad3b63ac6bc5031467a3c4ead2e07eed)), closes [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150)
|
||||
* **course:** fix [#149](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/149) course cloning proposes associated qualifications ([e141976](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1419766f3a06f702abad0ea42f6552305504ba0))
|
||||
* **course:** fix [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150) no longer allow duplicated associated qualifications and orders due to editing existing ([ec02767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec027675525b30198378745ed281f60a42471807))
|
||||
* **course:** WIP course cloning should propose same associated qualifications, towards [#149](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/149) ([bc47387](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc47387c91dda60a2f12e52dba28ea7b079316f0))
|
||||
* **lms:** max e-learning tries default removed and info added to lms overview ([11fdcf0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11fdcf0d445b8cfe97c3a3c26513a9229937c536))
|
||||
* **user:** format userDisplayNames having umlaut substitutes with respect to userSurname correctly ([e35a5e9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e35a5e99a6cea0976fd1c28f919e7d0ac0338503))
|
||||
|
||||
## [27.4.75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.74...v27.4.75) (2024-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **build:** make linter happy again ([c17c18f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c17c18f9247ef322bc051602a3cb4a52cd50affa))
|
||||
* **build:** minor ([ab28c8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ab28c8c2437680023d80e6ab43113d4328b3a151))
|
||||
* **firm:** fix [#157](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/157) by removing redundant duplicated code in firm user and supervision handling ([28e2739](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/28e2739e515700d15c75647c0efe2fe9a9cf15b1))
|
||||
* **job:** change some queueJob' to queueJob instead ([fa0541a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa0541aa4eaf10f98535a0959593b148b8346109))
|
||||
* **lms:** allow 2nd reminders to be independent of renewal period ([d853e85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d853e8559b753865ee818bf24764f5c8d2e2303f))
|
||||
* **lms:** move lms reuse info from QualificationR to LmsR ([468af9d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/468af9de9da44a8ad685ca4bb6890a3e630b58be))
|
||||
* **lms:** send second reminder indepentently from renewal period ([a97c3a5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a97c3a5c9d3cb9dddf90f561712f0845400893bd))
|
||||
* **nix:** workaround parsing port numbers failed in nix-shell ([b5215cc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b5215cc7e8df3a7ad636271c8e6950979b2b8e42))
|
||||
* **users:** nameHtml no longer complains about differing case for surname and displayname ([a1668f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1668f891a36b887439afb098f016ef22535af42))
|
||||
* **users:** remove users with company post address from list of unreachable users ([c813c66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c813c665ed306135b7813d91d23310341c689f41))
|
||||
|
||||
## [27.4.74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.73...v27.4.74) (2024-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **lms:** fix [#161](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/161) lms for multiple joint qualifications ([f869a82](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f869a829d2c1a726930864b3af62d1f0fbebe955))
|
||||
|
||||
## [27.4.73](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.72...v27.4.73) (2024-07-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **letter:** rephrase some minor letter parts ([0ac75e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0ac75e0d5948cb90855d0e36ca8e99c22a0f6fcb))
|
||||
|
||||
## [27.4.72](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.71...v27.4.72) (2024-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** do not associate users by AvsInfoPersonEmail ([9e2f221](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e2f2214ce5c7ee1e8d80e6fa75298b7a70d9043))
|
||||
* **avs:** fix superfluous quotes for matriculation numbers on newly created users ([ff9014c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff9014ce05d197c1dc0fce0774a640789cb38b26))
|
||||
* **avs:** towards [#169](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/169) - superiors are elevated to max priority for that company ([5bf8539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bf85394d4db6de8f10b4e318d667130d37601ac))
|
||||
* **firm:** supervisor secondary did not work as intended ([d4f3ce7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f3ce7bf3d208b16f95ab81971b47dfa752939a))
|
||||
|
||||
## [27.4.71](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.70...v27.4.71) (2024-06-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** company superior emails become company wide supervisors ([37efc89](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/37efc89e0723452e6d271ba5b43d6bd026642190))
|
||||
* **avs:** match mobile number better between LDAP and AVS ([f108c6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f108c6cfec2d94d866e7c1605b0abe5471fd0f2b))
|
||||
* **avs:** new AVS from existing LDAP user no longer misses fields ([2559346](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2559346d963ede802321dfc8cbd2088d9a5de685))
|
||||
* **avs:** priority for picking primary email demote superior ([e4fa1dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e4fa1ddd6873910bef82d569fe16aca936efc567))
|
||||
* **build:** add missing license file ([8721bdb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8721bdb3f349658baab144d64c19942bfd7fa49a))
|
||||
* **build:** hlint wants a newtype instead ([18cdc52](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/18cdc52df094b9dbccd4f015561367cea59e33fe))
|
||||
* **doc:** fix erroneous unintentional haddock annotations ([3dfc7f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3dfc7f8c8b12dd6ef87848a75f1669d700fffe4c))
|
||||
* **i18n:** add missing translation for new primary company ([c212f2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c212f2e8d735616e59c9b8111a34118e3a48fd47))
|
||||
* **i18n:** add missing translation for new primary company ([2cc529b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2cc529be39655c317ca028f8f09fa80826ec668d))
|
||||
* **ldap:** match mobile number better between LDAP and AVS ([47e5628](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/47e56280fce4ad37e6bc3b9f1c61cb7867069cc5))
|
||||
* **letter:** adjust spacing, pin location and interpolation ([d4a0e1f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4a0e1f201151f76e8e9afd67b456cc878d2afde))
|
||||
* **letter:** convenience links working again ([5f1af13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5f1af130edae7ada2f0c7f7829890bbe0d4f395a))
|
||||
* **letter:** expiry and valid dates were wrong ([f8c3663](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8c36636ff1f2591507e993af32ed01af94cf1fc))
|
||||
* **letter:** switch markdown for renewal letter too ([c38e87e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c38e87e1e0e9285a10c00521b7440cd8246af88a))
|
||||
* **print:** fix [#167](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/167) by sotring affected user in PrintJob ([73aecc2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/73aecc2df833bdeed93a113b6c756e36b50491b7))
|
||||
|
||||
## [27.4.70](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.69...v27.4.70) (2024-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **build:** hlint wants a newtype instead ([0766351](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/07663516e520814e26740d671325b7cd10855dd4))
|
||||
|
||||
## [27.4.69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.68...v27.4.69) (2024-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** fix type causing avs surname upate not working ([822c43c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/822c43c8a7db2086954ad187502ec2c4f1811d17))
|
||||
* **avs:** keep company on unchange address/email only if either is non-empty ([766b858](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/766b8589d6945df21fc6ce90d35a004655ffa471))
|
||||
* **avs:** synch job deletes used row instead of truncation ([d7acc7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7acc7a2d0fe5fc18929a8cb2d9c9f8a259c9944))
|
||||
|
||||
## [27.4.68](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.67...v27.4.68) (2024-06-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **letter:** minor ([2ae11dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ae11dc25c000486af9acc26439a0580f5c687f2))
|
||||
|
||||
## [27.4.67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.66...v27.4.67) (2024-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** fix rare avs update bug involving values optional in avs but compulsory in user entity ([a6d0105](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6d0105903caba0eb47715eeb217ea2c53d99e23))
|
||||
|
||||
## [27.4.66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.65...v27.4.66) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** fix [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164) by removing companyPersonalNumber and companyDepartment upon ldap sync expiry ([da74b95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da74b957295caefb010c90297af557f997b18e7c))
|
||||
* **avs:** fix [#165](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/165) by updating userCompanyDepartmen and userCompanyPersonalNumer ([76e0710](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76e0710c7b54a40d2c236299ea4fabd009d3f35a))
|
||||
* **avs:** repeated avs sync enqueue no longe violates duplicate db uniqueness constraints ([996e6a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/996e6a0ce563bda96638863efd40ce38fce8ac2b))
|
||||
* **avs:** update email on manual company switch ([9fd80f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fd80f25526eefce217c659f6ea2991771c11ece)), closes [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164)
|
||||
|
||||
## [27.4.65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.64...v27.4.65) (2024-06-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** company update no longer fails on duplicate key ([bb101de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb101dee7b40cd3d8ba10a559af642396d5b87b5))
|
||||
* **avs:** profile page correctly indicates automatic email and postal addresses ([e553ad4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e553ad4358a71fc96fa946533f0441d4af5202c9))
|
||||
* **avs:** steps towards [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164) ([aa1d230](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa1d230e497f0e59dbea9f4fd5c7da773f5a4280))
|
||||
* **lette:** adjust window for new pin letters ([6acfd84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6acfd849aeb473a018f7a9c34e69f61b3c22b6f8))
|
||||
|
||||
## [27.4.64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.63...v27.4.64) (2024-05-27)
|
||||
|
||||
## [27.4.63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.62...v27.4.63) (2024-05-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** company update checks uniques and ignores those updates if necessary ([9451d90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9451d90a9e00d08a2a7d169c4674d99ff1018ee9))
|
||||
|
||||
## [27.4.62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.61...v27.4.62) (2024-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** avs update on company shorthands working now ([ff2347b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff2347b1c950c7a2bb281cdcd07a52925e23b9f0))
|
||||
* **avs:** deal gracefully with empty card status results ([ccf9340](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ccf934044938277d821eb4b9ea08a8a134e84189))
|
||||
|
||||
## [27.4.61](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.60...v27.4.61) (2024-05-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** fix [#76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/76) allowing company changes and fix [#69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/69) ([3c4a0b8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c4a0b86c1e3d8a28405ab73b964ba1b988d2822))
|
||||
* **build:** add missing tex packages ([6750798](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6750798920dc76882f4e8ef39b47018fb7b77e44))
|
||||
* **build:** workaround non modal form result handler ([2fbd281](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2fbd28154cd7aea282eaa2604a42263ac90e3b1e))
|
||||
|
||||
## [27.4.60](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.59...v27.4.60) (2024-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **avs:** disable caching by 0s no longer causes an exception ([d578e80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d578e80282c8bf6872fa6040514a9d2c85582707))
|
||||
* **avs:** fix [#152](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/152) by providing new online avs card filter throughout ([ad2375b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad2375b338866f37c8b7825a9eab12fa6c9abccb))
|
||||
* **avs:** fix [#36](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/36) and remove dead code ([4f8850b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f8850b3b4f710f9cf59163175b27599c97ac5c0))
|
||||
* **avs:** fix [#69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/69) by redesigning live avs status page ([697979c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/697979c277ce7198f4573d6cea30373a1fcc17da))
|
||||
* **avs:** invalidate contact cache after licence writes ([c382be9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c382be9325fcc92e13cb5dc2ad7c20b198db26fc))
|
||||
* **avs:** several minor bugfixes ([a52c8a6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a52c8a6ad709029a8822d383370b0d2bdd25e7d7)), closes [#158](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/158)
|
||||
* **build:** add import needed for production only ([724e4a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/724e4a0bec343ab9c6d172d8e93b8040bbe3fe7d))
|
||||
* **build:** migration needs to check for table existens first ([f439ea4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f439ea45af9b1c4a029fc1b9b6383f3c97194ed0))
|
||||
* **build:** minor error non-development code ([66eaa4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66eaa4f7dcc124b631414d4a1adbe555a4029100))
|
||||
* **build:** missing parameters added ([83afdf7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/83afdf760f93fc1a553de3a122b444412ed84ba4))
|
||||
* **build:** simple type error ([d56a1cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d56a1cdd46259418faa737b9bb0a9d9ffba442e0))
|
||||
* **build:** type error in test db fill data ([f465cc9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f465cc972367233a4944dd0aeb81b223a187bb85))
|
||||
* **doc:** minor haddock problems ([d4f8a6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f8a6c77b2a4a4540935f7f0beca0d0605508c8))
|
||||
* **firm:** supervisor filter acts weird in test environment ([b566e59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b566e59eb1325485fe26dc4f0b5cb63165c58f74))
|
||||
* **i18n:** fix some bad plurals ([890f8ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/890f8ad8b60115533faa6b99f4c4504243cbfb1d))
|
||||
* **lint:** remove minor superfluous dollar ([64a1233](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/64a123387f3539b73649d02a6ecd97de577097e6))
|
||||
* **qualification:** fix [#159](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/159) by removing an misleadingly named column for user qualification table ([fd6a538](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fd6a5384d3517958a3c7726e32eed3bad197a591))
|
||||
|
||||
## [27.4.59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.58...v27.4.59) (2024-02-13)
|
||||
|
||||
|
||||
461
Makefile
461
Makefile
@ -1,123 +1,400 @@
|
||||
|
||||
export SHELL=bash
|
||||
|
||||
export CLEAN_DEPENDENCIES ?= false
|
||||
export CLEAN_IMAGES ?= false
|
||||
# MAKE=make -f Makefile-loggingsymbols
|
||||
# MAKE=make -d
|
||||
|
||||
# System information
|
||||
export CPU_CORES = $(shell cat /proc/cpuinfo | grep '^processor' | wc -l)
|
||||
|
||||
export CONTAINER_COMMAND ?= podman
|
||||
export CONTAINER_BGRUN ?= $(CONTAINER_COMMAND) run -dit --network=host --replace
|
||||
export CONTAINER_FGRUN ?= $(CONTAINER_COMMAND) run -it --network=host --replace
|
||||
|
||||
export IMAGE_REGISTRY = docker.io
|
||||
export MEMCACHED_IMAGE = $(IMAGE_REGISTRY)/memcached:latest
|
||||
export MINIO_IMAGE = $(IMAGE_REGISTRY)/minio/minio:latest
|
||||
export MAILDEV_IMAGE = $(IMAGE_REGISTRY)/maildev/maildev:latest # TODO: needs different port than 1025 to avoid conflicts
|
||||
|
||||
export IN_CONTAINER ?= false
|
||||
export IN_CI ?= false
|
||||
export CONTAINER_FILE
|
||||
export CONTAINER_IDENT
|
||||
export CF_PREFIX
|
||||
export DEVELOP
|
||||
export MOUNT_DIR=/mnt/fradrive
|
||||
export CONTAINER_ATTACHED
|
||||
export CONTAINER_INIT
|
||||
export CONTAINER_CLEANUP
|
||||
|
||||
# export STACK_INIT="./utils/stack-work-init.sh"
|
||||
# export STACK_CLEANUP="rm -f .stack-work.lock"
|
||||
|
||||
export SERVICE
|
||||
export SERVICE_VARIANT ?= $(SERVICE)
|
||||
export JOB
|
||||
export CONTAINER_CMD
|
||||
export SET_CONTAINER_CMD
|
||||
export ENTRYPOINT
|
||||
export EXEC_OPTS
|
||||
|
||||
export STACK_CORES = $(shell echo $(($(CPU_CORES)/2)))
|
||||
export BASE_PORTS
|
||||
export UNIWORXDB_OPTS ?= -cf
|
||||
export PROD ?= false
|
||||
|
||||
ifneq ($(PROD),true)
|
||||
export --DEVELOPMENT=--flag uniworx:dev
|
||||
endif
|
||||
|
||||
export DATE := $(shell date +'%Y-%m-%dT%H-%M-%S')
|
||||
|
||||
export CURR_DEV = $(shell cat develop/.current 2>/dev/null)
|
||||
export SET_DEVELOP = $(eval DEVELOP=develop/$$(CURR_DEV))
|
||||
export NEW_DEVELOP = $(eval DEVELOP=develop/$$(DATE))
|
||||
|
||||
export ENTRYPOINT ?= bash
|
||||
export SRC
|
||||
|
||||
.PHONY: help
|
||||
# HELP: print out this help message
|
||||
help:
|
||||
docker compose run help
|
||||
@if [ -z "$$(which perl 2>/dev/null)" ] ; then \
|
||||
$(CONTAINER_FGRUN) .:/mnt 'debian:12.5' '/mnt/utils/makehelp.pl' '/mnt/Makefile' ; \
|
||||
else \
|
||||
utils/makehelp.pl Makefile ; \
|
||||
fi
|
||||
|
||||
.PHONY: clean
|
||||
# HELP: clean compilation caches
|
||||
# HELP: stop all running containers and remove all compilation results in the directory (but leave images including dependencies unharmed)
|
||||
clean:
|
||||
$(MAKE) clean-frontend CLEAN_DEPENDENCIES=$(CLEAN_DEPENDENCIES) CLEAN_IMAGES=$(CLEAN_IMAGES)
|
||||
$(MAKE) clean-backend CLEAN_DEPENDENCIES=$(CLEAN_DEPENDENCIES) CLEAN_IMAGES=$(CLEAN_IMAGES)
|
||||
rm -rf develop
|
||||
-rm -rf node_modules .npm .cache assets/icons assets/favicons static well-known config/manifest.json
|
||||
-rm -rf .job-*
|
||||
-rm -rf bin .Dockerfile develop
|
||||
-$(CONTAINER_COMMAND) container prune --force
|
||||
.PHONY: clean-all
|
||||
# HELP: clean everything, including dependency and image caches
|
||||
clean-all: CLEAN_DEPENDENCIES = true
|
||||
clean-all: CLEAN_IMAGES = true
|
||||
clean-all: clean ;
|
||||
# HELP: like clean but with full container, image, and volume prune
|
||||
clean-all: clean
|
||||
-rm -rf .stack
|
||||
-$(CONTAINER_COMMAND) system prune --all --force --volumes
|
||||
-$(CONTAINER_COMMAND) image prune --all --force
|
||||
-$(CONTAINER_COMMAND) volume prune --force
|
||||
|
||||
.PHONY: clean-%
|
||||
# HELP(clean-$SERVICE): invalidate caches for a given service. Supported services: frontend, backend.
|
||||
clean-%:
|
||||
$(MAKE) stop-$*
|
||||
@$(MAKE) -- --clean-$*
|
||||
@echo "Cleaned $* build files and binaries."
|
||||
ifeq ("$(CLEAN_DEPENDENCIES)", "true")
|
||||
@$(MAKE) -- --clean-$*-deps
|
||||
@echo "Cleaned $* dependencies."
|
||||
endif
|
||||
ifeq ("$(CLEAN_IMAGES)", "true")
|
||||
$(MAKE) kill-$*
|
||||
docker compose rm --force --volumes
|
||||
docker compose down --rmi 'all' --volumes
|
||||
@echo "Cleaned $* image."
|
||||
endif
|
||||
--clean-frontend:
|
||||
-rm -rf assets/icons assets/favicons
|
||||
-rm -rf static well-known
|
||||
--clean-frontend-deps:
|
||||
-rm -rf frontend/node_modules
|
||||
-rm -rf frontend/.npm
|
||||
--clean-backend:
|
||||
-rm -rf backend/.stack-work
|
||||
-rm -rf bin/
|
||||
--clean-backend-deps:
|
||||
-rf -rf backend/.stack
|
||||
|
||||
|
||||
# TODO: only release when build and tests are passing!!!
|
||||
.PHONY: release
|
||||
# HELP: create, commit and push a new release
|
||||
release:
|
||||
VERSION=`./utils/version.pl -changelog CHANGELOG.md -v` ; \
|
||||
git add CHANGELOG.md ; \
|
||||
git commit -m "chore(release): $${VERSION}" ; \
|
||||
git push ; \
|
||||
git tag $${VERSION} ; \
|
||||
git push origin $${VERSION}
|
||||
./.gitlab-ci/version.pl -changelog CHANGELOG.md
|
||||
git add CHANGELOG.md
|
||||
VERSION=`.gitlab-ci/version.pl`
|
||||
git tag $${VERSION}
|
||||
git commit -m "chore(release): $${VERSION}"
|
||||
# git push
|
||||
|
||||
.PHONY: compile
|
||||
# HELP: perform full compilation (frontend and backend)
|
||||
compile: compile-frontend compile-backend ;
|
||||
.PHONY: compile-%
|
||||
# HELP(compile-$SERVICE): compile a given service once
|
||||
compile-%:
|
||||
docker compose run --remove-orphans --build --no-deps $* make compile
|
||||
compile:
|
||||
$(MAKE) compile-frontend
|
||||
$(MAKE) compile-backend
|
||||
|
||||
.PHONY: start
|
||||
# HELP: start complete development environment with a fresh test database
|
||||
start: start-postgres start-maildev start-memcached start-minio start-backend
|
||||
docker compose exec backend make start
|
||||
start:
|
||||
$(MAKE) start-postgres
|
||||
$(MAKE) start-memcached
|
||||
$(MAKE) start-minio
|
||||
$(MAKE) start-frontend
|
||||
$(MAKE) start-backend
|
||||
|
||||
.PHONY: %-backend
|
||||
%-backend: SERVICE=backend
|
||||
%-backend: SERVICE_VARIANT=backend
|
||||
%-backend: CONTAINER_CMD=localhost/fradrive/backend
|
||||
# %-backend: CONTAINER_INIT="$(STACK_INIT)"
|
||||
# %-backend: CONTAINER_CLEANUP="$(STACK_CLEANUP)"
|
||||
%-backend: BASE_PORTS = "DEV_PORT_HTTP=3000" "DEV_PORT_HTTPS=3443"
|
||||
|
||||
.PHONY: %-uniworxdb
|
||||
%-uniworxdb: SERVICE=backend
|
||||
%-uniworxdb: SERVICE_VARIANT=uniworxdb
|
||||
%-uniworxdb: CONTAINER_CMD=localhost/fradrive/backend
|
||||
# %-uniworxdb: CONTAINER_INIT="$(STACK_INIT)"
|
||||
# %-uniworxdb: CONTAINER_CLEANUP="$(STACK_CLEANUP)"
|
||||
|
||||
.PHONY: %-hoogle
|
||||
%-hoogle: SERVICE=backend
|
||||
%-hoogle: SERVICE_VARIANT=hoogle
|
||||
%-hoogle: BASE_PORTS = "HOOGLE_PORT=8081"
|
||||
%-hoogle: CONTAINER_CMD=localhost/fradrive/backend
|
||||
# %-hoogle: CONTAINER_INIT="$(STACK_INIT)"
|
||||
# %-hoogle: CONTAINER_CLEANUP="$(STACK_CLEANUP)"
|
||||
--start-hoogle:
|
||||
HOOGLE_PORT=`cat $(CONTAINER_FILE) | grep 'HOOGLE_PORT=' | sed 's/HOOGLE_PORT=//'` ; \
|
||||
stack $(STACK_CORES) hoogle -- server --local --port $${HOOGLE_PORT}
|
||||
|
||||
.PHONY: %-frontend
|
||||
%-frontend: SERVICE=frontend
|
||||
%-frontend: SERVICE_VARIANT=frontend
|
||||
%-frontend: CONTAINER_CMD=localhost/fradrive/frontend
|
||||
|
||||
.PHONY: %-postgres
|
||||
%-postgres: SERVICE=postgres
|
||||
%-postgres: SERVICE_VARIANT=postgres
|
||||
%-postgres: BASE_PORTS = "PGPORT=5432"
|
||||
%-postgres: CONTAINER_CMD=localhost/fradrive/postgres
|
||||
|
||||
.PHONY: %-memcached
|
||||
%-memcached: SERVICE=memcached
|
||||
%-memcached: SERVICE_VARIANT=memcached
|
||||
%-memcached: SET_CONTAINER_CMD=$$(MEMCACHED_IMAGE) --port=`cat $$(CONTAINER_FILE) | grep 'MEMCACHED_PORT=' | sed 's/MEMCACHED_PORT=//'`
|
||||
%-memcached: BASE_PORTS = "MEMCACHED_PORT=11211"
|
||||
|
||||
.PHONY: %-minio
|
||||
%-minio: SERVICE=minio
|
||||
%-minio: SERVICE_VARIANT=minio
|
||||
%-minio: SET_CONTAINER_CMD=$$(MINIO_IMAGE) -- server `mktemp` --address=:`cat $$(CONTAINER_FILE) | grep 'UPLOAD_S3_PORT=' | sed 's/UPLOAD_S3_PORT=//'`
|
||||
%-minio: BASE_PORTS = "UPLOAD_S3_PORT=9000"
|
||||
|
||||
.PHONY: start-%
|
||||
# HELP(start-$SERVICE): start a given service
|
||||
start-%:
|
||||
docker compose up -d --build $*
|
||||
start-%: JOB=start
|
||||
start-%: CF_PREFIX = start-
|
||||
start-%: CONTAINER_ATTACHED = false
|
||||
start-%: --act ;
|
||||
|
||||
.PHONY: shell-%
|
||||
# HELP(shell-$SERVICE): launch a (bash) shell inside a given service
|
||||
shell-%:
|
||||
docker compose run --build --no-deps --entrypoint="$(ENTRYPOINT)" $*
|
||||
.PHONY: ghci
|
||||
# HELP: launch ghci instance. Use in combination with SRC to specify the modules to be loaded by ghci: make ghci SRC=src/SomeModule.hs
|
||||
ghci: ENTRYPOINT=stack ghci $(SRC)
|
||||
ghci: shell-backend ;
|
||||
.PHONY: compile-%
|
||||
compile-%: JOB=compile
|
||||
compile-%: CF_PREFIX = compile-
|
||||
compile-%: CONTAINER_ATTACHED = true
|
||||
compile-%: --act ;
|
||||
|
||||
.PHONY: stop
|
||||
# HELP: stop all services
|
||||
stop:
|
||||
docker compose down
|
||||
.PHONY: stop-%
|
||||
# HELP(stop-$SERVICE): stop a given service
|
||||
stop-%:
|
||||
docker compose down $*
|
||||
.PHONY: kill-%
|
||||
# HELP(kill-$SERVICE): kill a given service the hard way. Use this if the servive does not respond to stop.
|
||||
kill-%:
|
||||
docker compose kill $*
|
||||
.PHONY: dependencies-%
|
||||
dependencies-%: JOB=dependencies
|
||||
dependencies-%: CF_PREFIX = dependencies-
|
||||
dependencies-%: CONTAINER_ATTACHED = true
|
||||
dependencies-%: --act ;
|
||||
|
||||
.PHONY: test-%
|
||||
test-%: JOB=test
|
||||
test-%: CF_PREFIX = test-
|
||||
test-%: CONTAINER_ATTACHED = true
|
||||
test-%: --act ;
|
||||
|
||||
.PHONY: lint-%
|
||||
lint-%: JOB=lint
|
||||
lint-%: CF_PREFIX = lint-
|
||||
lint-%: CONTAINER_ATTACHED = true
|
||||
lint-%: --act ;
|
||||
|
||||
--act: --develop_containerized;
|
||||
|
||||
--develop_%: PORTS = $(foreach PORT,$(BASE_PORTS),$(shell utils/next_free_port.pl $(PORT)))
|
||||
--develop_%: --ensure-develop
|
||||
DEVELOP=develop/`cat develop/.current` ; \
|
||||
CONTAINER_IDENT=$(CF_PREFIX)$(SERVICE_VARIANT) ; \
|
||||
CONTAINER_FILE=$${DEVELOP}/$${CONTAINER_IDENT} ; \
|
||||
if [[ -e $${CONTAINER_FILE} ]]; then \
|
||||
>&2 echo "Another $* service is already running! Use \"make new-develop\" to start a new develop instance despite currently running services." ; \
|
||||
exit 1 ; \
|
||||
fi ; \
|
||||
echo "$(PORTS)" | sed 's/ /\n/g' > $${CONTAINER_FILE} ; \
|
||||
$(MAKE) -- --$* CONTAINER_FILE=$${CONTAINER_FILE} CONTAINER_IDENT=$${CONTAINER_IDENT} JOB=$(JOB)
|
||||
|
||||
.PHONY: rebuild-%
|
||||
# HELP(rebuild-{backend,frontend,database,memcached,minio}): force-rebuild the stated docker image
|
||||
rebuild-%:
|
||||
$(MAKE) -- --image-build SERVICE=$* NO_CACHE=--no-cache
|
||||
--image-build:
|
||||
ifeq "$(CONTAINER_CMD)" "localhost/fradrive/$(SERVICE)"
|
||||
rm -f .Dockerfile
|
||||
ln -s docker/$(SERVICE)/Dockerfile .Dockerfile
|
||||
$(MAKE) .job-$(JOB)
|
||||
MOUNT_DIR=/mnt/fradrive; \
|
||||
PROJECT_DIR=/mnt/fradrive; \
|
||||
if [ "$(IN_CI)" == "true" ] ; then \
|
||||
PROJECT_DIR=/fradrive; \
|
||||
fi; \
|
||||
if [ "$(IN_CONTAINER)" == "false" ] ; then \
|
||||
$(CONTAINER_COMMAND) build $(NO_CACHE) \
|
||||
-v $(PWD):$${MOUNT_DIR} \
|
||||
--build-arg MOUNT_DIR=$(MOUNT_DIR) \
|
||||
--build-arg PROJECT_DIR=$${PROJECT_DIR} \
|
||||
--env IN_CONTAINER=true \
|
||||
--env JOB=$(JOB) \
|
||||
--tag fradrive/$(SERVICE) \
|
||||
--file $(PWD)/.Dockerfile ; \
|
||||
fi
|
||||
else
|
||||
:
|
||||
endif
|
||||
|
||||
--containerized:
|
||||
$(MAKE) .job-$(JOB)
|
||||
$(MAKE) -- --image-build
|
||||
DEVELOP=`cat develop/.current` ; \
|
||||
./utils/watchcontainerrun.sh "$(CONTAINER_COMMAND)" "$(CONTAINER_FILE)" "$(CONTAINER_INIT)" "$(CONTAINER_CLEANUP)" & \
|
||||
CONTAINER_NAME=fradrive.$(CURR_DEV).$(CONTAINER_IDENT) ; \
|
||||
if ! [ -z "$(SET_CONTAINER_CMD)" ] ; \
|
||||
then \
|
||||
CONTAINER_CMD="$(SET_CONTAINER_CMD)" ; \
|
||||
else \
|
||||
CONTAINER_CMD=$(CONTAINER_CMD) ; \
|
||||
fi ; \
|
||||
CONTAINER_ID=`$(CONTAINER_BGRUN) \
|
||||
-v $(PWD):$(MOUNT_DIR) \
|
||||
--env IN_CONTAINER=true \
|
||||
--env FRADRIVE_MAKE_TARGET="--$(JOB)-$(SERVICE_VARIANT)" \
|
||||
--env CONTAINER_FILE=$(CONTAINER_FILE) \
|
||||
--env CONTAINER_NAME=$${CONTAINER_NAME} \
|
||||
--env JOB=$(JOB) \
|
||||
--name $${CONTAINER_NAME} \
|
||||
$${CONTAINER_CMD} \
|
||||
` ; \
|
||||
printf "CONTAINER_ID=$${CONTAINER_ID}" >> "$(CONTAINER_FILE)" ; \
|
||||
if [[ "true" == "$(CONTAINER_ATTACHED)" ]] ; then \
|
||||
$(CONTAINER_COMMAND) attach $${CONTAINER_ID} || : ; \
|
||||
fi
|
||||
|
||||
# HELP(start-backend): start yesod-devel instance
|
||||
--start-backend:
|
||||
export DEV_PORT_HTTP=`cat $(CONTAINER_FILE) | grep 'DEV_PORT_HTTP=' | sed 's/DEV_PORT_HTTP=//'`; \
|
||||
export DEV_PORT_HTTPS=`cat $(CONTAINER_FILE) | grep 'DEV_PORT_HTTPS=' | sed 's/DEV_PORT_HTTPS=//'`; \
|
||||
export HOST=`hostname -s` ; \
|
||||
export DETAILED_LOGGING=$${DETAILED_LOGGING:-true} ; \
|
||||
export LOG_ALL=$${LOG_ALL:-false} ; \
|
||||
export LOGLEVEL=$${LOGLEVEL:-info} ; \
|
||||
export DUMMY_LOGIN=$${DUMMY_LOGIN:-true} ; \
|
||||
export SERVER_SESSION_ACID_FALLBACK=$${SERVER_SESSION_ACID_FALLBACK:-true} ; \
|
||||
export SERVER_SESSION_COOKIES_SECURE=$${SERVER_SESSION_COOKIES_SECURE:-false} ; \
|
||||
export COOKIES_SECURE=$${COOKIES_SECURE:-false} ; \
|
||||
export ALLOW_DEPRECATED=$${ALLOW_DEPRECATED:-true} ; \
|
||||
export ENCRYPT_ERRORS=$${ENCRYPT_ERRORS:-false} ; \
|
||||
export RIBBON=$${RIBBON:-$${HOST:-localhost}} ; \
|
||||
export APPROOT=$${APPROOT:-http://localhost:$${DEV_PORT_HTTP}} ; \
|
||||
export AVSPASS=$${AVSPASS:-nopasswordset} ; \
|
||||
stack $(STACK_CORES) exec --local-bin-path $$(pwd)/bin -- yesod devel -p "$${DEV_PORT_HTTP}" -q "$${DEV_PORT_HTTPS}"
|
||||
# HELP(compile-backend): compile backend binaries
|
||||
--compile-backend:
|
||||
stack build $(STACK_CORES) --fast --profile --library-profiling --executable-profiling --flag uniworx:-library-only $(--DEVELOPMENT) --local-bin-path $$(pwd)/bin
|
||||
# HELP(dependencies-backend): (re-)build backend dependencies
|
||||
--dependencies-backend: stack.yaml stack.yaml.lock package.yaml
|
||||
stack build $(STACK_CORES) --fast --only-dependencies
|
||||
# HELP(lint-backend): lint backend
|
||||
--lint-backend:
|
||||
stack build $(STACK_CORES) --test --fast --flag uniworx:library-only $(--DEVELOPMENT) uniworx:test:hlint
|
||||
# HELP(test-backend): test backend
|
||||
--test-backend:
|
||||
stack build $(STACK_CORES) --test --coverage --fast --flag uniworx:library-only $(--DEVELOPMENT)
|
||||
.PHONY: .job-%
|
||||
.job-%:
|
||||
./utils/dirsymlink.pl ".job-$*" .stack-work
|
||||
|
||||
# HELP(compile-frontend): compile frontend assets
|
||||
--compile-frontend: node_modules assets esbuild.config.mjs
|
||||
npm run build
|
||||
--start-frontend: --compile-frontend;
|
||||
--dependencies-frontend: node_modules assets static well-known;
|
||||
node_modules: package.json package-lock.json
|
||||
npm ci --cache .npm --prefer-offline
|
||||
package-lock.json: package.json
|
||||
npm install --cache .npm --prefer-offline
|
||||
assets: assets/favicons assets/icons;
|
||||
assets/favicons:
|
||||
./utils/faviconize.pl assets/favicon.svg long assets/favicons
|
||||
assets/icons: node_modules assets/icons-src/fontawesome.json
|
||||
./utils/renamer.pl node_modules/@fortawesome/fontawesome-free/svgs/solid assets/icons-src/fontawesome.json assets/icons/fradrive
|
||||
./utils/renamer.pl node_modules/@fortawesome/fontawesome-free/svgs/regular assets/icons-src/fontawesome.json assets/icons/fradrive
|
||||
-cp assets/icons-src/*.svg assets/icons/fradrive
|
||||
static: node_modules assets esbuild.config.mjs
|
||||
npm run build
|
||||
well-known: static;
|
||||
|
||||
# HELP(compile-uniworxdb): clear and fill database. requires running postgres
|
||||
# TODO (db-m-$MIGRATION-backend): apply migration (see src/Model/Migration/Definition.hs for list of available migrations)
|
||||
--compile-uniworxdb: --compile-backend
|
||||
SERVER_SESSION_ACID_FALLBACK=${SERVER_SESSION_ACID_FALLBACK:-true} ; \
|
||||
AVSPASS=${AVSPASS:-nopasswordset} ; \
|
||||
stack exec uniworxdb -- $(UNIWORXDB_OPTS)
|
||||
|
||||
# HELP(start-minio): start minio service
|
||||
|
||||
.PHONY: status
|
||||
# HELP: print an overview of currently running services and their health
|
||||
# HELP: print develop status: running containers, used ports
|
||||
status:
|
||||
docker compose ps
|
||||
.PHONY: top
|
||||
# HELP: print an overview of the ressource usage of the currently running services
|
||||
top:
|
||||
docker compose stats
|
||||
.PHONY: list-projects
|
||||
# HELP: list all currently running projects on this machine
|
||||
list-projects:
|
||||
docker compose ls
|
||||
@./utils/develop-status.pl -a
|
||||
|
||||
.PHONY: log-%
|
||||
# HELP(log-$SERVICE): follow the output of a given service. Service must be running.
|
||||
# HELP(log-$(JOB)-$(SERVICE)): inspect output of a given service. The service must be currently running When a service supports multiple running instances in one develop (i.e. backend), you need to specify the exact instance by its associated file (e.g. backend-1, backend-2, etc.), please check the contents of the develop/ directory for a list of running instances.
|
||||
log-%:
|
||||
docker compose logs --follow --timestamps $*
|
||||
DEVELOP=develop/`cat develop/.current` ; \
|
||||
SEARCH_FILE="$${DEVELOP}/$*" ; \
|
||||
if [[ ! -e "$${SEARCH_FILE}" ]] ; then \
|
||||
SEARCH_FILE="$${DEVELOP}/.exited.$*" ; \
|
||||
fi ; \
|
||||
if [[ -e "$${SEARCH_FILE}" ]] ; then \
|
||||
$(CONTAINER_COMMAND) logs --follow `cat "$${SEARCH_FILE}" | grep CONTAINER_ID= | sed 's/^CONTAINER_ID=//'` ; \
|
||||
else \
|
||||
>&2 echo "Cannot show log: No develop file found for '$*'" ; \
|
||||
exit 1 ; \
|
||||
fi
|
||||
|
||||
.PHONY: enter
|
||||
# HELP: launch (bash) shell inside a currently running container. Use ./enter shell wrapper for more convenient usage, possibly with tab-completion in the future
|
||||
enter: --ensure-develop
|
||||
$(MAKE) -- --shell
|
||||
|
||||
.PHONY: psql
|
||||
# HELP: enter psql (postgresql) cli inside a currently running database container
|
||||
psql: ENTRYPOINT=/usr/bin/psql -d uniworx
|
||||
psql: EXEC_OPTS=--user postgres
|
||||
psql: --ensure-develop
|
||||
$(MAKE) -- --shell CONTAINER_FILE=develop/`cat develop/.current`/start-postgres
|
||||
|
||||
.PHONY: ghci
|
||||
# HELP: launch new backend instance and enter interactive ghci shell (WIP)
|
||||
ghci: ENTRYPOINT=stack ghci
|
||||
ghci: --shell;
|
||||
|
||||
--shell:
|
||||
CONTAINER_ID=`cat $(CONTAINER_FILE) | grep 'CONTAINER_ID=' | sed 's/CONTAINER_ID=//'` ; \
|
||||
$(CONTAINER_COMMAND) exec -it $(EXEC_OPTS) $${CONTAINER_ID} $(if $(ENTRYPOINT),$(ENTRYPOINT),/bin/bash)
|
||||
|
||||
.PHONY: stop
|
||||
# HELP: stop all currently running develop instances
|
||||
stop:
|
||||
rm -rf develop
|
||||
.PHONY: stop-%
|
||||
# HELP(stop-{database,memcached,minio,backend,frontend,hoogle}): stop all currently running develop instances of a given type
|
||||
stop-%:
|
||||
$(SET_DEVELOP)
|
||||
rm -rf $(DEVELOP)/$*
|
||||
stop-container-by-file:
|
||||
rm $(CONTAINER_FILE)
|
||||
stop-container-by-id:
|
||||
$(CONTAINER_COMMAND) stop $(CONTAINER_ID)
|
||||
# CONTAINER_ID=`grep 'CONTAINER_ID=' $(CONTAINER_FILE) | sed 's/CONTAINER_ID=//'` ; \
|
||||
# $(MAKE) stop-container-by-id CONTAINER_ID=$${CONTAINER_ID}
|
||||
|
||||
.PHONY: new-develop
|
||||
# HELP: instantiate new development bundle, i.e. create new directory under develop/
|
||||
new-develop:
|
||||
$(NEW_DEVELOP)
|
||||
mkdir -p $(DEVELOP)
|
||||
$(MAKE) develop/.current
|
||||
.PHONY: switch-develop
|
||||
# HELP: switch current develop instance to DEVELOP=...
|
||||
switch-develop:
|
||||
if ! [ -e develop/$(DEVELOP) ]; then \
|
||||
echo "Specified develop $(DEVELOP) does not exist! Not switching." ; \
|
||||
exit 1 ; \
|
||||
fi ; \
|
||||
echo "$(DEVELOP)" > develop/.current
|
||||
--ensure-develop:
|
||||
if ! [[ -e develop ]]; then \
|
||||
$(MAKE) new-develop; \
|
||||
fi
|
||||
$(MAKE) develop/.current
|
||||
$(SET_DEVELOP)
|
||||
.PHONY: develop/.current
|
||||
develop/.current:
|
||||
ls -1 develop | tail -n1 > develop/.current
|
||||
|
||||
stack.yaml.lock: --dependencies-backend;
|
||||
|
||||
.PHONY: --%
|
||||
.SUFFIXES: # Delete all default suffixes
|
||||
|
||||
@ -29,7 +29,6 @@
|
||||
"file-upload": "file-arrow-up",
|
||||
"file-zip": "file-zipper",
|
||||
"file-csv": "file-csv",
|
||||
"file-missing": "file-circle-minus",
|
||||
"sft-question": "circle-question",
|
||||
"sft-hint": "life-ring",
|
||||
"sft-solution": "circle-exclamation",
|
||||
@ -77,7 +76,7 @@
|
||||
"submission-no-users": "user-slash",
|
||||
"reset": "arrow-rotate-left",
|
||||
"blocked": "ban",
|
||||
"certificate": "car-side",
|
||||
"certificate": "certificate",
|
||||
"print-center": "envelopes-bulk",
|
||||
"letter": "envelopes-bulk",
|
||||
"at": "at",
|
||||
@ -91,18 +90,12 @@
|
||||
"trash": "trash",
|
||||
"reset-tries": "trash-can-arrow-up",
|
||||
"company": "building",
|
||||
"company-warning": "building-circle-exclamation",
|
||||
"edit": "pen-to-square",
|
||||
"user-edit": "user-pen",
|
||||
"loading": "spinner",
|
||||
"placeholder": "notdef",
|
||||
"reroute": "diamond-turn-right",
|
||||
"top": "award",
|
||||
"wildcard": "asterisk",
|
||||
"user-unknown": "user-slash",
|
||||
"user-badge": "id-badge",
|
||||
"glasses": "glasses",
|
||||
"missing": "question",
|
||||
"pin-protect": "key"
|
||||
"wildcard": "asterisk"
|
||||
}
|
||||
|
||||
|
||||
239
azure-pipelines.yaml
Executable file → Normal file
239
azure-pipelines.yaml
Executable file → Normal file
@ -1,197 +1,52 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
# SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
#paths:
|
||||
# exclude:
|
||||
# - CHANGELOG.md
|
||||
|
||||
parameters:
|
||||
- name: services
|
||||
type: object
|
||||
default:
|
||||
- name: frontend
|
||||
imageBase:
|
||||
image: devfra.azurecr.io/de.fraport.build/npm
|
||||
tag: node-20
|
||||
# extraBuildOptions: |
|
||||
# --build-arg NPM_CUSTOM_REGISTRY=https://pkgs.dev.azure.com/fraport/_packaging/packages/npm/registry/
|
||||
dependsOn: []
|
||||
dependenciesCaches:
|
||||
- key: package.json | package-lock.json
|
||||
path: node_modules/
|
||||
- key: package.json | package-lock.json
|
||||
path: .npm/
|
||||
- key: package.json | esbuild.config.mjs | utils/renamer.pl | utils/faviconize.pl | frontend/src/icons.scss
|
||||
path: assets/icons/
|
||||
- key: package.json | esbuild.config.mjs | utils/renamer.pl | utils/faviconize.pl | frontend/src/icons.scss
|
||||
path: assets/favicons/
|
||||
buildPool: 'Prod Private Agent Pool'
|
||||
buildCores: 1
|
||||
buildTimeout: 60
|
||||
buildArtifacts: |
|
||||
assets/icons/fradrive/*.svg
|
||||
assets/favicons/*.png
|
||||
assets/favicons/include.html
|
||||
frontend/src/env.sass
|
||||
config/manifest.json
|
||||
static/**/*
|
||||
well-known/**/*
|
||||
- name: backend
|
||||
imageBase:
|
||||
image: devfra.azurecr.io/de.fraport.build/haskell
|
||||
tag: 8.10.4
|
||||
dependsOn:
|
||||
- Build_frontend
|
||||
dependenciesCaches:
|
||||
- key: stack.yaml | stack.yaml.lock
|
||||
path: .stack/
|
||||
buildPool: 'Prod Private Agent Pool DS3'
|
||||
buildCores: 3
|
||||
buildTimeout: 1440
|
||||
buildArtifacts: |
|
||||
bin/*
|
||||
|
||||
variables:
|
||||
buildImageUpstream: devfra.azurecr.io/de.fraport.fradrive.build
|
||||
setupImages: $[ or( eq(variables.forcePushLatest, true), eq(variables['Build.SourceBranch'], 'refs/heads/master'), startsWith(variables['Build.SourceBranch'], 'refs/heads/update'), startsWith(variables['Build.SourceBranch'], 'refs/tags/') ) ]
|
||||
|
||||
pool: 'Prod Private Agent Pool'
|
||||
|
||||
stages:
|
||||
|
||||
- stage: Setup
|
||||
jobs:
|
||||
- ${{ each service in parameters.services }}:
|
||||
- template: .azure-pipelines/templates/jobs/setup_image.yaml
|
||||
parameters:
|
||||
imageName: ${{service.name}}
|
||||
imageBase: ${{service.imageBase}}
|
||||
- template: .azure-pipelines/templates/jobs/setup_dependencies.yaml
|
||||
parameters:
|
||||
serviceName: ${{service.name}}
|
||||
dependenciesCaches: ${{service.dependenciesCaches}}
|
||||
dependenciesBuildPool: ${{service.buildPool}}
|
||||
dependenciesBuildCores: ${{service.buildCores}}
|
||||
dependenciesBuildTimeout: ${{service.buildTimeout}}
|
||||
|
||||
- stage: Build
|
||||
dependsOn: Setup
|
||||
jobs:
|
||||
- ${{ each service in parameters.services }}:
|
||||
- job: Build_${{service.name}}
|
||||
displayName: Compile ${{service.name}}
|
||||
dependsOn: ${{service.dependsOn}}
|
||||
pool: '${{service.buildPool}}'
|
||||
timeoutInMinutes: ${{service.buildTimeout}}
|
||||
container:
|
||||
${{ if eq(variables.setupImages, true) }}:
|
||||
image: $(buildImageUpstream)/${{service.name}}:$(Build.BuildNumber)
|
||||
${{ else }}:
|
||||
image: $(buildImageUpstream)/${{service.name}}:latest
|
||||
endpoint: devfra
|
||||
env:
|
||||
PROJECT_DIR: $(Build.Repository.LocalPath)
|
||||
IN_CONTAINER: true
|
||||
IN_CI: true
|
||||
steps:
|
||||
- ${{ each dependencyCache in service.dependenciesCaches }}:
|
||||
- template: .azure-pipelines/templates/steps/cache.yaml
|
||||
parameters:
|
||||
cacheIdent: '${{service.name}}-dependencies'
|
||||
cacheKeys: '${{dependencyCache.key}}'
|
||||
cachePath: '${{dependencyCache.path}}'
|
||||
- ${{ each dependency in service.dependsOn }}:
|
||||
- template: .azure-pipelines/templates/steps/artifact-download.yaml
|
||||
parameters:
|
||||
artifactName: '${{dependency}}'
|
||||
- template: .azure-pipelines/templates/steps/make.yaml
|
||||
parameters:
|
||||
makeJob: compile
|
||||
makeService: ${{service.name}}
|
||||
makeVars: 'CPU_CORES=${{service.buildCores}} STACK_CORES=-j${{service.buildCores}}'
|
||||
- task: CopyFiles@2
|
||||
displayName: Prepare ${{service.name}} build artifacts for upload
|
||||
inputs:
|
||||
Contents: '${{service.buildArtifacts}}'
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: Publish ${{service.name}} build artifacts
|
||||
inputs:
|
||||
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
||||
ArtifactName: 'Build_${{service.name}}'
|
||||
publishLocation: 'Container'
|
||||
|
||||
# - stage: Test
|
||||
# dependsOn: Build
|
||||
# condition: eq(variables.skipTests, false)
|
||||
# jobs:
|
||||
# - ${{ each service in parameters.services }}:
|
||||
# - job: Test_${{service.name}}
|
||||
# displayName: Run ${{service.name}} tests
|
||||
# pool: '${{service.buildPool}}'
|
||||
# timeoutInMinutes: ${{service.buildTimeout}}
|
||||
# container:
|
||||
# # TODO: do not use latest on update branches
|
||||
# image: $(buildImageUpstream)/${{service.name}}:latest
|
||||
# endpoint: devfra
|
||||
# env:
|
||||
# PROJECT_DIR: $(Build.Repository.LocalPath)
|
||||
# IN_CONTAINER: true
|
||||
# IN_CI: true
|
||||
# steps:
|
||||
# - ${{ each dependencyCache in service.dependenciesCaches }}:
|
||||
# - template: .azure-pipelines/templates/steps/cache.yaml
|
||||
# parameters:
|
||||
# cacheIdent: '${{service.name}}-dependencies'
|
||||
# cacheKeys: '${{dependencyCache.key}}'
|
||||
# cachePath: '${{dependencyCache.path}}'
|
||||
# - ${{ each dependency in service.dependsOn }}:
|
||||
# - template: .azure-pipelines/templates/steps/artifact-download.yaml
|
||||
# parameters:
|
||||
# artifactName: '${{dependency}}'
|
||||
# - task: Docker@2
|
||||
# displayName: Login to container registry
|
||||
# inputs:
|
||||
# command: login
|
||||
# containerRegistry: devfra
|
||||
# - task: Bash@3
|
||||
# displayName: Start database container for testing
|
||||
# inputs:
|
||||
# targetType: inline
|
||||
# script: |
|
||||
# docker run -d devfra.azurecr.io/de.fraport.trusted/postgres:16.1-bookworm
|
||||
# - template: .azure-pipelines/templates/steps/make.yaml
|
||||
# parameters:
|
||||
# makeJob: lint
|
||||
# makeService: ${{service.name}}
|
||||
# makeVars: 'CPU_CORES=${{service.buildCores}} STACK_CORES=-j${{service.buildCores}}'
|
||||
# - template: .azure-pipelines/templates/steps/make.yaml
|
||||
# parameters:
|
||||
# makeJob: test
|
||||
# makeService: ${{service.name}}
|
||||
# makeVars: 'CPU_CORES=${{service.buildCores}} STACK_CORES=-j${{service.buildCores}}'
|
||||
# - task: Docker@2
|
||||
# displayName: Logout from container registry
|
||||
# inputs:
|
||||
# command: logout
|
||||
# containerRegistry: devfra
|
||||
# - job: TestReport_${{service.name}}
|
||||
# displayName: Upload test reports for ${{service.name}}
|
||||
# steps:
|
||||
# - script: echo "Work in progress" # TODO
|
||||
|
||||
- stage: Release
|
||||
dependsOn: Build # TODO Test
|
||||
condition: or(eq(variables.forceRelease, true), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
|
||||
jobs:
|
||||
- template: .azure-pipelines/templates/jobs/release.yaml
|
||||
parameters:
|
||||
releaseTag: ${{split(variables['Build.SourceBranch'], '/')[2]}}
|
||||
jobs:
|
||||
# - job: HelloWorld
|
||||
# container:
|
||||
# image: 'devfra.azurecr.io/de.fraport.trusted/ubuntu:22.04'
|
||||
# endpoint: devfra
|
||||
# steps:
|
||||
# - script: echo Hello, world!
|
||||
# displayName: 'Run a one-line script'
|
||||
# - script: |
|
||||
# echo Add other tasks to build, test, and deploy your project.
|
||||
# echo See https://aka.ms/yaml
|
||||
# displayName: 'Run a multi-line script'
|
||||
# - job: DockerTaskTest
|
||||
# container:
|
||||
# image: 'devfra.azurecr.io/de.fraport.trusted/ubuntu:22.04'
|
||||
# endpoint: devfra
|
||||
# steps:
|
||||
# - task: Docker@2
|
||||
# displayName: Image build test
|
||||
# inputs:
|
||||
# command: buildAndPush
|
||||
# Dockerfile: docker/backend/Dockerfile
|
||||
# buildContext: .
|
||||
# tags: backend
|
||||
# - job: BuildKitTest
|
||||
# container:
|
||||
# image: 'devfra.azurecr.io/de.fraport.trusted/buildkit:0.12.1'
|
||||
# endpoint: devfra
|
||||
# steps:
|
||||
# - script: buildctl build \
|
||||
# --frontend=dockerfile.v0 \
|
||||
# --local context=. \
|
||||
# --local dockerfile=docker/backend/Dockerfile
|
||||
# displayName: BuildKit test
|
||||
- job: CustomBuildahTest
|
||||
container:
|
||||
image: 'devfra.azurecr.io/de.fraport.trusted/ubuntu:22.04'
|
||||
endpoint: devfra
|
||||
steps:
|
||||
- script: |
|
||||
id
|
||||
docker build --help
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install buildah
|
||||
buildah bud -t fradrive-backend-test --volume .:/mnt/fradrive --file docker/backend/Dockerfile
|
||||
displayName: Build buildah image
|
||||
@ -1,40 +0,0 @@
|
||||
ARG FROM_IMG=docker.io/library/debian
|
||||
ARG FROM_TAG=12.5
|
||||
|
||||
FROM ${FROM_IMG}:${FROM_TAG}
|
||||
|
||||
ENV LANG=de_DE.UTF-8
|
||||
|
||||
# basic dependencies
|
||||
RUN apt-get -y update && apt-get -y install git
|
||||
RUN apt-get -y update && apt-get -y install haskell-stack
|
||||
RUN apt-get -y update && apt-get -y install llvm
|
||||
RUN apt-get -y update && apt-get install -y --no-install-recommends locales locales-all
|
||||
|
||||
# compile-time dependencies
|
||||
RUN apt-get -y update && apt-get install -y libpq-dev libsodium-dev
|
||||
RUN apt-get -y update && apt-get -y install g++ libghc-zlib-dev libpq-dev libsodium-dev pkg-config
|
||||
RUN apt-get -y update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata
|
||||
|
||||
# run-time dependencies for uniworx binary
|
||||
RUN apt-get -y update && apt-get -y install fonts-roboto
|
||||
# RUN apt-get -y update && apt-get -y install pdftk
|
||||
# RUN apt-get -y update && apt-get -y install \
|
||||
# texlive texlive-latex-recommended texlive-luatex texlive-plain-generic texlive-lang-german texlive-lang-english
|
||||
RUN apt-get -y update && apt-get -y install texlive
|
||||
# RUN ls /usr/local/texlive
|
||||
# RUN chown -hR root /usr/local/texlive/2018
|
||||
# RUN tlmgr init-usertree
|
||||
# RUN tlmgr option repository ftp://tug.org/historic/systems/texlive/2018/tlnet-final
|
||||
# RUN tlmgr update --self --all
|
||||
|
||||
ARG PROJECT_DIR=/fradrive
|
||||
ENV PROJECT_DIR=${PROJECT_DIR}
|
||||
# RUN mkdir -p "${PROJECT_DIR}"; chmod -R 777 "${PROJECT_DIR}"
|
||||
WORKDIR ${PROJECT_DIR}
|
||||
ENV HOME=${PROJECT_DIR}
|
||||
ENV STACK_ROOT="${PROJECT_DIR}/.stack"
|
||||
|
||||
ENV STACK_SRC=""
|
||||
ENV STACK_ENTRY="ghci ${STACK_SRC}"
|
||||
ENTRYPOINT stack ${STACK_ENTRY}
|
||||
@ -1,51 +0,0 @@
|
||||
export CPU_CORES = $(shell cat /proc/cpuinfo | grep '^processor' | wc -l)
|
||||
export STACK_CORES = $(shell echo $(($(CPU_CORES)/2)))
|
||||
|
||||
ifeq ($(PROD),true)
|
||||
export --DEVELOPMENT=--flag uniworx:-dev
|
||||
else
|
||||
export --DEVELOPMENT=--flag uniworx:dev
|
||||
endif
|
||||
|
||||
.PHONY: dependencies
|
||||
dependencies:
|
||||
stack install hpack; stack install yesod-bin; \
|
||||
stack build -j2 --only-dependencies
|
||||
|
||||
.PHONY: compile
|
||||
compile: dependencies
|
||||
stack build $(STACK_CORES) --fast --profile --library-profiling --executable-profiling --flag uniworx:-library-only $(--DEVELOPMENT) --local-bin-path $$(pwd)/bin --copy-bins
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
stack build $(STACK_CORES) --test --fast --flag uniworx:library-only $(--DEVELOPMENT) uniworx:test:hlint
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
stack build $(STACK_CORES) --test --coverage --fast --flag uniworx:library-only $(--DEVELOPMENT)
|
||||
|
||||
# For Reverse Proxy Problem see: https://groups.google.com/g/yesodweb/c/2EO53kSOuy0/m/Lw6tq2VYat4J
|
||||
.PHONY: start
|
||||
start: dependencies
|
||||
export YESOD_IP_FROM_HEADER=true; \
|
||||
export DEV_PORT_HTTP=3000; \
|
||||
export DEV_PORT_HTTPS=3443; \
|
||||
export HOST=127.0.0.1 ; \
|
||||
export PORT=$${PORT:-$${DEV_PORT_HTTP}} ; \
|
||||
export DETAILED_LOGGING=$${DETAILED_LOGGING:-true} ; \
|
||||
export LOG_ALL=$${LOG_ALL:-false} ; \
|
||||
export LOGLEVEL=$${LOGLEVEL:-info} ; \
|
||||
export DUMMY_LOGIN=$${DUMMY_LOGIN:-true} ; \
|
||||
export SERVER_SESSION_ACID_FALLBACK=$${SERVER_SESSION_ACID_FALLBACK:-true} ; \
|
||||
export SERVER_SESSION_COOKIES_SECURE=$${SERVER_SESSION_COOKIES_SECURE:-false} ; \
|
||||
export COOKIES_SECURE=$${COOKIES_SECURE:-false} ; \
|
||||
export ALLOW_DEPRECATED=$${ALLOW_DEPRECATED:-true} ; \
|
||||
export ENCRYPT_ERRORS=$${ENCRYPT_ERRORS:-false} ; \
|
||||
export RIBBON=$${RIBBON:-$${HOST:-localhost}} ; \
|
||||
export APPROOT=$${APPROOT:-http://localhost:$${DEV_PORT_HTTP}} ; \
|
||||
export AVSPASS=$${AVSPASS:-nopasswordset} ; \
|
||||
stack $(STACK_CORES) exec --local-bin-path $$(pwd)/bin --copy-bins -- yesod devel -p "$${DEV_PORT_HTTP}" -q "$${DEV_PORT_HTTPS}"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf .stack-work .stack uniworx.cabal .ghc
|
||||
@ -1,38 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2022 Winnie Ros <winnie.ros@campus.lmu.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
MaterialList: Material
|
||||
MaterialName: Name
|
||||
MaterialType: Type
|
||||
MaterialTypePlaceholder: Slides, Code, Example, ...
|
||||
MaterialTypeSlides: Slides
|
||||
MaterialTypeCode: Code
|
||||
MaterialTypeExample: Example
|
||||
MaterialDescription: Description
|
||||
MaterialVisibleFrom: Visible to participants from
|
||||
MaterialVisibleFromTip: Never visible to participants if left empty; leaving the date empty is only sensible for unfinished course category material or when course category material should be provided only to sheet correctors
|
||||
MaterialVisibleFromEditWarning: This course category material has already been published and should not be edited. Doing so might confuse the participants.
|
||||
MaterialInvisible: This course category material is currently invisible to participants!
|
||||
MaterialFiles: Files
|
||||
MaterialHeading materialName: #{materialName}
|
||||
MaterialListHeading: Course category materials
|
||||
MaterialNewHeading: Publish new course category material
|
||||
MaterialNewTitle: New course category material
|
||||
MaterialEditHeading materialName: Edit course category material “#{materialName}”
|
||||
MaterialEditTitle materialName: Edit course category material “#{materialName}”
|
||||
MaterialSaveOk tid ssh csh materialName: Successfully saved “#{materialName}” for course category #{tid}-#{ssh}-#{csh}
|
||||
MaterialNameDup tid ssh csh materialName: Course category material with the name “#{materialName}” already exists for course category #{tid}-#{ssh}-#{csh}
|
||||
MaterialDeleteCaption: Do you really want to delete the course category material mentioned below?
|
||||
MaterialDelHasFiles count: including #{count} #{pluralEN count "file" "files"}
|
||||
MaterialIsVisible: Caution, this course category material has already been published.
|
||||
MaterialDeleted materialName: Successfully deleted course category material “#{materialName}”
|
||||
MaterialArchiveName tid ssh csh materialName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase materialName}
|
||||
MaterialVideo materialName: #{materialName} - Video
|
||||
MaterialVideoUnsupported: Your browser does not seem to support embedded video
|
||||
MaterialVideoDownload: Download
|
||||
MaterialFree: Course category material is publicly available.
|
||||
AccessibleSince: Accessible since
|
||||
VisibleFrom: Published
|
||||
FilterMaterialNameSearch !ident-ok: Name
|
||||
FilterMaterialTypeAndDescriptionSearch: Type/description
|
||||
@ -1,77 +0,0 @@
|
||||
{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-}
|
||||
|
||||
-- SPDX-FileCopyrightText: 2024-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-- | Missing instances for saltine@0.1.1.1, backported from saltine@0.2.0.0
|
||||
module Crypto.Saltine.Instances () where
|
||||
|
||||
import ClassyPrelude hiding (compare)
|
||||
|
||||
import Crypto.Saltine.Class
|
||||
import Crypto.Saltine.Core.Auth hiding (Key)
|
||||
import Crypto.Saltine.Core.SecretBox
|
||||
import Crypto.Saltine.Core.Hash
|
||||
import Crypto.Saltine.Internal.ByteSizes (shorthashKey)
|
||||
|
||||
import Data.ByteString.Unsafe
|
||||
import Foreign.C
|
||||
import Foreign.Marshal.Alloc (mallocBytes)
|
||||
import Foreign.Ptr
|
||||
import System.IO.Unsafe
|
||||
|
||||
import qualified Data.ByteString as S
|
||||
import qualified Data.ByteString.Char8 as S8
|
||||
import Data.Maybe (fromJust)
|
||||
|
||||
|
||||
-- | Used for our `Show` instances
|
||||
nullShKey :: ShorthashKey
|
||||
nullShKey = fromJust . decode $ S8.replicate shorthashKey '\NUL'
|
||||
|
||||
-- | Extremely unsafe function, use with utmost care! Builds a new
|
||||
-- ByteString using a ccall which is given access to the raw underlying
|
||||
-- pointer. Overwrites are UNCHECKED and 'unsafePerformIO' is used so
|
||||
-- it's difficult to predict the timing of the 'ByteString' creation.
|
||||
buildUnsafeByteString :: Int -> (Ptr CChar -> IO b) -> (b, ByteString)
|
||||
buildUnsafeByteString n = unsafePerformIO . buildUnsafeByteString' n
|
||||
|
||||
-- | Slightly safer cousin to 'buildUnsafeByteString' that remains in the
|
||||
-- 'IO' monad.
|
||||
buildUnsafeByteString' :: Int -> (Ptr CChar -> IO b) -> IO (b, ByteString)
|
||||
buildUnsafeByteString' n k = do
|
||||
ph <- mallocBytes n
|
||||
bs <- unsafePackMallocCStringLen (ph, n)
|
||||
out <- unsafeUseAsCString bs k
|
||||
return (out, bs)
|
||||
|
||||
-- | Convenience function for accessing constant C strings
|
||||
constByteStrings :: [ByteString] -> ([CStringLen] -> IO b) -> IO b
|
||||
constByteStrings =
|
||||
foldr (\v kk k -> unsafeUseAsCStringLen v (\a -> kk (\as -> k (a:as)))) ($ [])
|
||||
|
||||
-- | bin2hex conversion for showing various binary types
|
||||
foreign import ccall unsafe "sodium_bin2hex"
|
||||
c_sodium_bin2hex
|
||||
:: Ptr CChar -- Target zone
|
||||
-> CInt -- Max. length of target string (must be min. bin_len * 2 + 1)
|
||||
-> Ptr CChar -- Source
|
||||
-> CInt -- Source length
|
||||
-> IO (Ptr CChar)
|
||||
|
||||
bin2hex :: ByteString -> String
|
||||
bin2hex bs = let tlen = S.length bs * 2 + 1 in
|
||||
S8.unpack . S8.init . snd . buildUnsafeByteString tlen $ \t ->
|
||||
let aux [(pbs, _)] = c_sodium_bin2hex t (fromIntegral tlen) pbs (fromIntegral $ S.length bs)
|
||||
aux _ = error "Crypto.Saltine.Instances.bin2hex reached an impossible computation path"
|
||||
in constByteStrings [bs] aux
|
||||
|
||||
instance Show Key where
|
||||
show k = "SecretBox.Key {hashesTo = \"" <> (bin2hex . shorthash nullShKey $ encode k) <> "}\""
|
||||
|
||||
instance Show Nonce where
|
||||
show k = "SecretBox.Nonce " <> bin2hex (encode k)
|
||||
|
||||
instance Show Authenticator where
|
||||
show k = "Sign.Authenticator " <> bin2hex (encode k)
|
||||
@ -1,53 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.systems>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
module Foundation.Yesod.StaticContent
|
||||
( addStaticContent
|
||||
) where
|
||||
|
||||
import Import.NoFoundation hiding (addStaticContent)
|
||||
|
||||
import Foundation.Type
|
||||
|
||||
import qualified Database.Memcached.Binary.IO as Memcached
|
||||
|
||||
import qualified Data.ByteString.Lazy as Lazy
|
||||
import qualified Data.ByteString.Base64.URL as Base64 (encodeUnpadded)
|
||||
import Data.ByteArray (convert)
|
||||
import Crypto.Hash (SHAKE256)
|
||||
import Crypto.Hash.Conduit (sinkHash)
|
||||
import Data.Bits (Bits(zeroBits))
|
||||
|
||||
import qualified Data.Conduit.Combinators as C
|
||||
|
||||
|
||||
addStaticContent :: Text
|
||||
-> Text
|
||||
-> Lazy.ByteString
|
||||
-> HandlerFor UniWorX (Maybe (Either Text (Route UniWorX, [(Text, Text)])))
|
||||
addStaticContent ext _mime content = do
|
||||
UniWorX{appWidgetMemcached, appSettings'} <- getYesod
|
||||
for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConf = MemcachedConf { memcachedExpiry }, widgetMemcachedBaseUrl }) -> do
|
||||
let expiry = maybe 0 ceiling memcachedExpiry
|
||||
touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn
|
||||
addItem = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn
|
||||
absoluteLink = unpack widgetMemcachedBaseUrl </> fileName
|
||||
catchIf Memcached.isKeyNotFound touch . const $
|
||||
handleIf Memcached.isKeyExists (const $ return ()) addItem
|
||||
return . Left $ pack absoluteLink
|
||||
where
|
||||
-- Generate a unique filename based on the content itself, this is used
|
||||
-- for deduplication so a collision resistant hash function is required
|
||||
--
|
||||
-- SHA-3 (SHAKE256) seemed to be a future-proof choice
|
||||
--
|
||||
-- Length of hash is 144 bits ~~instead of MD5's 128, so as to avoid
|
||||
-- padding after base64-conversion~~ for backwards compatibility
|
||||
fileName = (<.> unpack ext)
|
||||
. unpack
|
||||
. decodeUtf8
|
||||
. Base64.encodeUnpadded
|
||||
. (convert :: Digest (SHAKE256 144) -> ByteString)
|
||||
. runConduitPure
|
||||
$ C.sourceLazy content .| sinkHash
|
||||
@ -1,234 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2023-2025 Steffen Jost <S.Jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Handler.Firm.Supervision
|
||||
( getFirmsSupervisionR , postFirmsSupervisionR
|
||||
)
|
||||
where
|
||||
|
||||
import Import
|
||||
|
||||
-- import Jobs
|
||||
import Utils.Company
|
||||
import Handler.Utils
|
||||
import Handler.Utils.Company
|
||||
|
||||
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Map as Map
|
||||
-- import qualified Data.Csv as Csv
|
||||
-- import qualified Data.Text as T
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
-- import qualified Data.Conduit.List as C
|
||||
-- import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
|
||||
import Database.Persist.Postgresql
|
||||
import Database.Esqueleto.Experimental ((:&)(..))
|
||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||
import qualified Database.Esqueleto.Legacy as EL (on) -- needed for legacy join expected by dbTable
|
||||
-- import qualified Database.Esqueleto.PostgreSQL as E
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
-- import Database.Esqueleto.Utils.TH
|
||||
|
||||
|
||||
-- decryptUser :: (MonadHandler m, HandlerSite m ~ UniWorX) => CryptoUUIDUser -> m UserId
|
||||
-- decryptUser = decrypt
|
||||
|
||||
-- encryptUser :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m CryptoUUIDUser
|
||||
-- encryptUser = encrypt
|
||||
|
||||
|
||||
-----------------------
|
||||
-- Supervision Sanity
|
||||
|
||||
data ActSupervision = ASChangeCompany | ASRemoveAssociation
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
deriving anyclass (Universe, Finite)
|
||||
|
||||
nullaryPathPiece ''ActSupervision $ camelToPathPiece' 2
|
||||
embedRenderMessage ''UniWorX ''ActSupervision id
|
||||
|
||||
data ActSupervisionData
|
||||
= ASChangeCompanyData { asTblCompany :: Maybe CompanyShorthand, asTblReason :: Maybe Text }
|
||||
| ASRemoveAssociationData
|
||||
deriving (Eq, Ord, Read, Show, Generic)
|
||||
|
||||
data SupervisionViolation = SupervisionViolationEither | SupervisionViolationClient | SupervisionViolationSupervisor | SupervisionViolationBoth
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
deriving anyclass (Universe, Finite)
|
||||
|
||||
nullaryPathPiece ''SupervisionViolation $ camelToPathPiece' 1
|
||||
embedRenderMessage ''UniWorX ''SupervisionViolation id
|
||||
|
||||
supervisionViolationField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m SupervisionViolation
|
||||
-- supervisionViolationField = radioGroupField (Just $ SomeMessage MsgSupervisionViolationEither) $ optionsFinite
|
||||
supervisionViolationField = radioGroupField Nothing $ optionsFinite
|
||||
|
||||
type TblSupervisionData = DBRow (Entity UserSupervisor, Entity User, Entity User)
|
||||
|
||||
mkSupervisionTable :: DB (FormResult (ActSupervisionData, Set UserSupervisorId), Widget)
|
||||
mkSupervisionTable = over _1 postprocess <$> dbTable validator DBTable{..}
|
||||
where
|
||||
dbtIdent = "sanity-super" :: Text
|
||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout}
|
||||
|
||||
queryRelation :: (E.SqlExpr (Entity UserSupervisor) `E.InnerJoin` E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity UserSupervisor)
|
||||
queryRelation = $(E.sqlIJproj 3 1)
|
||||
querySupervisor :: (E.SqlExpr (Entity UserSupervisor) `E.InnerJoin` E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity User)
|
||||
querySupervisor = $(E.sqlIJproj 3 2)
|
||||
queryClient :: (E.SqlExpr (Entity UserSupervisor) `E.InnerJoin` E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity User)
|
||||
queryClient = $(E.sqlIJproj 3 3)
|
||||
|
||||
resultRelation :: Lens' TblSupervisionData (Entity UserSupervisor)
|
||||
resultRelation = _dbrOutput . _1
|
||||
resultSupervisor :: Lens' TblSupervisionData (Entity User)
|
||||
resultSupervisor = _dbrOutput . _2
|
||||
resultClient :: Lens' TblSupervisionData (Entity User)
|
||||
resultClient = _dbrOutput . _3
|
||||
|
||||
dbtSQLQuery (uus `E.InnerJoin` spr `E.InnerJoin` sub) = do
|
||||
EL.on $ uus E.^. UserSupervisorSupervisor E.==. spr E.^. UserId
|
||||
EL.on $ uus E.^. UserSupervisorUser E.==. sub E.^. UserId
|
||||
E.where_ $ E.isJust (uus E.^. UserSupervisorCompany)
|
||||
return (uus, spr, sub)
|
||||
dbtRowKey = queryRelation >>> (E.^. UserSupervisorId)
|
||||
dbtProj = dbtProjId
|
||||
dbtColonnade = formColonnade $ mconcat
|
||||
[ dbSelect (applying _2) id (return . view (resultRelation . _entityKey))
|
||||
, sortable (Just "reroute") (i18nCell MsgTableRerouteActive) $ \(view $ resultRelation . _entityVal . _userSupervisorRerouteNotifications -> b) -> ifIconCell b IconReroute
|
||||
, sortable (Just "reason") (i18nCell MsgUserSupervisorReason) $ \(view $ resultRelation . _entityVal . _userSupervisorReason -> r) -> maybeCell r textCell
|
||||
, sortable (Just "rel-comp") (i18nCell MsgUserSupervisorCompany) $ \(view $ resultRelation . _entityVal . _userSupervisorCompany -> c) -> maybeCell c companyIdCell\
|
||||
, sortable (Just "supervisor") (i18nCell MsgTableSupervisor) $ \(view $ resultSupervisor -> u) -> cellHasUserModal ForProfileDataR u
|
||||
, sortable (Just "super-comp") (i18nCell MsgTableCompanies) $ \(view $ resultSupervisor . _entityKey -> uid) -> flip (set' cellContents) mempty $ liftHandler $ runDB $ -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX"
|
||||
maybeMonoid <$> wgtCompanies True uid
|
||||
, sortable (Just "client") (i18nCell MsgTableSupervisee) $ \(view $ resultClient -> u) -> cellHasUserModal ForProfileDataR u
|
||||
, sortable (Just "client-comp") (i18nCell MsgTableCompanies) $ \(view $ resultClient . _entityKey -> uid) -> flip (set' cellContents) mempty $ liftHandler $ runDB $ -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX"
|
||||
maybeMonoid <$> wgtCompanies True uid
|
||||
]
|
||||
validator = def & defaultSorting [SortAscBy "rel-comp", SortAscBy "supervisor", SortAscBy "client"]
|
||||
& defaultFilter (singletonMap "violation" [toPathPiece SupervisionViolationEither])
|
||||
dbtSorting = Map.fromList
|
||||
[ ("reason" , SortColumn $ queryRelation >>> (E.^. UserSupervisorReason))
|
||||
, ("rel-comp" , SortColumn $ queryRelation >>> (E.^. UserSupervisorCompany))
|
||||
, ("reroute" , SortColumn $ queryRelation >>> (E.^. UserSupervisorRerouteNotifications))
|
||||
, ("supervisor" , SortColumn $ querySupervisor >>> (E.^. UserDisplayName))
|
||||
, ("client" , SortColumn $ queryClient >>> (E.^. UserDisplayName))
|
||||
, ("super-comp" , SortColumn (\row -> E.subSelect $ do
|
||||
(cmp :& usrCmp) <- E.from $ E.table @Company `E.innerJoin` E.table @UserCompany `E.on` (\(cmp :& usrCmp) -> cmp E.^. CompanyId E.==. usrCmp E.^. UserCompanyCompany)
|
||||
E.where_ $ usrCmp E.^. UserCompanyUser E.==. querySupervisor row E.^. UserId
|
||||
E.orderBy [E.asc $ cmp E.^. CompanyName]
|
||||
return (cmp E.^. CompanyName)
|
||||
))
|
||||
, ("client-comp" , SortColumn (\row -> E.subSelect $ do
|
||||
(cmp :& usrCmp) <- E.from $ E.table @Company `E.innerJoin` E.table @UserCompany `E.on` (\(cmp :& usrCmp) -> cmp E.^. CompanyId E.==. usrCmp E.^. UserCompanyCompany)
|
||||
E.where_ $ usrCmp E.^. UserCompanyUser E.==. queryClient row E.^. UserId
|
||||
E.orderBy [E.asc $ cmp E.^. CompanyName]
|
||||
return (cmp E.^. CompanyName)
|
||||
))
|
||||
]
|
||||
|
||||
dbtFilter = Map.fromList
|
||||
[ ("violation", FilterColumn $ \(queryRelation -> us) (getLast -> criterion) -> case criterion of
|
||||
Just SupervisionViolationSupervisor -> missingCompanySupervisor us
|
||||
Just SupervisionViolationClient -> missingCompanyClient us
|
||||
Just SupervisionViolationBoth -> missingCompanySupervisor us E.&&. missingCompanyClient us
|
||||
_ -> missingCompanySupervisor us E.||. missingCompanyClient us
|
||||
)
|
||||
, ("rel-company", FilterColumn $ E.mkExistsFilter $ \(queryRelation -> us) (commaSeparatedText -> criteria) -> do
|
||||
let numCrits = setMapMaybe readMay criteria
|
||||
cmp <- E.from $ E.table @Company
|
||||
E.where_ $ cmp E.^. CompanyId E.=?. us E.^. UserSupervisorCompany
|
||||
E.&&. E.or (
|
||||
bcons (notNull numCrits)
|
||||
(E.mkExactFilter (E.^. CompanyAvsId) cmp numCrits)
|
||||
[E.mkContainsFilterWith CI.mk (E.^. CompanyName) cmp criteria
|
||||
,E.mkContainsFilterWith CI.mk (E.^. CompanyShorthand) cmp criteria
|
||||
]
|
||||
)
|
||||
)
|
||||
, ("supervisor-company", fltrCompanyShortNrUsr (querySupervisor >>> (E.^. UserId)))
|
||||
, ("client-company" , fltrCompanyShortNrUsr (queryClient >>> (E.^. UserId)))
|
||||
, ("supervisor", FilterColumn . E.mkContainsFilter $ querySupervisor >>> (E.^. UserDisplayName))
|
||||
, ("client" , FilterColumn . E.mkContainsFilter $ queryClient >>> (E.^. UserDisplayName))
|
||||
]
|
||||
dbtFilterUI mPrev = mconcat -- Maybe (Map FilterKey [Text]) -> AForm DB (Map FilterKey [Text])
|
||||
[ prismAForm (singletonFilter "violation" . maybePrism _PathPiece) mPrev $ aopt supervisionViolationField (fslI MsgSupervisionViolationChoice)
|
||||
, prismAForm (singletonFilter "rel-company") mPrev $ aopt textField (fslI MsgUserSupervisorCompany & setTooltip MsgTableFilterCommaNameNr)
|
||||
, prismAForm (singletonFilter "supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
|
||||
, fltrCompanyNameNrUsrHdrUI "supervisor-company" (someMessages [MsgTableSupervisor, MsgTableCompanyShort]) mPrev
|
||||
, prismAForm (singletonFilter "client") mPrev $ aopt textField (fslI MsgTableSupervisee)
|
||||
, fltrCompanyNameNrUsrHdrUI "client-company" (someMessages [MsgTableSupervisee, MsgTableCompanyShort]) mPrev
|
||||
]
|
||||
|
||||
suggestionSupervision :: Handler (OptionList Text)
|
||||
suggestionSupervision = mkOptionListText <$> runDB
|
||||
(E.select $ do
|
||||
us <- E.from $ E.table @UserSupervisor
|
||||
let reason = us E.^. UserSupervisorReason
|
||||
countRows' :: E.SqlExpr (E.Value Int64) = E.countRows
|
||||
E.where_ $ E.isJust reason
|
||||
E.groupBy reason
|
||||
E.orderBy [E.desc countRows']
|
||||
E.limit 9
|
||||
pure $ E.coalesceDefault [reason] (E.val "")
|
||||
)
|
||||
dbtParams = DBParamsForm
|
||||
{ dbParamsFormMethod = POST
|
||||
, dbParamsFormAction = Nothing
|
||||
, dbParamsFormAttrs = []
|
||||
, dbParamsFormSubmit = FormSubmit
|
||||
, dbParamsFormAdditional =
|
||||
let acts :: Map ActSupervision (AForm Handler ActSupervisionData)
|
||||
acts = mconcat
|
||||
[ singletonMap ASChangeCompany $ ASChangeCompanyData
|
||||
<$> aopt companyField (fslI MsgUserSupervisorCompany) Nothing
|
||||
<*> aopt (textField & cfStrip & addDatalist suggestionSupervision) (fslI MsgUserSupervisorReason & setTooltip MsgStarKeepsEmptyDeletes) (Just $ Just "*")
|
||||
, singletonMap ASRemoveAssociation $ pure ASRemoveAssociationData
|
||||
]
|
||||
in renderAForm FormStandard $ (, mempty) . First . Just <$> multiActionA acts (fslI MsgTableAction) Nothing
|
||||
, dbParamsFormEvaluate = liftHandler . runFormPost
|
||||
, dbParamsFormResult = id
|
||||
, dbParamsFormIdent = def
|
||||
}
|
||||
dbtCsvEncode = noCsvEncode
|
||||
dbtCsvDecode = Nothing
|
||||
dbtExtraReps = []
|
||||
|
||||
postprocess :: FormResult (First ActSupervisionData, DBFormResult UserSupervisorId Bool TblSupervisionData)
|
||||
-> FormResult ( ActSupervisionData, Set UserSupervisorId)
|
||||
postprocess inp = do
|
||||
(First (Just act), jobMap) <- inp
|
||||
let jobSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) jobMap
|
||||
return (act, jobSet)
|
||||
|
||||
|
||||
getFirmsSupervisionR, postFirmsSupervisionR :: Handler Html
|
||||
getFirmsSupervisionR = postFirmsSupervisionR
|
||||
postFirmsSupervisionR = do
|
||||
(svRes, svTbl) <- runDB mkSupervisionTable
|
||||
formResult svRes $ \case
|
||||
(ASRemoveAssociationData, relations) -> do
|
||||
nrDel <- runDB $ deleteWhereCount [UserSupervisorId <-. Set.toList relations]
|
||||
addMessageOutOfI MsgSupervisionsRemoved nrDel $ Set.size relations
|
||||
reloadKeepGetParams FirmsSupervisionR
|
||||
(ASChangeCompanyData{..}, relations) -> do
|
||||
let rsnChg = case asTblReason of
|
||||
Just "*" -> Nothing
|
||||
_ -> Just $ UserSupervisorReason =. asTblReason
|
||||
chgs = mcons rsnChg [UserSupervisorCompany =. CompanyKey <$> canonical asTblCompany]
|
||||
nrChg <- runDB $ updateWhereCount [UserSupervisorId <-. Set.toList relations] chgs
|
||||
addMessageOutOfI MsgSupervisionsEdited nrChg $ Set.size relations
|
||||
reloadKeepGetParams FirmsSupervisionR
|
||||
-- TODO: Bug Firmenwechsel: Bestehende Ansprechpartnerbeziehung - Firma ändern!
|
||||
let heading = MsgMenuFirmsSupervision
|
||||
siteLayoutMsg heading $ do
|
||||
setTitleI heading
|
||||
[whamlet|$newline never
|
||||
<p>
|
||||
_{MsgFirmSupervisionRInfo} In folgenden Ansprechpartnerbeziehungen gehören entweder der Ansprechpartner oder der Angesprochene #
|
||||
nicht mehr der Firma an, welche als Begründung für die Beziehung eingetragen ist:
|
||||
<p>
|
||||
^{svTbl}
|
||||
|]
|
||||
@ -1,115 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2025 Steffen Jost <S.Jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Handler.Qualification.Edit
|
||||
( getQualificationNewR, postQualificationNewR
|
||||
, getQualificationEditR, postQualificationEditR
|
||||
)
|
||||
where
|
||||
|
||||
import Import
|
||||
|
||||
import qualified Data.Text as Text
|
||||
import qualified Control.Monad.State.Class as State
|
||||
|
||||
import Handler.Utils
|
||||
-- import Database.Esqueleto.Experimental ((:&)(..))
|
||||
-- import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||
|
||||
|
||||
getQualificationNewR, postQualificationNewR :: SchoolId -> Handler Html
|
||||
getQualificationNewR = postQualificationNewR
|
||||
postQualificationNewR ssh = handleQualificationEdit ssh Nothing
|
||||
|
||||
getQualificationEditR, postQualificationEditR :: SchoolId -> QualificationShorthand -> Handler Html
|
||||
getQualificationEditR = postQualificationEditR
|
||||
postQualificationEditR ssh qsh = do
|
||||
qent <- runDBRead $ getBy404 $ SchoolQualificationShort ssh qsh
|
||||
handleQualificationEdit ssh $ Just qent
|
||||
|
||||
|
||||
mkQualificationForm :: SchoolId -> Maybe Qualification -> Form Qualification
|
||||
mkQualificationForm ssh templ = identifyForm FIDQualificationEdit . validateForm (validateQualificationEdit ssh) $ \html ->
|
||||
flip (renderAForm FormStandard) html $ reorderedQualification
|
||||
<$> areq hiddenField "" (Just ssh) -- 1 -> 1
|
||||
<*> areq ciField (fslI MsgQualificationShort) (qualificationShorthand <$> templ) -- 2 -> 2
|
||||
<*> areq ciField (fslI MsgQualificationName) (qualificationName <$> templ) -- 3 -> 3
|
||||
<*> aopt htmlField (fslI MsgQualificationDescription) (qualificationDescription <$> templ) -- 4 -> 4
|
||||
<*> aopt_natFieldI MsgQualificationValidDuration (qualificationValidDuration <$> templ) -- 5 -> 5
|
||||
<*> aopt calendarDiffDaysField (fslI MsgQualificationRefreshWithin &
|
||||
setTooltip MsgQualificationRefreshWithinTooltip) (qualificationRefreshWithin <$> templ) -- 6 -> 7
|
||||
|
||||
<*> areq checkBoxField (fslI MsgQualificationElearningStart) (qualificationElearningStart <$> templ) -- 7 -> 9
|
||||
<*> aopt calendarDiffDaysField (fslI MsgQualificationRefreshReminder &
|
||||
setTooltip MsgQualificationRefreshReminderTooltip) (qualificationRefreshReminder <$> templ) -- 8 -> 8
|
||||
<*> areq checkBoxField (fslI MsgQualificationExpiryNotification) (qualificationExpiryNotification <$> templ) -- 9 -> 13
|
||||
<*> areq_natFieldI MsgQualificationAuditDuration (qualificationAuditDuration <$> templ) -- 10 -> 6
|
||||
<*> areq checkBoxField (fslI MsgQualificationElearningRenew) (qualificationElearningRenews <$> templ) -- 11 -> 10
|
||||
<*> aopt_natFieldI MsgQualificationElearningLimit (qualificationElearningLimit <$> templ) -- 12 -> 11
|
||||
<*> aopt qualificationField (fslI MsgTableQualificationLmsReuses &
|
||||
setTooltip MsgTableQualificationLmsReusesTooltip) (qualificationLmsReuses <$> templ) -- 13 -> 12
|
||||
<*> aopt avsLicenceField (fslI MsgQualificationAvsLicence &
|
||||
setTooltip MsgTableQualificationIsAvsLicenceTooltip) (qualificationAvsLicence <$> templ) -- 14 -> 14
|
||||
<*> aopt textField (fslI MsgQualificationSapId &
|
||||
setTooltip MsgTableQualificationSapExportTooltip) (qualificationSapId <$> templ) -- 15 -> 15
|
||||
where
|
||||
avsLicenceField :: Field Handler AvsLicence
|
||||
avsLicenceField = selectFieldList [ (Text.singleton $ licence2char lic, lic) | lic <- universeF, lic /= AvsNoLicence ]
|
||||
|
||||
aopt_natFieldI msg = aopt (natFieldI $ SomeMessages " " [SomeMessage msg, SomeMessage MsgMustBePositive]) (fslI msg)
|
||||
areq_natFieldI msg = areq (natFieldI $ SomeMessages " " [SomeMessage msg, SomeMessage MsgMustBePositive]) (fslI msg)
|
||||
-- [ 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15]
|
||||
reorderedQualification = $(permuteFun [ 1, 2, 3, 4, 5,10, 6, 8, 7,11,12,13, 9,14,15]) Qualification -- == inversePermutation [1,2,3,4,5,7,9,8,13,6,10,11,12,14,15]
|
||||
|
||||
validateQualificationEdit :: SchoolId -> FormValidator Qualification Handler ()
|
||||
validateQualificationEdit ssh = do
|
||||
canonise
|
||||
Qualification{..} <- State.get
|
||||
guardValidation MsgQualFormErrorSshMismatch $ qualificationSchool == ssh
|
||||
guardValidation MsgLmsErrorNoRefreshElearning $ not qualificationElearningStart || isJust qualificationRefreshWithin
|
||||
guardValidation MsgLmsErrorNoRenewElearning $ not qualificationElearningStart || isJust qualificationValidDuration
|
||||
when (isJust qualificationLmsReuses) $
|
||||
liftHandler $ addMessageI Info MsgQualificationAuditDurationReuseInfo
|
||||
where
|
||||
canonise = do -- i.e. map Just 0 to Nothing
|
||||
Qualification{..} <- State.get
|
||||
-- canonisation, i.e. map Just 0 to Nothing
|
||||
when (qualificationRefreshWithin == Just mempty) $ State.modify $ set _qualificationRefreshWithin Nothing
|
||||
when (qualificationRefreshReminder == Just mempty) $ State.modify $ set _qualificationRefreshReminder Nothing
|
||||
when (qualificationValidDuration == Just 0) $ State.modify $ set _qualificationValidDuration Nothing
|
||||
when (qualificationElearningLimit == Just 0) $ State.modify $ set _qualificationElearningLimit Nothing
|
||||
|
||||
|
||||
|
||||
handleQualificationEdit :: SchoolId -> Maybe (Entity Qualification) -> Handler Html
|
||||
handleQualificationEdit ssh templ = do
|
||||
((qRes, qWgt), qEnc) <- runFormPost $ mkQualificationForm ssh $ entityVal <$> templ
|
||||
let qForm = wrapForm qWgt def
|
||||
{ formEncoding = qEnc
|
||||
}
|
||||
formResult qRes $ \resQuali -> do
|
||||
uniqViolation <- runDB $ case templ of
|
||||
Just Entity{entityKey=qid} -> replaceUnique qid resQuali -- edit old qualification
|
||||
_ -> maybeM (checkUnique resQuali) (const $ return Nothing) (insertUnique resQuali) -- insert new qualification
|
||||
case uniqViolation of
|
||||
Just (SchoolQualificationShort _ nconflict) -> addMessageI Error $ MsgQualFormErrorDuplShort $ ciOriginal nconflict
|
||||
Just (SchoolQualificationName _ nconflict) -> addMessageI Error $ MsgQualFormErrorDuplName $ ciOriginal nconflict
|
||||
Nothing -> do
|
||||
let qshort = qualificationShorthand resQuali
|
||||
qmsg = if isNothing templ then MsgQualificationCreated else MsgQualificationEdit
|
||||
addMessageI Success $ qmsg $ ciOriginal qshort
|
||||
redirect $ QualificationR ssh qshort
|
||||
let heading = bool MsgMenuQualificationNew MsgMenuQualificationEdit $ isJust templ
|
||||
siteLayoutMsg heading $ do
|
||||
setTitleI heading
|
||||
[whamlet|
|
||||
<p>
|
||||
^{qForm}
|
||||
$maybe _ <- templ
|
||||
<p>
|
||||
_{MsgQualificationEditNote}
|
||||
|]
|
||||
@ -1,877 +0,0 @@
|
||||
|
||||
-- SPDX-FileCopyrightText: 2024-2025 Steffen Jost <s.jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
|
||||
|
||||
module Handler.School.DayTasks
|
||||
( getSchoolDayR, postSchoolDayR
|
||||
, getSchoolDayCheckR
|
||||
) where
|
||||
|
||||
import Import
|
||||
|
||||
import Handler.Utils
|
||||
import Handler.Utils.Company
|
||||
-- import Handler.Utils.Occurrences
|
||||
import Handler.Utils.Avs
|
||||
import Handler.Utils.Course.Cache
|
||||
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Text as Text
|
||||
|
||||
-- import Database.Persist.Sql (updateWhereCount)
|
||||
import Database.Esqueleto.Experimental ((:&)(..))
|
||||
import qualified Database.Esqueleto.Experimental as E
|
||||
import qualified Database.Esqueleto.PostgreSQL as E
|
||||
import qualified Database.Esqueleto.Legacy as EL (on, from) -- only `on` and `from` are different, needed for dbTable using Esqueleto.Legacy
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
-- import Database.Esqueleto.PostgreSQL.JSON ((@>.))
|
||||
-- import qualified Database.Esqueleto.PostgreSQL.JSON as E hiding ((?.))
|
||||
import Database.Esqueleto.Utils.TH
|
||||
|
||||
|
||||
-- | Maximal number of suggestions for note fields in Day Task view
|
||||
maxSuggestions :: Int64
|
||||
maxSuggestions = 7
|
||||
|
||||
-- data DailyTableAction = DailyActDummy -- just a dummy, since we don't now yet which actions we will be needing
|
||||
-- deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
|
||||
-- instance Universe DailyTableAction
|
||||
-- instance Finite DailyTableAction
|
||||
-- nullaryPathPiece ''DailyTableAction $ camelToPathPiece' 2
|
||||
-- embedRenderMessage ''UniWorX ''DailyTableAction id
|
||||
|
||||
-- data DailyTableActionData = DailyActDummyData
|
||||
-- deriving (Eq, Ord, Read, Show, Generic)
|
||||
|
||||
|
||||
type DailyTableExpr =
|
||||
( E.SqlExpr (Entity Course)
|
||||
`E.InnerJoin` E.SqlExpr (Entity Tutorial)
|
||||
`E.InnerJoin` E.SqlExpr (Entity TutorialParticipant)
|
||||
`E.InnerJoin` E.SqlExpr (Entity User)
|
||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity UserAvs))
|
||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity UserDay))
|
||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity TutorialParticipantDay))
|
||||
)
|
||||
|
||||
type DailyTableOutput = E.SqlQuery
|
||||
( E.SqlExpr (Entity Course)
|
||||
, E.SqlExpr (Entity Tutorial)
|
||||
, E.SqlExpr (Entity TutorialParticipant)
|
||||
, E.SqlExpr (Entity User)
|
||||
, E.SqlExpr (Maybe (Entity UserAvs))
|
||||
, E.SqlExpr (Maybe (Entity UserDay))
|
||||
, E.SqlExpr (Maybe (Entity TutorialParticipantDay))
|
||||
, E.SqlExpr (E.Value (Maybe CompanyId))
|
||||
, E.SqlExpr (E.Value (Maybe [QualificationId]))
|
||||
)
|
||||
|
||||
type DailyTableData = DBRow
|
||||
( Entity Course
|
||||
, Entity Tutorial
|
||||
, Entity TutorialParticipant
|
||||
, Entity User
|
||||
, Maybe (Entity UserAvs)
|
||||
, Maybe (Entity UserDay)
|
||||
, Maybe (Entity TutorialParticipantDay)
|
||||
, E.Value (Maybe CompanyId)
|
||||
, E.Value (Maybe [QualificationId])
|
||||
)
|
||||
|
||||
data DailyFormData = DailyFormData
|
||||
{ dailyFormDrivingPermit :: Maybe UserDrivingPermit
|
||||
, dailyFormEyeExam :: Maybe UserEyeExam
|
||||
, dailyFormParticipantNote :: Maybe Text
|
||||
, dailyFormAttendance :: Bool
|
||||
, dailyFormAttendanceNote :: Maybe Text
|
||||
, dailyFormParkingToken :: Bool
|
||||
} deriving (Eq, Show)
|
||||
|
||||
makeLenses_ ''DailyFormData
|
||||
|
||||
-- force declarations before this point to avoid staging restrictions
|
||||
$(return [])
|
||||
|
||||
|
||||
queryCourse :: DailyTableExpr -> E.SqlExpr (Entity Course)
|
||||
queryCourse = $(sqlMIXproj' ''DailyTableExpr 1)
|
||||
|
||||
queryTutorial :: DailyTableExpr -> E.SqlExpr (Entity Tutorial)
|
||||
queryTutorial = $(sqlMIXproj' ''DailyTableExpr 2)
|
||||
|
||||
queryParticipant :: DailyTableExpr -> E.SqlExpr (Entity TutorialParticipant)
|
||||
queryParticipant = $(sqlMIXproj' ''DailyTableExpr 3)
|
||||
-- queryParticipant = $(sqlMIXproj DAILY_TABLE_JOIN 3) -- reify seems problematic for now
|
||||
|
||||
queryUser :: DailyTableExpr -> E.SqlExpr (Entity User)
|
||||
queryUser = $(sqlMIXproj' ''DailyTableExpr 4)
|
||||
|
||||
queryUserAvs :: DailyTableExpr -> E.SqlExpr (Maybe (Entity UserAvs))
|
||||
queryUserAvs = $(sqlMIXproj' ''DailyTableExpr 5)
|
||||
|
||||
queryUserDay :: DailyTableExpr -> E.SqlExpr (Maybe (Entity UserDay))
|
||||
queryUserDay = $(sqlMIXproj' ''DailyTableExpr 6)
|
||||
|
||||
queryParticipantDay :: DailyTableExpr -> E.SqlExpr (Maybe (Entity TutorialParticipantDay))
|
||||
queryParticipantDay = $(sqlMIXproj' ''DailyTableExpr 7)
|
||||
|
||||
resultCourse :: Lens' DailyTableData (Entity Course)
|
||||
resultCourse = _dbrOutput . _1
|
||||
|
||||
resultTutorial :: Lens' DailyTableData (Entity Tutorial)
|
||||
resultTutorial = _dbrOutput . _2
|
||||
|
||||
resultParticipant :: Lens' DailyTableData (Entity TutorialParticipant)
|
||||
resultParticipant = _dbrOutput . _3
|
||||
|
||||
resultUser :: Lens' DailyTableData (Entity User)
|
||||
resultUser = _dbrOutput . _4
|
||||
|
||||
resultUserAvs :: Traversal' DailyTableData UserAvs
|
||||
resultUserAvs = _dbrOutput . _5 . _Just . _entityVal
|
||||
|
||||
resultUserDay :: Traversal' DailyTableData UserDay
|
||||
resultUserDay = _dbrOutput . _6 . _Just . _entityVal
|
||||
|
||||
resultParticipantDay :: Traversal' DailyTableData TutorialParticipantDay
|
||||
resultParticipantDay = _dbrOutput . _7 . _Just . _entityVal
|
||||
|
||||
resultCompanyId :: Traversal' DailyTableData CompanyId
|
||||
resultCompanyId = _dbrOutput . _8 . _unValue . _Just
|
||||
|
||||
resultCourseQualis :: Traversal' DailyTableData [QualificationId]
|
||||
resultCourseQualis = _dbrOutput . _9 . _unValue . _Just
|
||||
|
||||
|
||||
instance HasEntity DailyTableData User where
|
||||
hasEntity = resultUser
|
||||
|
||||
instance HasUser DailyTableData where
|
||||
hasUser = resultUser . _entityVal
|
||||
|
||||
-- see colRatedField' for an example of formCell usage
|
||||
|
||||
drivingPermitField :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m, HandlerSite m ~ UniWorX) => Field m UserDrivingPermit
|
||||
drivingPermitField = selectField' (Just $ SomeMessage MsgBoolIrrelevant) optionsFinite
|
||||
|
||||
eyeExamField :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m, HandlerSite m ~ UniWorX) => Field m UserEyeExam
|
||||
eyeExamField = selectField' (Just $ SomeMessage MsgBoolIrrelevant) optionsFinite
|
||||
|
||||
mkDailyFormColumn :: (RenderMessage UniWorX msg) => Text -> msg -> Lens' DailyTableData a -> ASetter' DailyFormData a -> Field _ a -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
mkDailyFormColumn k msg lg ls f = sortable (Just $ SortingKey $ stripCI k) (i18nCell msg) $ formCell
|
||||
id -- lens focussing on the form result within the larger DBResult; id iff the form delivers the only result of the table
|
||||
(views (resultParticipant . _entityKey) return) -- generate row identfifiers for use in form result
|
||||
(\(view lg -> x) mkUnique ->
|
||||
over (_1.mapped) (ls .~) . over _2 fvWidget <$> mreq f (fsUniq mkUnique k) (Just x)
|
||||
) -- Given the row data and a callback to make an input name suitably unique generate the MForm
|
||||
|
||||
colParticipantPermitField :: Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colParticipantPermitField = colParticipantPermitField' _dailyFormDrivingPermit
|
||||
|
||||
colParticipantPermitField' :: ASetter' a (Maybe UserDrivingPermit) -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId a DailyTableData)))
|
||||
colParticipantPermitField' l = sortable (Just "permit") (i18nCell MsgTutorialDrivingPermit) $ (cellAttrs <>~ [("style","width:1%")]) <$> formCell
|
||||
id -- lens focussing on the form result within the larger DBResult; id iff the form delivers the only result of the table
|
||||
(views (resultParticipant . _entityKey) return) -- generate row identfifiers for use in form result
|
||||
(\(view (resultParticipant . _entityVal . _tutorialParticipantDrivingPermit) -> x) mkUnique ->
|
||||
over (_1.mapped) (l .~) . over _2 fvWidget <$> mopt drivingPermitField (fsUniq mkUnique "permit" & addClass' "uwx-narrow") (Just x)
|
||||
) -- Given the row data and a callback to make an input name suitably unique generate the MForm
|
||||
|
||||
colParticipantEyeExamField :: Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colParticipantEyeExamField = colParticipantEyeExamField' _dailyFormEyeExam
|
||||
|
||||
colParticipantEyeExamField' :: ASetter' a (Maybe UserEyeExam) -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId a DailyTableData)))
|
||||
colParticipantEyeExamField' l = sortable (Just "eye-exam") (i18nCell MsgTutorialEyeExam) $ (cellAttrs <>~ [("style","width:1%")]) <$> formCell id
|
||||
(views (resultParticipant . _entityKey) return)
|
||||
(\(view (resultParticipant . _entityVal . _tutorialParticipantEyeExam) -> x) mkUnique ->
|
||||
over (_1.mapped) (l .~) . over _2 fvWidget <$> mopt eyeExamField (fsUniq mkUnique "eye-exam" & addClass' "uwx-narrow") (Just x)
|
||||
)
|
||||
|
||||
-- colParticipantNoteField :: Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
-- colParticipantNoteField = sortable (Just "note-tutorial") (i18nCell MsgTutorialNote) $ (cellAttrs <>~ [("style","width:60%")]) <$> formCell id
|
||||
-- (views (resultParticipant . _entityKey) return)
|
||||
-- (\(view (resultParticipant . _entityVal . _tutorialParticipantNote) -> note) mkUnique ->
|
||||
-- over (_1.mapped) ((_dailyFormParticipantNote .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvWidget <$>
|
||||
-- mopt textareaField (fsUniq mkUnique "note-tutorial") (Just $ Textarea <$> note)
|
||||
-- )
|
||||
|
||||
colParticipantNoteField :: Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colParticipantNoteField = sortable (Just "note-tutorial") (i18nCell MsgTutorialNote) $ (cellAttrs <>~ [("style","min-width:12em")]) <$>
|
||||
formCell id
|
||||
(views (resultParticipant . _entityKey) return)
|
||||
(\row mkUnique ->
|
||||
let note = row ^. resultParticipant . _entityVal . _tutorialParticipantNote
|
||||
sid = row ^. resultCourse . _entityVal . _courseSchool
|
||||
cid = row ^. resultCourse . _entityKey
|
||||
tid = row ^. resultTutorial . _entityKey
|
||||
in over (_1.mapped) ((_dailyFormParticipantNote .~) . assertM (not . null) . fmap Text.strip) . over _2 fvWidget <$>
|
||||
mopt (textField & cfStrip & addDatalist (suggsParticipantNote sid cid tid)) (fsUniq mkUnique "note-tutorial") (Just note)
|
||||
)
|
||||
|
||||
suggsParticipantNote :: SchoolId -> CourseId -> TutorialId -> Handler (OptionList Text)
|
||||
suggsParticipantNote sid cid tid = do
|
||||
ol <- memcachedBy (Just . Right $ 2 * diffHour) (CacheKeySuggsParticipantNote sid tid) $ do
|
||||
suggs <- runDB $ E.select $ do
|
||||
let countRows' :: E.SqlExpr (E.Value Int64) = E.countRows
|
||||
(tpn, prio) <- E.from $
|
||||
( do
|
||||
tpa <- E.from $ E.table @TutorialParticipant
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantNote)
|
||||
E.&&. tpa E.^. TutorialParticipantTutorial E.==. E.val tid
|
||||
E.groupBy $ tpa E.^. TutorialParticipantNote
|
||||
E.orderBy [E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantNote, E.val (1 :: Int64))
|
||||
) `E.unionAll_`
|
||||
( do
|
||||
(tpa :& tut) <- E.from $ E.table @TutorialParticipant
|
||||
`E.innerJoin` E.table @Tutorial
|
||||
`E.on` (\(tpa :& tut) -> tut E.^. TutorialId E.==. tpa E.^. TutorialParticipantTutorial)
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantNote)
|
||||
E.&&. tpa E.^. TutorialParticipantTutorial E.!=. E.val tid
|
||||
E.&&. tut E.^. TutorialCourse E.==. E.val cid
|
||||
E.groupBy (tut E.^. TutorialLastChanged, tpa E.^. TutorialParticipantNote)
|
||||
E.orderBy [E.desc $ tut E.^. TutorialLastChanged, E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantNote, E.val 2)
|
||||
) `E.unionAll_`
|
||||
( do
|
||||
tpa :& tut :& crs <- E.from $ E.table @TutorialParticipant
|
||||
`E.innerJoin` E.table @Tutorial
|
||||
`E.on` (\(tpa :& tut) -> tut E.^. TutorialId E.==. tpa E.^. TutorialParticipantTutorial)
|
||||
`E.innerJoin` E.table @Course
|
||||
`E.on` (\(_ :& tut :& crs) -> tut E.^. TutorialCourse E.==. crs E.^. CourseId)
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantNote)
|
||||
E.&&. tpa E.^. TutorialParticipantTutorial E.!=. E.val tid
|
||||
E.&&. tut E.^. TutorialCourse E.!=. E.val cid
|
||||
E.&&. crs E.^. CourseSchool E.==. E.val sid
|
||||
E.groupBy (tut E.^. TutorialLastChanged, tpa E.^. TutorialParticipantNote)
|
||||
E.orderBy [E.desc $ tut E.^. TutorialLastChanged, E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantNote, E.val 3)
|
||||
)
|
||||
E.groupBy (tpn, prio)
|
||||
E.orderBy [E.asc prio, E.asc tpn]
|
||||
E.limit maxSuggestions
|
||||
pure $ E.coalesceDefault [tpn] $ E.val "" -- default never used due to where_ condtions, but conveniently changes type
|
||||
-- $logInfoS "NOTE-SUGGS *** A: " $ tshow suggs
|
||||
pure $ mkOptionListCacheable $ mkOptionText <$> nubOrd suggs
|
||||
-- $logInfoS "NOTE-SUGGS *** B: " $ tshow ol
|
||||
pure $ mkOptionListFromCacheable ol
|
||||
|
||||
suggsAttendanceNote :: SchoolId -> CourseId -> TutorialId -> Handler (OptionList Text)
|
||||
suggsAttendanceNote sid cid tid = do
|
||||
ol <- memcachedBy (Just . Right $ 2 * diffHour) (CacheKeySuggsAttendanceNote sid tid) $ do
|
||||
suggs <- runDB $ E.select $ do
|
||||
let countRows' :: E.SqlExpr (E.Value Int64) = E.countRows
|
||||
(tpn, prio) <- E.from $
|
||||
( do
|
||||
tpa <- E.from $ E.table @TutorialParticipantDay
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantDayNote)
|
||||
E.&&. tpa E.^. TutorialParticipantDayTutorial E.==. E.val tid
|
||||
E.groupBy (tpa E.^. TutorialParticipantDayNote, tpa E.^. TutorialParticipantDayDay)
|
||||
E.orderBy [E.desc $ tpa E.^. TutorialParticipantDayDay, E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantDayNote, E.val (1 :: Int64))
|
||||
) `E.unionAll_`
|
||||
( do
|
||||
(tpa :& tut) <- E.from $ E.table @TutorialParticipantDay
|
||||
`E.innerJoin` E.table @Tutorial
|
||||
`E.on` (\(tpa :& tut) -> tut E.^. TutorialId E.==. tpa E.^. TutorialParticipantDayTutorial)
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantDayNote)
|
||||
E.&&. tpa E.^. TutorialParticipantDayTutorial E.!=. E.val tid
|
||||
E.&&. tut E.^. TutorialCourse E.==. E.val cid
|
||||
E.groupBy (tpa E.^. TutorialParticipantDayNote, tpa E.^. TutorialParticipantDayDay, tut E.^. TutorialLastChanged)
|
||||
E.orderBy [E.desc $ tpa E.^. TutorialParticipantDayDay, E.desc $ tut E.^. TutorialLastChanged, E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantDayNote, E.val 2)
|
||||
) `E.unionAll_`
|
||||
( do
|
||||
tpa :& tut :& crs <- E.from $ E.table @TutorialParticipantDay
|
||||
`E.innerJoin` E.table @Tutorial
|
||||
`E.on` (\(tpa :& tut) -> tut E.^. TutorialId E.==. tpa E.^. TutorialParticipantDayTutorial)
|
||||
`E.innerJoin` E.table @Course
|
||||
`E.on` (\(_ :& tut :& crs) -> tut E.^. TutorialCourse E.==. crs E.^. CourseId)
|
||||
E.where_ $ E.isJust (tpa E.^. TutorialParticipantDayNote)
|
||||
E.&&. tpa E.^. TutorialParticipantDayTutorial E.!=. E.val tid
|
||||
E.&&. tut E.^. TutorialCourse E.!=. E.val cid
|
||||
E.&&. crs E.^. CourseSchool E.==. E.val sid
|
||||
E.groupBy (tpa E.^. TutorialParticipantDayNote, tpa E.^. TutorialParticipantDayDay, tut E.^. TutorialLastChanged)
|
||||
E.orderBy [E.desc $ tpa E.^. TutorialParticipantDayDay, E.desc $ tut E.^. TutorialLastChanged, E.desc countRows']
|
||||
E.limit maxSuggestions
|
||||
pure (tpa E.^. TutorialParticipantDayNote, E.val 3)
|
||||
)
|
||||
E.groupBy (tpn, prio)
|
||||
E.orderBy [E.asc prio, E.asc tpn]
|
||||
E.limit maxSuggestions
|
||||
pure $ E.coalesceDefault [tpn] $ E.val "" -- default never used due to where_ condtions, but conveniently changes type
|
||||
pure $ mkOptionListCacheable $ mkOptionText <$> nubOrd suggs -- NOTE: datalist does not work on textarea inputs
|
||||
pure $ mkOptionListFromCacheable ol
|
||||
|
||||
|
||||
colAttendanceField :: Text -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colAttendanceField dday = sortable (Just "attendance") (i18nCell $ MsgTutorialDayAttendance dday) $ (cellAttrs %~ addAttrsClass "text--center") <$> formCell id
|
||||
(views (resultParticipant . _entityKey) return)
|
||||
(\(preview (resultParticipantDay . _tutorialParticipantDayAttendance) -> attendance) mkUnique ->
|
||||
over (_1.mapped) (_dailyFormAttendance .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "attendance") attendance
|
||||
)
|
||||
|
||||
colAttendanceNoteField :: Text -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colAttendanceNoteField dday = sortable (Just "note-attend") (i18nCell $ MsgTutorialDayNote dday) $ (cellAttrs <>~ [("style","min-width:12em")]) <$>
|
||||
formCell id
|
||||
(views (resultParticipant . _entityKey) return)
|
||||
(\row mkUnique ->
|
||||
let note = row ^? resultParticipantDay . _tutorialParticipantDayNote
|
||||
sid = row ^. resultCourse . _entityVal . _courseSchool
|
||||
cid = row ^. resultCourse . _entityKey
|
||||
tid = row ^. resultTutorial . _entityKey
|
||||
in over (_1.mapped) ((_dailyFormAttendanceNote .~) . assertM (not . null) . fmap Text.strip) . over _2 fvWidget <$> -- For Textarea use: fmap (Text.strip . unTextarea)
|
||||
mopt (textField & cfStrip & addDatalist (suggsAttendanceNote sid cid tid)) (fsUniq mkUnique "note-attendance") note
|
||||
---- Version für Textare
|
||||
-- mopt (textareaField) -- & addDatalist (suggsAttendanceNote sid cid tid)) -- NOTE: datalist does not work on textarea inputs
|
||||
-- (fsUniq mkUnique "note-attendance" & addClass' "uwx-short"
|
||||
-- -- & addAttr "rows" "2" -- does not work without class uwx-short
|
||||
-- -- & addAttr "cols" "12" -- let it stretch
|
||||
-- -- & addAutosubmit -- submits while typing
|
||||
-- ) (Textarea <<$>> note)
|
||||
)
|
||||
|
||||
colParkingField :: Text -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData)))
|
||||
colParkingField = colParkingField' _dailyFormParkingToken
|
||||
|
||||
-- colParkingField' :: ASetter' a Bool -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId a DailyTableData)))
|
||||
-- colParkingField' l = sortable (Just "parking") (i18nCell MsgTableUserParkingToken) $ formCell id
|
||||
-- (views (resultParticipant . _entityKey) return)
|
||||
-- (\(preview (resultUserDay . _userDayParkingToken) -> parking) mkUnique ->
|
||||
-- over (_1.mapped) (l .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "parktoken") parking
|
||||
-- )
|
||||
|
||||
colParkingField' :: ASetter' a Bool -> Text -> Colonnade Sortable DailyTableData (DBCell _ (FormResult (DBFormResult TutorialParticipantId a DailyTableData)))
|
||||
colParkingField' l dday = sortable (Just "parking") (i18nCell $ MsgTableUserParkingToken dday) $ (cellAttrs %~ addAttrsClass "text--center") <$> formCell
|
||||
id -- TODO: this should not be id! Refactor to simplify the third argument below
|
||||
(views (resultParticipant . _entityKey) return)
|
||||
(\(preview (resultUserDay . _userDayParkingToken) -> parking) mkUnique ->
|
||||
over (_1.mapped) (l .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "parktoken") parking
|
||||
)
|
||||
|
||||
mkDailyTable :: Bool -> SchoolId -> Day -> Maybe DayCheckResults -> DB (FormResult (DBFormResult TutorialParticipantId DailyFormData DailyTableData), Maybe Widget)
|
||||
mkDailyTable isAdmin ssh nd dcrs = getDayTutorials ssh (nd,nd) >>= \case
|
||||
tutLessons
|
||||
| Map.null tutLessons -> return (FormMissing, Nothing)
|
||||
| otherwise -> do
|
||||
dday <- formatTime SelFormatDate nd
|
||||
let
|
||||
tutIds = Map.keys tutLessons
|
||||
dbtSQLQuery :: DailyTableExpr -> DailyTableOutput
|
||||
dbtSQLQuery (crs `E.InnerJoin` tut `E.InnerJoin` tpu `E.InnerJoin` usr `E.LeftOuterJoin` avs `E.LeftOuterJoin` udy `E.LeftOuterJoin` tdy) = do
|
||||
EL.on $ tut E.^. TutorialId E.=?. tdy E.?. TutorialParticipantDayTutorial
|
||||
E.&&. usr E.^. UserId E.=?. tdy E.?. TutorialParticipantDayUser
|
||||
E.&&. E.val nd E.=?. tdy E.?. TutorialParticipantDayDay
|
||||
EL.on $ usr E.^. UserId E.=?. udy E.?. UserDayUser
|
||||
E.&&. E.val nd E.=?. udy E.?. UserDayDay
|
||||
EL.on $ usr E.^. UserId E.=?. avs E.?. UserAvsUser
|
||||
EL.on $ usr E.^. UserId E.==. tpu E.^. TutorialParticipantUser
|
||||
EL.on $ tut E.^. TutorialId E.==. tpu E.^. TutorialParticipantTutorial
|
||||
EL.on $ tut E.^. TutorialCourse E.==. crs E.^. CourseId
|
||||
E.where_ $ tut E.^. TutorialId `E.in_` E.valList tutIds
|
||||
let associatedQualifications = E.subSelectMaybe . EL.from $ \cq -> do
|
||||
E.where_ $ cq E.^. CourseQualificationCourse E.==. crs E.^. CourseId
|
||||
let cqQual = cq E.^. CourseQualificationQualification
|
||||
cqOrder = [E.asc $ cq E.^. CourseQualificationSortOrder, E.asc cqQual]
|
||||
return $ E.arrayAggWith E.AggModeAll cqQual cqOrder
|
||||
return (crs, tut, tpu, usr, avs, udy, tdy, selectCompanyUserPrime usr, associatedQualifications)
|
||||
dbtRowKey = queryParticipant >>> (E.^. TutorialParticipantId)
|
||||
dbtProj = dbtProjId
|
||||
dbtColonnade = formColonnade $ mconcat
|
||||
[ -- dbSelect (applying _2) id (return . view (resultTutorial . _entityKey))
|
||||
sortable (Just "course") (i18nCell MsgTableCourse) $ \(view $ resultCourse . _entityVal -> c) -> courseCell c
|
||||
, sortable (Just "tutorial") (i18nCell MsgCourseTutorial) $ \row ->
|
||||
let Course{courseTerm=tid, courseSchool=cssh, courseShorthand=csh}
|
||||
= row ^. resultCourse . _entityVal
|
||||
tutName = row ^. resultTutorial . _entityVal . _tutorialName
|
||||
in anchorCell (CTutorialR tid cssh csh tutName TUsersR) $ citext2widget tutName
|
||||
, sortable Nothing (i18nCell MsgTableTutorialOccurrence) $ \(view $ resultTutorial . _entityKey -> tutId) -> cellMaybe (lessonTimesCell False . snd) $ Map.lookup tutId tutLessons
|
||||
, sortable Nothing (i18nCell MsgTableTutorialRoom) $ \(view $ resultTutorial . _entityKey -> tutId) ->
|
||||
-- listInlineCell (nubOrd . concat $ mapMM lessonRoom $ Map.lookup tutId tutLessons) roomReferenceCell
|
||||
cellMaybe ((`listInlineCell` roomReferenceCell) . nubOrd) $ mapMM lessonRoom $ snd <$> Map.lookup tutId tutLessons
|
||||
-- , sortable Nothing (i18nCell MsgTableTutorialRoom) $ \(view $ resultTutorial . _entityKey -> _) -> listCell ["A","D","C","B"] textCell -- DEMO: listCell reverses the order, for list-types! listInlineCell is fixed now
|
||||
, sortable Nothing (i18nCell $ MsgCourseQualifications 3) $ \(preview resultCourseQualis -> cqs) -> maybeCell cqs $ flip listInlineCell qualificationIdShortCell
|
||||
-- , sortable (Just "user-company") (i18nCell MsgTablePrimeCompany) $ \(preview resultCompanyId -> mcid) -> cellMaybe companyIdCell mcid
|
||||
-- , sortable (Just "booking-firm") (i18nCell MsgTableBookingCompany) $ \(view $ resultParticipant . _entityVal . _tutorialParticipantCompany -> mcid) -> cellMaybe companyIdCell mcid
|
||||
, sortable (Just "booking-firm") (i18nCell MsgTableBookingCompany) $ \row ->
|
||||
let bookComp = row ^. resultParticipant . _entityVal . _tutorialParticipantCompany
|
||||
primComp = row ^? resultCompanyId
|
||||
bookLink = cellMaybe companyIdCell bookComp
|
||||
result
|
||||
| primComp /= bookComp
|
||||
, Just (unCompanyKey -> csh) <- primComp
|
||||
= cell (iconTooltip [whamlet|_{MsgAvsPrimaryCompany}: ^{companyWidget True (csh, csh, False)}|]
|
||||
(Just IconCompany) True)
|
||||
<> spacerCell
|
||||
<> bookLink
|
||||
| otherwise = bookLink
|
||||
in result
|
||||
-- , sortable (Just "booking-firm") (i18nCell MsgTableBookingCompany) $ \row ->
|
||||
-- let bookComp = row ^. resultParticipant . _entityVal . _tutorialParticipantCompany
|
||||
-- primComp = row ^? resultCompanyId
|
||||
-- bookLink = cellMaybe companyIdCell bookComp
|
||||
-- warnIcon = \csh -> iconTooltip [whamlet|_{MsgAvsPrimaryCompany}: ^{companyWidget True (csh, csh, False)}|] (Just IconCompanyWarning) True
|
||||
-- result
|
||||
-- | primComp /= bookComp
|
||||
-- , Just (unCompanyKey -> csh) <- primComp
|
||||
-- = bookLink
|
||||
-- <> spacerCell
|
||||
-- <> cell (modal (warnIcon csh) (Right -- maybe just use iconCompanyWarning instead of modal?
|
||||
-- [whamlet|
|
||||
-- <h2>
|
||||
-- ^{userWidget row}
|
||||
-- <p>
|
||||
-- _{MsgAvsPrimaryCompany}: ^{companyWidget True (csh, csh, False)}
|
||||
-- |]
|
||||
-- ))
|
||||
-- | otherwise = bookLink
|
||||
-- in result
|
||||
, maybeEmpty dcrs $ \DayCheckResults{..} ->
|
||||
sortable (Just "check-fail") (timeCell dcrTimestamp) $ \(view $ resultParticipant . _entityKey -> tpid) ->
|
||||
maybeCell (Map.lookup tpid dcrResults) $ wgtCell . dcr2widgetIcn Nothing
|
||||
, colUserNameModalHdr MsgCourseParticipant ForProfileDataR
|
||||
, colUserMatriclenr isAdmin
|
||||
, sortable (Just "card-no") (i18nCell MsgAvsCardNo) $ \(preview $ resultUserAvs . _userAvsLastCardNo . _Just -> cn :: Maybe AvsFullCardNo) -> cellMaybe (textCell . tshowAvsFullCardNo) cn
|
||||
, colParticipantEyeExamField
|
||||
, colParticipantPermitField
|
||||
, colParticipantNoteField
|
||||
, colAttendanceField dday
|
||||
, colAttendanceNoteField dday
|
||||
, colParkingField dday
|
||||
-- FOR DEBUGGING ONLY:
|
||||
-- , sortable (Just "permit") (i18nCell MsgTutorialDrivingPermit) $ \(view $ resultParticipant . _entityVal . _tutorialParticipantDrivingPermit -> x) -> x & cellMaybe i18nCell
|
||||
-- , sortable (Just "eye-exam") (i18nCell MsgTutorialEyeExam) $ \(view $ resultParticipant . _entityVal . _tutorialParticipantEyeExam -> x) -> x & cellMaybe i18nCell
|
||||
-- , sortable (Just "note-tutorial") (i18nCell MsgTutorialNote) $ \(view $ resultParticipant . _entityVal . _tutorialParticipantNote -> x) -> x & cellMaybe textCell
|
||||
-- , sortable (Just "attendance") (i18nCell $ MsgTutorialDayAttendance dday) $ \(preview $ resultParticipantDay . _tutorialParticipantDayAttendance -> x) -> x & cellMaybe tickmarkCell
|
||||
-- , sortable (Just "note-attend") (i18nCell $ MsgTutorialDayNote dday) $ \(preview $ resultParticipantDay . _tutorialParticipantDayNote . _Just -> x) -> x & cellMaybe textCell
|
||||
-- , sortable (Just "parking") (i18nCell MsgTableUserParkingToken) $ \(preview $ resultUserDay . _userDayParkingToken -> x) -> maybeCell x tickmarkCell
|
||||
]
|
||||
dbtSorting = Map.fromList
|
||||
[ sortUserNameLink queryUser
|
||||
, sortUserMatriclenr queryUser
|
||||
, ("course" , SortColumn $ queryCourse >>> (E.^. CourseName))
|
||||
, ("tutorial" , SortColumn $ queryTutorial >>> (E.^. TutorialName))
|
||||
, ("user-company" , SortColumn $ queryUser >>> selectCompanyUserPrime)
|
||||
, ("booking-firm" , SortColumn $ queryParticipant >>> (E.^. TutorialParticipantCompany))
|
||||
, ("card-no" , SortColumn $ queryUserAvs >>> (E.?. UserAvsLastCardNo))
|
||||
, ("permit" , SortColumnNullsInv $ queryParticipant >>> (E.^. TutorialParticipantDrivingPermit))
|
||||
, ("eye-exam" , SortColumnNullsInv $ queryParticipant >>> (E.^. TutorialParticipantEyeExam))
|
||||
, ("note-tutorial" , SortColumn $ queryParticipant >>> (E.^. TutorialParticipantNote))
|
||||
, ("attendance" , SortColumnNullsInv $ queryParticipantDay >>> (E.?. TutorialParticipantDayAttendance))
|
||||
, ("note-attend" , SortColumn $ queryParticipantDay >>> (E.?. TutorialParticipantDayNote))
|
||||
, ("parking" , SortColumnNullsInv $ queryUserDay >>> (E.?. UserDayParkingToken))
|
||||
-- , ("check-fail" , SortColumn $ queryParticipant >>> (\pid -> pid E.^. TutorialParticipantId `E.in_` E.vals (maybeEmpty dcrs $ dcrResults >>> Map.keys)))
|
||||
, let dcrsLevels = maybeEmpty dcrs $ dcrSeverityGroups . dcrResults in
|
||||
("check-fail" , SortColumn $ queryParticipant >>> (\((E.^. TutorialParticipantId) -> pid) -> E.case_
|
||||
[ E.when_ (pid `E.in_` E.vals (dcrsLevels ^. _1)) E.then_ (E.val 1)
|
||||
, E.when_ (pid `E.in_` E.vals (dcrsLevels ^. _2)) E.then_ (E.val 2)
|
||||
, E.when_ (pid `E.in_` E.vals (dcrsLevels ^. _3)) E.then_ (E.val 3)
|
||||
, E.when_ (pid `E.in_` E.vals (dcrsLevels ^. _4)) E.then_ (E.val 4)
|
||||
, E.when_ (pid `E.in_` E.vals (dcrsLevels ^. _5)) E.then_ (E.val 5)
|
||||
] (E.else_ E.val (99 :: Int64))
|
||||
))
|
||||
]
|
||||
dbtFilter = Map.fromList
|
||||
[ fltrUserNameEmail queryUser
|
||||
, fltrUserMatriclenr queryUser
|
||||
, ("course" , FilterColumn . E.mkContainsFilter $ queryCourse >>> (E.^. CourseName))
|
||||
, ("tutorial" , FilterColumn . E.mkContainsFilter $ queryTutorial >>> (E.^. TutorialName))
|
||||
, ("booking-firm" , FilterColumn . E.mkContainsFilterWith Just $ queryParticipant >>> (E.^. TutorialParticipantCompany))
|
||||
, ("user-company" , FilterColumn . E.mkContainsFilterWith Just $ queryUser >>> selectCompanyUserPrime)
|
||||
]
|
||||
dbtFilterUI mPrev = mconcat
|
||||
[ prismAForm (singletonFilter "course" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgFilterCourse)
|
||||
, prismAForm (singletonFilter "tutorial" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgCourseTutorial)
|
||||
, prismAForm (singletonFilter "booking-firm" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableBookingCompanyShort)
|
||||
, prismAForm (singletonFilter "user-company" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTablePrimeCompanyShort)
|
||||
, fltrUserNameEmailUI mPrev
|
||||
, fltrUserMatriclenrUI mPrev
|
||||
]
|
||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout}
|
||||
dbtIdent :: Text
|
||||
dbtIdent = "daily"
|
||||
dbtCsvEncode = noCsvEncode
|
||||
dbtCsvDecode = Nothing
|
||||
dbtExtraReps = []
|
||||
dbtParams = def { dbParamsFormAction = Just $ SomeRoute $ SchoolR ssh $ SchoolDayR nd }
|
||||
-- dbtParams = DBParamsForm
|
||||
-- { dbParamsFormMethod = POST
|
||||
-- , dbParamsFormAction = Nothing -- Just $ SomeRoute currentRoute
|
||||
-- , dbParamsFormAttrs = []
|
||||
-- , dbParamsFormSubmit = FormSubmit
|
||||
-- , dbParamsFormAdditional = \frag -> do
|
||||
-- let acts :: Map DailyTableAction (AForm Handler DailyTableActionData)
|
||||
-- acts = mconcat
|
||||
-- [ singletonMap DailyActDummy $ pure DailyActDummyData
|
||||
-- ]
|
||||
-- (actionRes, action) <- multiActionM acts "" Nothing mempty
|
||||
-- return ((, mempty) . Last . Just <$> actionRes, toWidget frag <> action)
|
||||
-- -- , dbParamsFormAdditional
|
||||
-- -- = let acts :: Map DailyTableAction (AForm Handler DailyTableActionData)
|
||||
-- -- acts = mconcat
|
||||
-- -- [ singletonMap DailyActDummy $ pure DailyActDummyData
|
||||
-- -- ]
|
||||
-- -- in renderAForm FormStandard
|
||||
-- -- $ (, mempty) . First . Just
|
||||
-- -- <$> multiActionA acts (fslI MsgTableAction) Nothing
|
||||
-- , dbParamsFormEvaluate = liftHandler . runFormPost
|
||||
-- , dbParamsFormResult = _1
|
||||
-- , dbParamsFormIdent = def
|
||||
-- }
|
||||
-- postprocess :: FormResult (First DailyTableActionData, DBFormResult TutorialParticipantId Bool DailyTableData)
|
||||
-- -> FormResult ( DailyTableActionData, Set TutorialId)
|
||||
-- postprocess inp = do
|
||||
-- (First (Just act), jobMap) <- inp
|
||||
-- let jobSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) jobMap
|
||||
-- return (act, jobSet)
|
||||
psValidator = def & defaultSorting [SortAscBy "user-name", SortAscBy "course", SortAscBy "tutorial"]
|
||||
-- over _1 postprocess <$> dbTable psValidator DBTable{..}
|
||||
over _2 Just <$> dbTable psValidator DBTable{..}
|
||||
|
||||
|
||||
getSchoolDayR, postSchoolDayR :: SchoolId -> Day -> Handler Html
|
||||
getSchoolDayR = postSchoolDayR
|
||||
postSchoolDayR ssh nd = do
|
||||
isAdmin <- hasReadAccessTo AdminR
|
||||
dday <- formatTime SelFormatDate nd
|
||||
let unFormResult = getDBFormResult $ \row -> let tpt = row ^. resultParticipant . _entityVal
|
||||
in DailyFormData
|
||||
{ dailyFormDrivingPermit = tpt ^. _tutorialParticipantDrivingPermit
|
||||
, dailyFormEyeExam = tpt ^. _tutorialParticipantEyeExam
|
||||
, dailyFormParticipantNote = tpt ^. _tutorialParticipantNote
|
||||
, dailyFormAttendance = row ^? resultParticipantDay ._tutorialParticipantDayAttendance & fromMaybe False
|
||||
, dailyFormAttendanceNote = row ^? resultParticipantDay ._tutorialParticipantDayNote . _Just
|
||||
, dailyFormParkingToken = row ^? resultUserDay . _userDayParkingToken & fromMaybe False
|
||||
}
|
||||
dcrs <- memcachedByGet (CacheKeyTutorialCheckResults ssh nd)
|
||||
(fmap unFormResult -> tableRes, tableDaily) <- runDB $ mkDailyTable isAdmin ssh nd dcrs
|
||||
-- logInfoS "****DailyTable****" $ tshow tableRes
|
||||
formResult tableRes $ \resMap -> do
|
||||
tuts <- runDB $ forM (Map.toList resMap) $ \(tpid, DailyFormData{..}) -> do
|
||||
-- logDebugS "TableForm" (tshow dfd)
|
||||
TutorialParticipant{..} <- get404 tpid -- needed anyway to find the ParticipantDay/UserDay updated
|
||||
when ( tutorialParticipantDrivingPermit /= dailyFormDrivingPermit
|
||||
|| tutorialParticipantEyeExam /= dailyFormEyeExam
|
||||
|| tutorialParticipantNote /= dailyFormParticipantNote) $
|
||||
update tpid [ TutorialParticipantDrivingPermit =. dailyFormDrivingPermit
|
||||
, TutorialParticipantEyeExam =. dailyFormEyeExam
|
||||
, TutorialParticipantNote =. dailyFormParticipantNote
|
||||
]
|
||||
let tpdUq = UniqueTutorialParticipantDay tutorialParticipantTutorial tutorialParticipantUser nd
|
||||
if not dailyFormAttendance && isNothing (canonical dailyFormAttendanceNote)
|
||||
then deleteBy tpdUq
|
||||
else upsertBy_ tpdUq (TutorialParticipantDay tutorialParticipantTutorial tutorialParticipantUser nd dailyFormAttendance dailyFormAttendanceNote)
|
||||
[ TutorialParticipantDayAttendance =. dailyFormAttendance
|
||||
, TutorialParticipantDayNote =. dailyFormAttendanceNote
|
||||
]
|
||||
let udUq = UniqueUserDay tutorialParticipantUser nd
|
||||
updateUserDay = if dailyFormParkingToken
|
||||
then flip upsertBy_ (UserDay tutorialParticipantUser nd dailyFormParkingToken) -- upsert if a permit was issued
|
||||
else updateBy -- only update to no permit, if the record exists, but do not create a fresh record with parkingToken==False
|
||||
updateUserDay udUq [ UserDayParkingToken =. dailyFormParkingToken]
|
||||
return tutorialParticipantTutorial
|
||||
forM_ tuts $ \tid -> do
|
||||
memcachedByInvalidate (CacheKeySuggsParticipantNote ssh tid) $ Proxy @(OptionListCacheable Text)
|
||||
memcachedByInvalidate (CacheKeySuggsAttendanceNote ssh tid) $ Proxy @(OptionListCacheable Text)
|
||||
-- audit log? Currently decided against.
|
||||
memcachedByInvalidate (CacheKeyTutorialCheckResults ssh nd) $ Proxy @DayCheckResults
|
||||
addMessageI Success $ MsgTutorialParticipantsDayEdits dday
|
||||
redirect $ SchoolR ssh $ SchoolDayR nd
|
||||
|
||||
siteLayoutMsg (MsgMenuSchoolDay ssh dday) $ do
|
||||
let consistencyBtn = btnModal MsgMenuSchoolDayCheck [BCIsButton, BCDefault] (Left $ SomeRoute $ SchoolR ssh $ SchoolDayCheckR nd)
|
||||
setTitleI (MsgMenuSchoolDay ssh dday)
|
||||
$(i18nWidgetFile "day-view")
|
||||
|
||||
|
||||
-- | A wrapper for several check results on tutorial participants
|
||||
data DayCheckResult = DayCheckResult
|
||||
{ dcAvsKnown :: Bool
|
||||
, dcApronAccess :: Bool
|
||||
, dcBookingFirmOk :: Bool
|
||||
, dcEyeFitsPermit :: Maybe Bool
|
||||
}
|
||||
deriving (Eq, Show, Generic, Binary)
|
||||
|
||||
data DayCheckResults = DayCheckResults
|
||||
{ dcrTimestamp :: UTCTime
|
||||
, dcrResults :: Map TutorialParticipantId DayCheckResult
|
||||
}
|
||||
deriving (Show, Generic, Binary)
|
||||
|
||||
-- | True iff there is no problem at all
|
||||
dcrIsOk :: DayCheckResult -> Bool
|
||||
dcrIsOk (DayCheckResult True True True (Just True)) = True
|
||||
dcrIsOk _ = False
|
||||
|
||||
-- | defines categories on DayCheckResult, implying an ordering, with most severe being least
|
||||
dcrSeverity :: DayCheckResult -> Int
|
||||
dcrSeverity DayCheckResult{dcAvsKnown = False } = 1
|
||||
dcrSeverity DayCheckResult{dcApronAccess = False } = 2
|
||||
dcrSeverity DayCheckResult{dcBookingFirmOk = False } = 3
|
||||
dcrSeverity DayCheckResult{dcEyeFitsPermit = Nothing } = 4
|
||||
dcrSeverity DayCheckResult{dcEyeFitsPermit = Just False} = 5
|
||||
dcrSeverity _ = 99
|
||||
|
||||
instance Ord DayCheckResult where
|
||||
compare = compare `on` dcrSeverity
|
||||
|
||||
type DayCheckGroups = ( Set TutorialParticipantId -- 1 severity
|
||||
, Set TutorialParticipantId -- 2
|
||||
, Set TutorialParticipantId -- 3
|
||||
, Set TutorialParticipantId -- 4
|
||||
, Set TutorialParticipantId -- 5
|
||||
)
|
||||
|
||||
dcrSeverityGroups :: Map TutorialParticipantId DayCheckResult -> DayCheckGroups
|
||||
dcrSeverityGroups = Map.foldMapWithKey groupBySeverity
|
||||
where
|
||||
groupBySeverity :: TutorialParticipantId -> DayCheckResult -> DayCheckGroups
|
||||
groupBySeverity tpid dcr =
|
||||
let sempty = mempty :: DayCheckGroups
|
||||
in case dcrSeverity dcr of
|
||||
1 -> set _1 (Set.singleton tpid) sempty
|
||||
2 -> set _2 (Set.singleton tpid) sempty
|
||||
3 -> set _3 (Set.singleton tpid) sempty
|
||||
4 -> set _4 (Set.singleton tpid) sempty
|
||||
5 -> set _5 (Set.singleton tpid) sempty
|
||||
_ -> sempty
|
||||
|
||||
-- | Possible outcomes for DayCheckResult
|
||||
dcrMessages :: [SomeMessage UniWorX]
|
||||
dcrMessages = [ SomeMessage MsgAvsPersonSearchEmpty
|
||||
, SomeMessage MsgAvsNoApronCard
|
||||
, SomeMessage $ MsgAvsNoCompanyCard Nothing
|
||||
, SomeMessage MsgCheckEyePermitMissing
|
||||
, SomeMessage MsgCheckEyePermitIncompatible
|
||||
]
|
||||
|
||||
-- | Show most important problem as text
|
||||
dcr2widgetTxt :: Maybe CompanyName -> DayCheckResult -> Widget
|
||||
dcr2widgetTxt _ DayCheckResult{dcAvsKnown=False} = i18n MsgAvsPersonSearchEmpty
|
||||
dcr2widgetTxt _ DayCheckResult{dcApronAccess=False} = i18n MsgAvsNoApronCard
|
||||
dcr2widgetTxt mcn DayCheckResult{dcBookingFirmOk=False} = i18n $ MsgAvsNoCompanyCard mcn
|
||||
dcr2widgetTxt _ DayCheckResult{dcEyeFitsPermit=Nothing} = i18n MsgCheckEyePermitMissing
|
||||
dcr2widgetTxt _ DayCheckResult{dcEyeFitsPermit=Just False}= i18n MsgCheckEyePermitIncompatible
|
||||
dcr2widgetTxt _ _ = i18n MsgNoProblem
|
||||
|
||||
-- | Show all problems as icon with tooltip
|
||||
dcr2widgetIcn :: Maybe CompanyName -> DayCheckResult -> Widget
|
||||
dcr2widgetIcn mcn DayCheckResult{..} = mconcat [avsChk, apronChk, bookChk, permitChk]
|
||||
where
|
||||
mkTooltip ico msg = iconTooltip msg (Just ico) True
|
||||
|
||||
avsChk = guardMonoid (not dcAvsKnown) $ mkTooltip IconUserUnknown (i18n MsgAvsPersonSearchEmpty)
|
||||
apronChk = guardMonoid (not dcApronAccess) $ mkTooltip IconUserBadge (i18n MsgAvsNoApronCard)
|
||||
bookChk = guardMonoid (not dcBookingFirmOk) $ mkTooltip IconCompanyWarning (i18n $ MsgAvsNoCompanyCard mcn)
|
||||
permitChk | isNothing dcEyeFitsPermit = mkTooltip IconFileMissing (i18n MsgCheckEyePermitMissing)
|
||||
| dcEyeFitsPermit == Just False = mkTooltip IconGlasses (i18n MsgCheckEyePermitIncompatible)
|
||||
| otherwise = mempty
|
||||
|
||||
type ParticipantCheckData = (Entity TutorialParticipant, UserDisplayName, UserSurname, Maybe AvsPersonId, Maybe CompanyName)
|
||||
|
||||
dayCheckParticipant :: Map AvsPersonId AvsDataPerson
|
||||
-> ParticipantCheckData
|
||||
-> DayCheckResult
|
||||
dayCheckParticipant avsStats (Entity {entityVal=TutorialParticipant{..}}, _udn, _usn, mapi, mcmp) =
|
||||
let dcEyeFitsPermit = liftM2 eyeExamFitsDrivingPermit tutorialParticipantEyeExam tutorialParticipantDrivingPermit
|
||||
(dcAvsKnown, (dcApronAccess, dcBookingFirmOk))
|
||||
| Just AvsDataPerson{avsPersonPersonCards = apcs} <- lookupMaybe avsStats mapi
|
||||
= (True , mapBoth getAny $ foldMap (hasApronAccess &&& fitsBooking mcmp) apcs)
|
||||
| otherwise
|
||||
= (False, (False, False))
|
||||
in DayCheckResult{..}
|
||||
where
|
||||
hasApronAccess :: AvsDataPersonCard -> Any
|
||||
hasApronAccess AvsDataPersonCard{avsDataValid=True, avsDataCardColor=AvsCardColorGelb} = Any True
|
||||
hasApronAccess AvsDataPersonCard{avsDataValid=True, avsDataCardColor=AvsCardColorRot} = Any True
|
||||
hasApronAccess _ = Any False
|
||||
|
||||
fitsBooking :: Maybe CompanyName -> AvsDataPersonCard -> Any
|
||||
fitsBooking (Just cn) AvsDataPersonCard{avsDataValid=True,avsDataFirm=Just df} = Any $ cn == stripCI df
|
||||
fitsBooking _ _ = Any False
|
||||
|
||||
-- | Prüft die Teilnehmer der Tagesansicht: AVS online aktualisieren, gültigen Vorfeldausweis prüfen, buchende Firma mit Ausweisnummer aus AVS abgleichen
|
||||
getSchoolDayCheckR :: SchoolId -> Day -> Handler Html
|
||||
getSchoolDayCheckR ssh nd = do
|
||||
-- isAdmin <- hasReadAccessTo AdminR
|
||||
now <- liftIO getCurrentTime
|
||||
let nowaday = utctDay now
|
||||
dday <- formatTime SelFormatDate nd
|
||||
|
||||
(tuts, parts_avs, examProblemsTbl) <- runDB $ do
|
||||
tuts <- getDayTutorials ssh (nd,nd)
|
||||
parts_avs :: [ParticipantCheckData] <- $(unValueNIs 5 [2..5]) <<$>> E.select (do
|
||||
(tpa :& usr :& avs :& cmp) <- E.from $ E.table @TutorialParticipant
|
||||
`E.innerJoin` E.table @User
|
||||
`E.on` (\(tpa :& usr) -> tpa E.^. TutorialParticipantUser E.==. usr E.^. UserId)
|
||||
`E.leftJoin` E.table @UserAvs
|
||||
`E.on` (\(tpa :& _ :& avs) -> tpa E.^. TutorialParticipantUser E.=?. avs E.?. UserAvsUser)
|
||||
`E.leftJoin` E.table @Company
|
||||
`E.on` (\(tpa :& _ :& _ :& cmp) -> tpa E.^. TutorialParticipantCompany E.==. cmp E.?. CompanyId)
|
||||
E.where_ $ tpa E.^. TutorialParticipantTutorial `E.in_` E.vals (Map.keys tuts)
|
||||
-- E.orderBy [E.asc $ tpa E.^. TutorialParticipantTutorial, E.asc $ usr E.^. UserDisplayName] -- order no longer needed
|
||||
return (tpa, usr E.^. UserDisplayName, usr E.^. UserSurname, avs E.?. UserAvsPersonId, cmp E.?. CompanyName)
|
||||
)
|
||||
-- additionally queue proper AVS synchs for all users, unless there were already done today
|
||||
void $ queueAvsUpdateByUID (foldMap (^. _1 . _entityVal . _tutorialParticipantUser . to Set.singleton) parts_avs) (Just nowaday)
|
||||
-- check for double examiners
|
||||
examProblemsTbl <- mkExamProblemsTable ssh nd
|
||||
return (tuts, parts_avs, examProblemsTbl)
|
||||
let getApi :: ParticipantCheckData -> Set AvsPersonId
|
||||
getApi = foldMap Set.singleton . view _4
|
||||
avsStats :: Map AvsPersonId AvsDataPerson <- catchAVShandler False False True mempty $ lookupAvsUsers $ foldMap getApi parts_avs -- query AVS, but does not affect DB (no update)
|
||||
-- gültigen Vorfeldausweis prüfen, buchende Firma mit Ausweisnummer aus AVS abgleichen
|
||||
let toPartMap :: ParticipantCheckData -> Map TutorialParticipantId DayCheckResult
|
||||
toPartMap pcd = Map.singleton (pcd ^. _1 . _entityKey) $ dayCheckParticipant avsStats pcd
|
||||
participantResults = foldMap toPartMap parts_avs
|
||||
memcachedBySet (Just . Right $ 2 * diffHour) (CacheKeyTutorialCheckResults ssh nd) $ DayCheckResults now participantResults
|
||||
|
||||
-- the following is only for displaying results neatly
|
||||
let sortBadParticipant acc pcd =
|
||||
let tid = pcd ^. _1 . _entityVal . _tutorialParticipantTutorial
|
||||
pid = pcd ^. _1 . _entityKey
|
||||
udn = pcd ^. _2
|
||||
ok = maybe False dcrIsOk $ Map.lookup pid participantResults
|
||||
in if ok then acc else Map.insertWith (<>) tid (Map.singleton (udn,pid) pcd) acc
|
||||
badTutPartMap :: Map TutorialId (Map (UserDisplayName, TutorialParticipantId) ParticipantCheckData) -- UserDisplayName as Key ensures proper sort order
|
||||
badTutPartMap = foldl' sortBadParticipant mempty parts_avs
|
||||
|
||||
mkBaddieWgt :: TutorialParticipantId -> ParticipantCheckData -> Widget
|
||||
mkBaddieWgt pid pcd =
|
||||
let name = nameWidget (pcd ^. _2) (pcd ^. _3)
|
||||
bookFirm = pcd ^. _5
|
||||
problemText = maybe (text2widget "???") (dcr2widgetTxt bookFirm) (Map.lookup pid participantResults)
|
||||
problemIcons = maybe mempty (dcr2widgetIcn bookFirm) (Map.lookup pid participantResults)
|
||||
in [whamlet|^{name}: ^{problemIcons} ^{problemText}|]
|
||||
|
||||
siteLayoutMsg MsgMenuSchoolDayCheck $ do
|
||||
setTitleI MsgMenuSchoolDayCheck
|
||||
[whamlet|
|
||||
<section>
|
||||
<h2>
|
||||
_{MsgMenuSchoolDay ssh dday}
|
||||
<p>
|
||||
$if Map.null badTutPartMap
|
||||
_{MsgNoProblem}.
|
||||
$else
|
||||
<dl .deflist.profile-dl>
|
||||
$forall (tid,badis) <- Map.toList badTutPartMap
|
||||
<dt .deflist__dt>
|
||||
#{maybe "???" fst (Map.lookup tid tuts)}
|
||||
<dd .deflist__dd>
|
||||
<ul>
|
||||
$forall ((_udn,pid),pcd) <- Map.toList badis
|
||||
<li>
|
||||
^{mkBaddieWgt pid pcd}
|
||||
<section>
|
||||
<p>
|
||||
<h4 .show-hide__toggle uw-show-hide data-show-hide-collapsed>
|
||||
_{MsgPossibleCheckResults}
|
||||
<p>
|
||||
<ul>
|
||||
$forall msg <- dcrMessages
|
||||
<li>_{msg}
|
||||
<p>
|
||||
_{MsgAvsUpdateDayCheck}
|
||||
<section>
|
||||
^{maybeTable' MsgExamProblemReoccurrence (Just MsgExamNoProblemReoccurrence) Nothing examProblemsTbl}
|
||||
<section>
|
||||
^{linkButton mempty (i18n MsgBtnCloseReload) [BCIsButton, BCPrimary] (SomeRoute (SchoolR ssh (SchoolDayR nd)))}
|
||||
|]
|
||||
|
||||
|
||||
type TblExamPrbsExpr = ( E.SqlExpr (Entity Course)
|
||||
`E.InnerJoin` E.SqlExpr (Entity Exam)
|
||||
`E.InnerJoin` E.SqlExpr (Entity ExamRegistration)
|
||||
`E.InnerJoin` E.SqlExpr (Entity ExamOccurrence)
|
||||
`E.InnerJoin` E.SqlExpr (Entity User)
|
||||
`E.InnerJoin` E.SqlExpr (Entity User)
|
||||
)
|
||||
type TblExamPrbsData = DBRow (Entity Course, Entity Exam, Entity ExamRegistration, Entity ExamOccurrence, Entity User, Entity User)
|
||||
|
||||
-- | Table listing double examiner problems for a given school and day
|
||||
mkExamProblemsTable :: SchoolId -> Day -> DB (Bool, Widget)
|
||||
mkExamProblemsTable =
|
||||
let dbtIdent = "exams-user" :: Text
|
||||
dbtStyle = def
|
||||
dbtSQLQuery' exOccs (crs `E.InnerJoin` exm `E.InnerJoin` reg `E.InnerJoin` occ `E.InnerJoin` usr `E.InnerJoin` xmr) = do
|
||||
EL.on $ xmr E.^. UserId E.=?. occ E.^. ExamOccurrenceExaminer
|
||||
EL.on $ usr E.^. UserId E.==. reg E.^. ExamRegistrationUser
|
||||
EL.on $ occ E.^. ExamOccurrenceId E.=?. reg E.^. ExamRegistrationOccurrence
|
||||
EL.on $ exm E.^. ExamId E.==. reg E.^. ExamRegistrationExam
|
||||
EL.on $ exm E.^. ExamCourse E.==. crs E.^. CourseId
|
||||
E.where_ $ occ E.^. ExamOccurrenceId `E.in_` E.vals exOccs
|
||||
E.&&. E.exists (do
|
||||
altReg :& altOcc <- E.from $ E.table @ExamRegistration `E.innerJoin` E.table @ExamOccurrence
|
||||
`E.on` (\(altReg :& altOcc) -> altReg E.^. ExamRegistrationOccurrence E.?=. altOcc E.^. ExamOccurrenceId)
|
||||
E.where_ $ altReg E.^. ExamRegistrationUser E.==. reg E.^. ExamRegistrationUser
|
||||
E.&&. altReg E.^. ExamRegistrationId E.!=. reg E.^. ExamRegistrationId
|
||||
E.&&. altOcc E.^. ExamOccurrenceExaminer E.==. occ E.^. ExamOccurrenceExaminer
|
||||
E.&&. altOcc E.^. ExamOccurrenceId E.!=. occ E.^. ExamOccurrenceId
|
||||
)
|
||||
return (crs,exm,reg,occ,usr,xmr)
|
||||
queryExmCourse :: TblExamPrbsExpr -> E.SqlExpr (Entity Course)
|
||||
queryExmCourse = $(sqlIJproj 6 1)
|
||||
queryExam :: TblExamPrbsExpr -> E.SqlExpr (Entity Exam)
|
||||
queryExam = $(sqlIJproj 6 2)
|
||||
queryRegistration :: TblExamPrbsExpr -> E.SqlExpr (Entity ExamRegistration)
|
||||
queryRegistration = $(sqlIJproj 6 3)
|
||||
queryOccurrence :: TblExamPrbsExpr -> E.SqlExpr (Entity ExamOccurrence)
|
||||
queryOccurrence = $(sqlIJproj 6 4)
|
||||
queryTestee :: TblExamPrbsExpr -> E.SqlExpr (Entity User)
|
||||
queryTestee = $(sqlIJproj 6 5)
|
||||
queryExaminer :: TblExamPrbsExpr -> E.SqlExpr (Entity User)
|
||||
queryExaminer = $(sqlIJproj 6 6)
|
||||
resultExmCourse :: Lens' TblExamPrbsData (Entity Course)
|
||||
resultExmCourse = _dbrOutput . _1
|
||||
resultExam :: Lens' TblExamPrbsData (Entity Exam)
|
||||
resultExam = _dbrOutput . _2
|
||||
resultRegistration :: Lens' TblExamPrbsData (Entity ExamRegistration)
|
||||
resultRegistration = _dbrOutput . _3
|
||||
resultOccurrence :: Lens' TblExamPrbsData (Entity ExamOccurrence)
|
||||
resultOccurrence = _dbrOutput . _4
|
||||
resultTestee :: Lens' TblExamPrbsData (Entity User)
|
||||
resultTestee = _dbrOutput . _5
|
||||
resultExaminer :: Lens' TblExamPrbsData (Entity User)
|
||||
resultExaminer = _dbrOutput . _6
|
||||
dbtRowKey = queryRegistration >>> (E.^. ExamRegistrationId)
|
||||
dbtProj = dbtProjId
|
||||
dbtColonnade = mconcat
|
||||
[ sortable (Just "course") (i18nCell MsgTableCourse) $ fmap addIndicatorCell courseCell <$> view (resultExmCourse . _entityVal)
|
||||
, sortable (Just "exam") (i18nCell MsgCourseExam) $ \row -> examCell (row ^. resultExmCourse . _entityVal) (row ^. resultExam . _entityVal)
|
||||
, sortable (Just "registration")(i18nCell MsgCourseExamRegistrationTime)$ dateCell . view (resultRegistration . _entityVal . _examRegistrationTime)
|
||||
, sortable (Just "occurrence") (i18nCell MsgTableExamOccurrence) $ examOccurrenceCell . view resultOccurrence
|
||||
, sortable (Just "testee") (i18nCell MsgExamParticipant) $ cellHasUserLink ForProfileDataR . view resultTestee
|
||||
, sortable (Just "examiner") (i18nCell MsgExamCorrectors) $ cellHasUser . view resultExaminer
|
||||
]
|
||||
validator = def & defaultSorting [SortAscBy "course", SortAscBy "exam", SortAscBy "testee"] -- [SortDescBy "registration"]
|
||||
dbtSorting = Map.fromList
|
||||
[ ( "course" , SortColumn $ queryExmCourse >>> (E.^. CourseName))
|
||||
, ( "exam" , SortColumn $ queryExam >>> (E.^. ExamName))
|
||||
, ( "registration", SortColumn $ queryRegistration >>> (E.^. ExamRegistrationTime))
|
||||
, ( "occurrence" , SortColumn $ queryOccurrence >>> (E.^. ExamOccurrenceName))
|
||||
, ( "testee" , SortColumn $ queryTestee >>> (E.^. UserDisplayName))
|
||||
, ( "examiner" , SortColumn $ queryExaminer >>> (E.^. UserDisplayName))
|
||||
]
|
||||
dbtFilter = mempty
|
||||
dbtFilterUI = mempty
|
||||
dbtParams = def
|
||||
dbtCsvEncode = noCsvEncode
|
||||
dbtCsvDecode = Nothing
|
||||
dbtExtraReps = []
|
||||
in \ssh nd -> do
|
||||
exOccs <- getDayExamOccurrences False ssh Nothing (nd,nd)
|
||||
let dbtSQLQuery = dbtSQLQuery' $ Map.keys exOccs
|
||||
(_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
||||
|
||||
@ -1,375 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2022-2025 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
{-# LANGUAGE TypeApplications, BlockArguments #-}
|
||||
|
||||
|
||||
module Handler.Tutorial.Users
|
||||
( getTUsersR, postTUsersR
|
||||
, getTExamR, postTExamR
|
||||
) where
|
||||
|
||||
import Import
|
||||
import Control.Monad.Zip (munzip)
|
||||
|
||||
import Utils.Form
|
||||
import Utils.Print
|
||||
import Handler.Utils
|
||||
import Handler.Utils.Course
|
||||
import Handler.Utils.Course.Cache
|
||||
import Handler.Utils.Tutorial
|
||||
import Handler.Exam.Form (ExamOccurrenceForm(..), examOccurrenceMultiForm, upsertExamOccurrences, copyExamOccurrences)
|
||||
import Database.Persist.Sql (deleteWhereCount)
|
||||
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
|
||||
-- import qualified Data.Text as Text
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
-- import qualified Data.Time.Zones as TZ
|
||||
|
||||
import Database.Esqueleto.Experimental ((:&)(..))
|
||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
|
||||
import Handler.Course.Users
|
||||
|
||||
|
||||
data TutorialUserAction
|
||||
= TutorialUserAssignExam
|
||||
| TutorialUserPrintQualification
|
||||
| TutorialUserRenewQualification
|
||||
| TutorialUserGrantQualification
|
||||
| TutorialUserSendMail
|
||||
| TutorialUserDeregister
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
|
||||
instance Universe TutorialUserAction
|
||||
instance Finite TutorialUserAction
|
||||
nullaryPathPiece ''TutorialUserAction $ camelToPathPiece' 2
|
||||
embedRenderMessage ''UniWorX ''TutorialUserAction id
|
||||
|
||||
data TutorialUserActionData
|
||||
= TutorialUserPrintQualificationData
|
||||
| TutorialUserRenewQualificationData
|
||||
{ tuQualification :: QualificationId }
|
||||
| TutorialUserGrantQualificationData
|
||||
{ tuQualification :: QualificationId
|
||||
, tuValidUntil :: Maybe Day
|
||||
}
|
||||
| TutorialUserSendMailData
|
||||
| TutorialUserDeregisterData
|
||||
| TutorialUserAssignExamData
|
||||
{ tuOccurrenceId :: ExamOccurrenceId
|
||||
, tuExaminerAgain :: Bool
|
||||
, tuReassign :: Bool
|
||||
}
|
||||
deriving (Eq, Ord, Read, Show, Generic)
|
||||
|
||||
-- non-table form for general tutorial actions
|
||||
data GenTutAction
|
||||
= GenTutActShowExam
|
||||
| GenTutActOccCopyWeek
|
||||
| GenTutActOccCopyLast
|
||||
| GenTutActOccEdit
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||
deriving anyclass (Universe, Finite)
|
||||
|
||||
nullaryPathPiece ''GenTutAction $ camelToPathPiece' 1
|
||||
embedRenderMessage ''UniWorX ''GenTutAction id
|
||||
|
||||
data GenTutActionData = GenTutActionData { gtaAct :: GenTutAction, gtaExam :: ExamId }
|
||||
deriving (Eq, Ord, Show, Generic)
|
||||
|
||||
-- mkGenTutForm :: [Filter Exam] -> Form GenTutActionData
|
||||
-- mkGenTutForm fltr = renderAForm FormStandard maa
|
||||
-- where
|
||||
-- maa = multiActionA acts (fslI MsgCourseExam) Nothing
|
||||
|
||||
-- acts :: Map GenTutAction (AForm Handler GenTutActionData)
|
||||
-- acts = Map.fromList
|
||||
-- [ (GenTutActOccCopy, GenTutActOccCopyData <$> areq (examFieldFilter (Just $ SomeMessage MsgMenuExamNew) fltr) (fslI MsgCourseExam) Nothing)
|
||||
-- , (GenTutActOccEdit, GenTutActOccEditData <$> aopt (examFieldFilter (Just $ SomeMessage MsgMenuExamNew) fltr) (fslI MsgCourseExam) Nothing)
|
||||
-- ]
|
||||
mkGenTutForm :: [Filter Exam] -> Form GenTutActionData
|
||||
mkGenTutForm fltr html = do
|
||||
(actRes, actView) <- mreq (selectFieldList ((\a->(a,a)) <$> universeF)) (fslI MsgCourseExam) Nothing
|
||||
(exmRes, exmView) <- mreq (examFieldFilter (Just $ SomeMessage MsgMenuExamNew) fltr) (fslI MsgCourseExam) Nothing
|
||||
let res :: FormResult GenTutAction -> FormResult ExamId -> FormResult GenTutActionData
|
||||
res (FormSuccess gta) (FormSuccess eid) = FormSuccess $ GenTutActionData{gtaAct=gta, gtaExam=eid}
|
||||
res (FormFailure e1) (FormFailure e2) = FormFailure $ e1 <> e2
|
||||
res (FormFailure e) _ = FormFailure e
|
||||
res _ (FormFailure e) = FormFailure e
|
||||
res _ _ = FormMissing
|
||||
viw = [whamlet|
|
||||
<p>
|
||||
#{html}^{fvInput actView} _{MsgFor} ^{fvInput exmView}
|
||||
|]
|
||||
return (res actRes exmRes, viw)
|
||||
|
||||
|
||||
getTUsersR, postTUsersR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler TypedContent
|
||||
getTUsersR = postTUsersR
|
||||
postTUsersR tid ssh csh tutn = do
|
||||
let heading = prependCourseTitle tid ssh csh $ CI.original tutn
|
||||
croute = CTutorialR tid ssh csh tutn TUsersR
|
||||
now <- liftIO getCurrentTime
|
||||
let nowaday = utctDay now
|
||||
isAdmin <- hasReadAccessTo AdminR
|
||||
(Entity tutid tut@Tutorial{..}, (participantRes, participantTable), qualifications, dbegin, hasExams, exmFltr, exOccs) <- runDB do
|
||||
trm <- get404 tid
|
||||
-- cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||
-- tutEnt@(Entity tutid _) <- fetchTutorial tid ssh csh tutn
|
||||
(cid, tutEnt@(Entity tutid _)) <- fetchCourseIdTutorial tid ssh csh tutn
|
||||
qualifications <- getCourseQualifications cid
|
||||
let dayExpiry = case nubOrd (mapMaybe (view _qualificationValidDuration) qualifications) of
|
||||
[oneDuration] -> Just $ Just $ computeNewValidDate oneDuration nowaday -- suggest end day only if it is unique for all course qualifications
|
||||
_ -> Nothing -- using the minimum here causes confusion, better leave blank!
|
||||
colChoices = mconcat $
|
||||
[ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
|
||||
, colUserNameModalHdr MsgTableCourseMembers ForProfileDataR
|
||||
, colUserEmail
|
||||
, colUserMatriclenr isAdmin
|
||||
] <>
|
||||
[ colUserQualificationBlocked isAdmin nowaday q | q <- qualifications] <>
|
||||
[ colUserExamOccurrencesCheck tid ssh csh
|
||||
, colUserExams tid ssh csh
|
||||
]
|
||||
psValidator = def
|
||||
& defaultSortingByName
|
||||
& restrictSorting (\name _ -> none (== name) ["note", "registration", "tutorials", "exams", "submission-group", "state"]) -- We need to be careful to restrict allowed sorting/filter to not expose sensitive information
|
||||
& restrictFilter (\name _ -> none (== name) ["tutorial", "exam", "submission-group", "active", "has-personalised-sheet-files"])
|
||||
isInTut q = E.exists do
|
||||
tutorialParticipant <- E.from $ E.table @TutorialParticipant
|
||||
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. queryUser q E.^. UserId
|
||||
E.&&. tutorialParticipant E.^. TutorialParticipantTutorial E.==. E.val tutid
|
||||
csvColChoices = flip elem ["name", "matriculation", "email", "qualifications"]
|
||||
qualOptions = qualificationsOptionList qualifications
|
||||
lessons = occurringLessons trm $ tutEnt ^. _entityVal . _tutorialTime . _Wrapped'
|
||||
timespan = lessonTimesSpan lessons
|
||||
(dbegin, dend) = munzip timespan
|
||||
tbegin = toMidnight . succ <$> dbegin
|
||||
tend = toMidnight <$> dend
|
||||
exmFltr = ([ExamEnd >=. tbegin] ||. [ExamEnd ==. Nothing]) ++ [ExamCourse ==. cid, ExamStart <=. tend]
|
||||
-- $logInfoS "ExamOccurrenceForm" [st|Exams from #{tshow tbegin} until #{tshow tend}.|]
|
||||
exOccs <- flip foldMapM timespan $ getDayExamOccurrences False ssh $ Just cid -- :: ExamOccurrenceMap
|
||||
hasExams <- if null exOccs then exists exmFltr else pure True
|
||||
let
|
||||
acts :: Map TutorialUserAction (AForm Handler TutorialUserActionData)
|
||||
acts = Map.fromList $
|
||||
bcons (not $ null exOccs)
|
||||
( TutorialUserAssignExam
|
||||
, TutorialUserAssignExamData
|
||||
<$> apopt (selectField $ pure $ mkExamOccurrenceOptions exOccs) (fslI MsgCourseUserExamOccurrences) Nothing
|
||||
<*> apopt checkBoxField (fslI MsgCourseUserExamOccurrenceAgainExaminer) (Just False)
|
||||
<*> apopt checkBoxField (fslI MsgCourseUserExamOccurrenceOverride) (Just False)
|
||||
) $
|
||||
(if null qualifications then mempty else
|
||||
[ ( TutorialUserRenewQualification
|
||||
, TutorialUserRenewQualificationData
|
||||
<$> apopt (selectField $ pure qualOptions) (fslI MsgQualificationName) Nothing
|
||||
)
|
||||
, ( TutorialUserGrantQualification
|
||||
, TutorialUserGrantQualificationData
|
||||
<$> apopt (selectField $ pure qualOptions) (fslI MsgQualificationName) Nothing
|
||||
<*> aopt dayField (fslI MsgLmsQualificationValidUntil & setTooltip MsgTutorialUserGrantQualificationDateTooltip) dayExpiry
|
||||
)
|
||||
]
|
||||
) ++
|
||||
[ ( TutorialUserSendMail , pure TutorialUserSendMailData )
|
||||
, ( TutorialUserDeregister , pure TutorialUserDeregisterData )
|
||||
, ( TutorialUserPrintQualification, pure TutorialUserPrintQualificationData )
|
||||
]
|
||||
table <- makeCourseUserTable cid acts isInTut colChoices psValidator (Just csvColChoices)
|
||||
return (tutEnt, table, qualifications, dbegin, hasExams, exmFltr, exOccs)
|
||||
|
||||
let courseQids = entities2map qualifications
|
||||
tcontent <- formResultMaybe participantRes $ \case
|
||||
(TutorialUserPrintQualificationData, selectedUsers) -> do
|
||||
rcvr <- requireAuth
|
||||
encRcvr <- encrypt $ entityKey rcvr
|
||||
letters <- runDB $ makeCourseCertificates tut Nothing $ toList selectedUsers
|
||||
let mbAletter = anyone letters
|
||||
case mbAletter of
|
||||
Nothing -> addMessageI Error MsgErrorUnknownFormAction >> return Nothing -- cannot really happen
|
||||
Just aletter -> do
|
||||
apcIdent <- letterApcIdent aletter encRcvr now
|
||||
let fName = letterFileName aletter
|
||||
renderLetters rcvr letters apcIdent >>= \case
|
||||
Left err -> sendResponseStatus internalServerError500 $ "PDF generation failed: \n" <> err
|
||||
Right pdf -> return $ Just (sendByteStringAsFile fName (LBS.toStrict pdf) now)
|
||||
-- sendResponseByteStringFile "demoPDF.pdf" (LBS.toStrict pdf)
|
||||
-- let typePDF :: ContentType
|
||||
-- typePDF = "application/pdf"
|
||||
-- sendResponse (typePDF, toContent pdf)
|
||||
(TutorialUserGrantQualificationData{..}, selectedUsers)
|
||||
| Just grantQual <- Map.lookup tuQualification courseQids ->
|
||||
case tuValidUntil <|> (flip computeNewValidDate nowaday <$> qualificationValidDuration grantQual) of
|
||||
Nothing -> do -- TODO: change QualificationUser to have an optionnal validUntil for idefinitely valid qualifications
|
||||
addMessageI Error $ MsgTutorialUserGrantQualificationDateError $ qualificationShorthand grantQual
|
||||
return Nothing
|
||||
(Just expiryDay) -> do
|
||||
let qsh = qualificationShorthand grantQual
|
||||
reason = "Kurs " <> CI.original (unSchoolKey ssh) <> "-" <> CI.original csh <> "-" <> CI.original tutn
|
||||
selUsrs = Set.toList selectedUsers
|
||||
expiryDayText <- formatTime SelFormatDate expiryDay
|
||||
nterm <- runDB $ do
|
||||
forM_ selUsrs $ upsertQualificationUser tuQualification now expiryDay Nothing reason
|
||||
terminateLms (LmsOrphanReasonManualGrant [st|bis #{expiryDayText}, #{reason}|]) tuQualification selUsrs
|
||||
addMessageI (if 0 < Set.size selectedUsers then Success else Warning) . MsgTutorialUserGrantedQualification qsh expiryDayText $ Set.size selectedUsers
|
||||
when (nterm > 0) $ addMessageI Warning $ MsgLmsActTerminated nterm
|
||||
reloadKeepGetParams croute
|
||||
(TutorialUserRenewQualificationData{..}, selectedUsers)
|
||||
| Just grantQual <- Map.lookup tuQualification courseQids -> do
|
||||
let qsh = qualificationShorthand grantQual
|
||||
selUsrs = Set.toList selectedUsers
|
||||
mr <- getMessageRender
|
||||
(noks,nterm) <- runDB $ (,)
|
||||
<$> renewValidQualificationUsers tuQualification Nothing Nothing selUsrs
|
||||
<*> terminateLms (LmsOrphanReasonManualGrant $ mr heading) tuQualification selUsrs
|
||||
addMessageI (if noks > 0 && noks == Set.size selectedUsers then Success else Warning) $ MsgTutorialUserRenewedQualification qsh noks
|
||||
when (nterm > 0) $ addMessageI Warning $ MsgLmsActTerminated nterm
|
||||
reloadKeepGetParams croute
|
||||
(TutorialUserSendMailData, selectedUsers) -> do
|
||||
cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser]
|
||||
redirect (CTutorialR tid ssh csh tutn TCommR, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cids])
|
||||
(TutorialUserDeregisterData, selectedUsers) -> do
|
||||
nrDel <- runDB $ deleteWhereCount
|
||||
[ TutorialParticipantTutorial ==. tutid
|
||||
, TutorialParticipantUser <-. Set.toList selectedUsers
|
||||
]
|
||||
addMessageI Success $ MsgTutorialUsersDeregistered nrDel
|
||||
reloadKeepGetParams croute
|
||||
(TutorialUserAssignExamData{..}, setSelectedUsers)
|
||||
| (Just (ExamOccurrence{..}, _, (eid,_))) <- Map.lookup tuOccurrenceId exOccs -> do
|
||||
assignRes <- runDB $ do
|
||||
(Set.toList &&& Set.size -> (selectedUsers, nr_usrs)) <- if -- remove duplicate examiners, if desired
|
||||
| isJust examOccurrenceExaminer && not tuExaminerAgain -> do
|
||||
conflictingUsers <- E.select $ do
|
||||
reg :& occ <- E.from $ E.table @ExamRegistration
|
||||
`E.innerJoin` E.table @ExamOccurrence
|
||||
`E.on` (\(reg :& occ) -> occ E.^. ExamOccurrenceId E.=?. reg E.^. ExamRegistrationOccurrence)
|
||||
E.where_ $ occ E.^. ExamOccurrenceExaminer E.==. E.val examOccurrenceExaminer
|
||||
E.&&. occ E.^. ExamOccurrenceExam E.!=. E.val examOccurrenceExam
|
||||
E.&&. (reg E.^. ExamRegistrationUser `E.in_` E.vals setSelectedUsers)
|
||||
E.orderBy [E.asc $ reg E.^. ExamRegistrationUser]
|
||||
E.distinct $ pure $ reg E.^. ExamRegistrationUser
|
||||
return $ setSelectedUsers `Set.difference` Set.fromAscList (E.unValue <$> conflictingUsers)
|
||||
| otherwise -> return setSelectedUsers
|
||||
runExceptT $ do
|
||||
whenIsJust examOccurrenceCapacity $ \(fromIntegral -> totalCap) -> do
|
||||
usedCap <- lift $ count [ExamRegistrationOccurrence ==. Just tuOccurrenceId, ExamRegistrationUser /<-. selectedUsers]
|
||||
let remCap = totalCap - usedCap
|
||||
when (nr_usrs > remCap) $ throwE $ MsgExamRoomCapacityInsufficient remCap
|
||||
let regTemplate uid = ExamRegistration eid uid (Just tuOccurrenceId) now
|
||||
lift $ if tuReassign
|
||||
then putMany [regTemplate uid | uid <- selectedUsers] >> pure nr_usrs
|
||||
else forM selectedUsers (insertUnique . regTemplate) <&> (length . catMaybes)
|
||||
case assignRes of
|
||||
Left errm -> do
|
||||
addMessageI Error errm
|
||||
return Nothing
|
||||
Right nrOk -> do
|
||||
let total = Set.size setSelectedUsers
|
||||
allok = bool Warning Success $ nrOk == total
|
||||
addMessageI allok $ MsgTutorialUserExamAssignedFor nrOk total $ ciOriginal examOccurrenceName
|
||||
reloadKeepGetParams croute
|
||||
_other -> addMessageI Error MsgErrorUnknownFormAction >> return Nothing
|
||||
|
||||
case tcontent of
|
||||
Just act -> act -- execute action and return produced content (i.e. pdf)
|
||||
Nothing -> do -- no table action content to return, continue normally
|
||||
let mkExamCreateBtn = linkButton mempty (msg2widget MsgMenuExamNew) [BCIsButton, BCPrimary] $ SomeRoute $ CourseR tid ssh csh CExamNewR
|
||||
((gtaRes, gtaWgt), gtaEnctype) <- runFormPost . identifyForm ("FIDGeneralTutorialAction"::Text) $ mkGenTutForm exmFltr
|
||||
let gtaAnchor = "general-tutorial-action-form" :: Text
|
||||
gtaRoute = croute :#: gtaAnchor
|
||||
gtaForm = wrapForm' BtnPerform gtaWgt FormSettings
|
||||
{ formMethod = POST
|
||||
, formAction = Just . SomeRoute $ gtaRoute
|
||||
, formEncoding = gtaEnctype
|
||||
, formAttrs = []
|
||||
, formSubmit = FormSubmit
|
||||
, formAnchor = Just gtaAnchor
|
||||
}
|
||||
copyAction eId step = case dbegin of
|
||||
Nothing -> addMessageI Error MsgExamOccurrenceCopyNoStartDate
|
||||
Just dto ->
|
||||
let cfailure = addMessageI Error MsgExamOccurrenceCopyFail
|
||||
csuccess n = addMessageI Success (MsgExamOccurrencesCopied n) >> reloadKeepGetParams croute
|
||||
copyFrom dfrom = copyExamOccurrences eId dfrom dto <&> (toMaybe =<< (> 0))
|
||||
step_dto = addDays (negate step) dto
|
||||
in maybeM cfailure csuccess $
|
||||
runDB $ firstJustM $ map copyFrom $ take 69 $ drop 1 [dto, step_dto..] -- search for up to 2 months / 1 year backwards
|
||||
formResult gtaRes $ \GenTutActionData{..} -> case gtaAct of
|
||||
GenTutActOccCopyWeek -> copyAction gtaExam 7
|
||||
GenTutActOccCopyLast -> copyAction gtaExam 1
|
||||
GenTutActOccEdit -> do
|
||||
Exam{examName=ename} <- runDBRead $ get404 gtaExam
|
||||
redirect $ CTutorialR tid ssh csh tutn $ TExamR ename
|
||||
GenTutActShowExam -> do
|
||||
Exam{examName=ename} <- runDBRead $ get404 gtaExam
|
||||
redirect (CExamR tid ssh csh ename EUsersR, [("exam-users-tutorial", toPathPiece tutn)])
|
||||
|
||||
tutors <- runDBRead $ E.select do
|
||||
(tutor :& user) <- E.from $ E.table @Tutor `E.innerJoin` E.table @User
|
||||
`E.on` (\(tutor :& user) -> tutor E.^. TutorUser E.==. user E.^. UserId)
|
||||
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
|
||||
return user
|
||||
-- $(i18nWidgetFile "exam-missing")
|
||||
html <- siteLayoutMsg heading do
|
||||
setTitleI heading
|
||||
$(widgetFile "tutorial-participants")
|
||||
return $ toTypedContent html
|
||||
|
||||
|
||||
getTExamR, postTExamR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> ExamName -> Handler Html
|
||||
getTExamR = postTExamR
|
||||
postTExamR tid ssh csh tutn exmName = do
|
||||
let baseroute = CTutorialR tid ssh csh tutn
|
||||
(Entity{entityKey=eId,entityVal=exm},exOccs) <- runDB do
|
||||
trm <- get404 tid
|
||||
(cid, tutEnt) <- fetchCourseIdTutorial tid ssh csh tutn
|
||||
exm <- getBy404 $ UniqueExam cid exmName
|
||||
let lessons = occurringLessons trm $ tutEnt ^. _entityVal . _tutorialTime . _Wrapped'
|
||||
timespan = lessonTimesSpan lessons
|
||||
-- (fmap (toMidnight . succ) -> tbegin, fmap toMidnight -> tend) = munzip timespan
|
||||
-- exms <- selectList ([ExamCourse ==. cid, ExamStart <=. tend] ++ ([ExamEnd >=. tbegin] ||. [ExamEnd ==. Nothing])) [Asc ExamName]
|
||||
exOccs <- flip foldMapM timespan $ getDayExamOccurrences False ssh $ Just cid
|
||||
return (exm,exOccs)
|
||||
cueId :: CryptoUUIDExam <- encrypt eId
|
||||
let eid2eos = convertExamOccurrenceMap exOccs
|
||||
(cuEoIds, eos) = munzip $ Map.lookup eId eid2eos
|
||||
exOcForm = (,,)
|
||||
<$> areq hiddenField "" (Just cueId)
|
||||
<*> areq (mkSetField hiddenField) "" cuEoIds
|
||||
<*> examOccurrenceMultiForm eos
|
||||
((eofRes, eofWgt), eofEnctype) <- runFormPost $ identifyForm FIDTutorialExamOccurrences $ renderAForm FormStandard exOcForm
|
||||
let eofForm = wrapForm eofWgt def{formEncoding = eofEnctype}
|
||||
formResult eofRes $ \(edCEId, edCEOIds, edOccs) -> do
|
||||
let ceoidsDelete = edCEOIds `Set.difference` setMapMaybe eofId edOccs
|
||||
$logInfoS "ExamOccurrenceEdit" [st|Exam-Edit: #{length edCEOIds} old occurrences, #{length ceoidsDelete} to delete, #{length $ Set.filter (isNothing . eofId) edOccs} to insert, #{length $ Set.filter (isJust . eofId) edOccs} to edit|]
|
||||
reId <- decrypt edCEId
|
||||
eoIdsDelete <- mapM decrypt $ Set.toList ceoidsDelete
|
||||
when (reId == eId) $ do
|
||||
(fromIntegral -> nrDel, nrUps) <- runDB $ (,)
|
||||
<$> deleteWhereCount [ExamOccurrenceExam ==. reId, ExamOccurrenceId <-. eoIdsDelete]
|
||||
<*> upsertExamOccurrences eId (Set.toList edOccs)
|
||||
let nr = nrUps + nrDel
|
||||
mstat = if nr > 0 then Success else Warning
|
||||
addMessageI mstat $ MsgExamOccurrencesEdited nrUps nrDel
|
||||
reload $ baseroute $ TExamR exmName
|
||||
|
||||
let csh_tutn = csh <> "-" <> tutn -- hack to reuse prependCourseTitle
|
||||
heading = prependCourseTitle tid ssh csh_tutn $ MsgMenuTutorialExam exmName
|
||||
siteLayoutMsg heading do
|
||||
-- setTitle $ citext2Html exmName
|
||||
setTitleI heading
|
||||
[whamlet|
|
||||
<section>
|
||||
<h2>#{CI.original exmName}
|
||||
<p>#{examDescription exm}
|
||||
<section>
|
||||
^{eofForm}
|
||||
|]
|
||||
@ -1,188 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
module Handler.Utils.Course.Cache where
|
||||
|
||||
import Import
|
||||
|
||||
import Handler.Utils
|
||||
-- import Handler.Utils.Occurrences
|
||||
import Handler.Exam.Form (ExamOccurrenceForm(..))
|
||||
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Map as Map
|
||||
-- import qualified Data.Aeson as Aeson
|
||||
|
||||
|
||||
-- import Database.Persist.Sql (updateWhereCount)
|
||||
import Database.Esqueleto.Experimental ((:&)(..))
|
||||
import qualified Database.Esqueleto.Experimental as E
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
-- import Database.Esqueleto.PostgreSQL.JSON ((@>.))
|
||||
-- import qualified Database.Esqueleto.PostgreSQL.JSON as E hiding ((?.))
|
||||
|
||||
|
||||
|
||||
-- partial JSON object to be used for filtering with "@>"
|
||||
-- ensure that a GIN index for the jsonb column is created in Model.Migration.Definitions
|
||||
-- occurrenceDayValue :: Day -> Value
|
||||
-- occurrenceDayValue d = Aeson.object
|
||||
-- [ "exceptions" Aeson..=
|
||||
-- [ Aeson.object
|
||||
-- [ "exception" Aeson..= ("occur"::Text)
|
||||
-- , "day" Aeson..= d
|
||||
-- ] ] ]
|
||||
|
||||
{- More efficient DB-only version, but ignores regular schedules
|
||||
getDayTutorials :: SchoolId -> Day -> DB [TutorialId]
|
||||
getDayTutorials ssh d = E.unValue <<$>> E.select (do
|
||||
(trm :& crs :& tut) <- E.from $ E.table @Term
|
||||
`E.innerJoin` E.table @Course `E.on` (\(trm :& crs) -> crs E.^. CourseTerm E.==. trm E.^. TermId)
|
||||
`E.innerJoin` E.table @Tutorial `E.on` (\(_ :& crs :& tut) -> crs E.^. CourseId E.==. tut E.^. TutorialCourse)
|
||||
E.where_ $ E.between (E.val d) (trm E.^. TermStart, trm E.^. TermEnd)
|
||||
E.&&. crs E.^. CourseSchool E.==. E.val ssh
|
||||
E.&&. (E.just (tut E.^. TutorialTime) @>. E.jsonbVal (occurrenceDayValue d))
|
||||
return $ tut E.^. TutorialId
|
||||
)
|
||||
-}
|
||||
|
||||
-- | Datatype to be used as key for memcaching DayTask related stuff; note that newtype-CacheKeys are optimized away, so multiple constructors are advisable
|
||||
data CourseCacheKeys
|
||||
= CacheKeyTutorialOccurrences SchoolId (Day,Day) -- ^ Map TutorialId (TutorialName, [LessonTime])
|
||||
| CacheKeyExamOccurrences SchoolId (Day,Day) (Maybe CourseId) -- ^ Map ExamOccurrenceId (CourseId, ExamName, ExamOccurrence)
|
||||
| CacheKeySuggsParticipantNote SchoolId TutorialId
|
||||
| CacheKeySuggsAttendanceNote SchoolId TutorialId
|
||||
| CacheKeyTutorialCheckResults SchoolId Day
|
||||
deriving (Eq, Ord, Read, Show, Generic)
|
||||
deriving anyclass (Hashable, Binary, NFData)
|
||||
|
||||
-- getDayTutorials :: SchoolId -> (Day,Day) -> DB [TutorialId]
|
||||
-- getDayTutorials ssh dlimit@(dstart, dend )
|
||||
-- | dstart > dend = return mempty
|
||||
-- | otherwise = memcachedByClass MemcachedKeyClassTutorialOccurrences (Just . Right $ 12 * diffDay) (CacheKeyTutorialOccurrences ssh dlimit) $ do -- same key is ok, distinguished by return type
|
||||
-- candidates <- E.select $ do
|
||||
-- (trm :& crs :& tut) <- E.from $ E.table @Term
|
||||
-- `E.innerJoin` E.table @Course `E.on` (\(trm :& crs) -> crs E.^. CourseTerm E.==. trm E.^. TermId)
|
||||
-- `E.innerJoin` E.table @Tutorial `E.on` (\(_ :& crs :& tut) -> crs E.^. CourseId E.==. tut E.^. TutorialCourse)
|
||||
-- E.where_ $ crs E.^. CourseSchool E.==. E.val ssh
|
||||
-- E.&&. trm E.^. TermStart E.<=. E.val dend
|
||||
-- E.&&. trm E.^. TermEnd E.>=. E.val dstart
|
||||
-- return (trm, tut, E.just (tut E.^. TutorialTime) @>. E.jsonbVal (occurrenceDayValue dstart))
|
||||
-- -- logErrorS "DAILY" $ foldMap (\(Entity{entityVal=someTerm},Entity{entityVal=Tutorial{..}},_) -> tshow someTerm <> " *** " <> ciOriginal tutorialName <> ": " <> tshow (unJSONB tutorialTime)) candidates
|
||||
-- return $ mapMaybe checkCandidate candidates
|
||||
-- where
|
||||
-- period = Set.fromAscList [dstart..dend]
|
||||
|
||||
-- checkCandidate (_, Entity{entityKey=tutId}, E.unValue -> True) = Just tutId -- most common case
|
||||
-- checkCandidate (Entity{entityVal=trm}, Entity{entityKey=tutId, entityVal=Tutorial{tutorialTime=JSONB occ}}, _)
|
||||
-- | not $ Set.null $ Set.intersection period $ occurrencesCompute' trm occ
|
||||
-- = Just tutId
|
||||
-- | otherwise
|
||||
-- = Nothing
|
||||
|
||||
-- | like the previous version above, but also returns the lessons occurring within the given time frame
|
||||
-- Due to caching, we only use the more informative version, unless experiments with the full DB show otherwise
|
||||
getDayTutorials :: SchoolId -> (Day,Day) -> DB (Map TutorialId (TutorialName, [LessonTime]))
|
||||
getDayTutorials ssh dlimit@(dstart, dend )
|
||||
| dstart > dend = return mempty
|
||||
| otherwise = memcachedByClass MemcachedKeyClassTutorialOccurrences (Just . Right $ 12 * diffDay) (CacheKeyTutorialOccurrences ssh dlimit) $ do
|
||||
candidates <- E.select $ do
|
||||
(trm :& crs :& tut) <- E.from $ E.table @Term
|
||||
`E.innerJoin` E.table @Course `E.on` (\(trm :& crs) -> crs E.^. CourseTerm E.==. trm E.^. TermId)
|
||||
`E.innerJoin` E.table @Tutorial `E.on` (\(_ :& crs :& tut) -> crs E.^. CourseId E.==. tut E.^. TutorialCourse)
|
||||
E.where_ $ crs E.^. CourseSchool E.==. E.val ssh
|
||||
E.&&. trm E.^. TermStart E.<=. E.val dend
|
||||
E.&&. trm E.^. TermEnd E.>=. E.val dstart
|
||||
return (trm, tut)
|
||||
-- logErrorS "DAILY" $ foldMap (\(Entity{entityVal=someTerm},Entity{entityVal=Tutorial{..}},_) -> tshow someTerm <> " *** " <> ciOriginal tutorialName <> ": " <> tshow (unJSONB tutorialTime)) candidates
|
||||
return $ foldMap checkCandidate candidates
|
||||
where
|
||||
checkCandidate :: (Entity Term, Entity Tutorial) -> Map TutorialId (TutorialName, [LessonTime])
|
||||
checkCandidate (Entity{entityVal=trm}, Entity{entityKey=tutId, entityVal=Tutorial{tutorialTime=JSONB occ, tutorialName=tName}})
|
||||
| let lessons = Set.filter lessonFltr $ occurringLessons trm occ
|
||||
, notNull lessons
|
||||
= Map.singleton tutId (tName , Set.toAscList lessons) -- due to Set not having a Functor instance, we need mostly need lists anyway
|
||||
| otherwise
|
||||
= mempty
|
||||
|
||||
lessonFltr :: LessonTime -> Bool
|
||||
lessonFltr LessonTime{..} = dstart <= localDay lessonStart
|
||||
&& dend >= localDay lessonEnd
|
||||
|
||||
-- -- retrieve all exam occurrences for a school for a term in a given time period; uses caching
|
||||
-- getDayExamOccurrences :: SchoolId -> (Day,Day) -> DB (Map ExamOccurrenceId (CourseId, ExamName, ExamOccurrence))
|
||||
-- getDayExamOccurrences ssh dlimit@(dstart, dend )
|
||||
-- | dstart > dend = return mempty
|
||||
-- | otherwise = memcachedByClass MemcachedKeyClassExamOccurrences (Just . Right $ 12 * diffDay) (CacheKeyExamOccurrences ssh dlimit) $ do
|
||||
-- candidates <- E.select $ do
|
||||
-- (trm :& crs :& exm :& occ) <- E.from $ E.table @Term
|
||||
-- `E.innerJoin` E.table @Course `E.on` (\(trm :& crs) -> crs E.^. CourseTerm E.==. trm E.^. TermId)
|
||||
-- `E.innerJoin` E.table @Exam `E.on` (\(_ :& crs :& exm) -> crs E.^. CourseId E.==. exm E.^. ExamCourse)
|
||||
-- `E.innerJoin` E.table @ExamOccurrence `E.on` (\(_ :& _ :& exm :& occ) -> exm E.^. ExamId E.==. occ E.^. ExamOccurrenceExam)
|
||||
-- E.where_ $ E.val ssh E.==. crs E.^. CourseSchool
|
||||
-- E.&&. E.val dstart E.<=. trm E.^. TermEnd
|
||||
-- E.&&. E.val dend E.>=. trm E.^. TermStart
|
||||
-- E.&&. ( E.between (E.day $ occ E.^. ExamOccurrenceStart) (E.val dstart, E.val dend)
|
||||
-- E.||. E.between (E.dayMaybe $ occ E.^. ExamOccurrenceEnd) (E.justVal dstart, E.justVal dend)
|
||||
-- )
|
||||
-- return (exm, occ)
|
||||
-- return $ foldMap mkOccMap candidates
|
||||
-- where
|
||||
-- mkOccMap :: (Entity Exam, Entity ExamOccurrence) -> Map ExamOccurrenceId (CourseId, ExamName, ExamOccurrence)
|
||||
-- mkOccMap (entityVal -> exm, Entity{..}) = Map.singleton entityKey (exm ^. _examCourse, exm ^. _examName, entityVal)
|
||||
|
||||
type ExamOccurrenceMap = Map ExamOccurrenceId (ExamOccurrence, CryptoUUIDExamOccurrence, (ExamId, ExamName))
|
||||
type ExamToOccurrencesMap = Map ExamId (Set CryptoUUIDExamOccurrence, Set ExamOccurrenceForm)
|
||||
|
||||
-- | retrieve all exam occurrences for a school in a given time period, ignoring term times; uses caching
|
||||
-- if a CourseId is specified, only exams from that course are returned
|
||||
getDayExamOccurrences :: Bool -> SchoolId -> Maybe CourseId -> (Day,Day) -> DB ExamOccurrenceMap
|
||||
getDayExamOccurrences onlyOpen ssh mbcid dlimit@(dstart, dend)
|
||||
| dstart > dend = return mempty
|
||||
| otherwise = memcachedByClass MemcachedKeyClassExamOccurrences (Just . Right $ 12 * diffDay) (CacheKeyExamOccurrences ssh dlimit mbcid) $ do
|
||||
now <- liftIO getCurrentTime
|
||||
candidates <- E.select $ do
|
||||
(crs :& exm :& occ) <- E.from $ E.table @Course
|
||||
`E.innerJoin` E.table @Exam `E.on` (\(crs :& exm) -> crs E.^. CourseId E.==. exm E.^. ExamCourse)
|
||||
`E.innerJoin` E.table @ExamOccurrence `E.on` (\(_ :& exm :& occ) -> exm E.^. ExamId E.==. occ E.^. ExamOccurrenceExam)
|
||||
E.where_ $ E.and $ catMaybes
|
||||
[ toMaybe onlyOpen $ E.justVal now E.>=. exm E.^. ExamRegisterFrom -- fail on null
|
||||
E.&&. E.val now E.<~. exm E.^. ExamRegisterTo -- success on null
|
||||
, mbcid <&> ((E.==. (crs E.^. CourseId)) . E.val)
|
||||
, Just $ crs E.^. CourseSchool E.==. E.val ssh
|
||||
, Just $ E.withinPeriod dlimit (occ E.^. ExamOccurrenceStart) (occ E.^. ExamOccurrenceEnd)
|
||||
]
|
||||
-- E.orderBy [E.asc $ exm E.^. ExamName] -- we return a map, so the order does not matter
|
||||
return (occ, exm E.^. ExamId, exm E.^. ExamName) -- No Binary instance for Entity Exam, so we only extract what is needed for now
|
||||
foldMapM mkOccMap candidates
|
||||
where
|
||||
mkOccMap :: (Entity ExamOccurrence, E.Value ExamId, E.Value ExamName) -> DB ExamOccurrenceMap
|
||||
mkOccMap (Entity{..}, E.Value eId, E.Value eName) = encrypt entityKey <&> (\ceoId -> Map.singleton entityKey (entityVal, ceoId, (eId, eName)))
|
||||
|
||||
mkExamOccurrenceOptions :: ExamOccurrenceMap -> OptionList ExamOccurrenceId
|
||||
mkExamOccurrenceOptions = mkOptionListGrouped . map (over _2 $ sortBy (compare `on` optionDisplay)) . groupSort . map mkEOOption . Map.toList
|
||||
where
|
||||
mkEOOption :: (ExamOccurrenceId, (ExamOccurrence, CryptoUUIDExamOccurrence, (ExamId, ExamName))) -> (Text, [Option ExamOccurrenceId])
|
||||
mkEOOption (eid, (ExamOccurrence{examOccurrenceName}, ceoId, (_,eName))) = (ciOriginal eName, [Option{..}])
|
||||
where
|
||||
optionDisplay = ciOriginal examOccurrenceName
|
||||
optionExternalValue = toPathPiece ceoId
|
||||
optionInternalValue = eid
|
||||
|
||||
convertExamOccurrenceMap :: ExamOccurrenceMap -> ExamToOccurrencesMap
|
||||
convertExamOccurrenceMap eom = Map.fromListWith (<>) $ map aux $ Map.toList eom
|
||||
where
|
||||
aux :: (ExamOccurrenceId, (ExamOccurrence, CryptoUUIDExamOccurrence, (ExamId, ExamName))) -> (ExamId, (Set CryptoUUIDExamOccurrence, Set ExamOccurrenceForm))
|
||||
aux (_, (ExamOccurrence{..}, cueoId, (eid,_))) = (eid, (Set.singleton cueoId, Set.singleton ExamOccurrenceForm
|
||||
{ eofId = Just cueoId
|
||||
, eofName = Just examOccurrenceName
|
||||
, eofExaminer = examOccurrenceExaminer
|
||||
, eofRoom = examOccurrenceRoom
|
||||
, eofRoomHidden = examOccurrenceRoomHidden
|
||||
, eofCapacity = examOccurrenceCapacity
|
||||
, eofStart = examOccurrenceStart
|
||||
, eofEnd = examOccurrenceEnd
|
||||
, eofDescription = examOccurrenceDescription
|
||||
}
|
||||
))
|
||||
@ -1,38 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
module Handler.Utils.Table
|
||||
( module Handler.Utils.Table
|
||||
) where
|
||||
|
||||
import Import hiding (link)
|
||||
|
||||
import Handler.Utils.Table.Pagination as Handler.Utils.Table
|
||||
import Handler.Utils.Table.Columns as Handler.Utils.Table
|
||||
import Handler.Utils.Table.Cells as Handler.Utils.Table
|
||||
|
||||
|
||||
-- | Given a header message, a bool and widget; display widget and header only if the boolean is true
|
||||
maybeTable :: (RenderMessage UniWorX a)
|
||||
=> a -> (Bool, Widget) -> Widget
|
||||
maybeTable m = maybeTable' m Nothing Nothing
|
||||
|
||||
maybeTable' :: (RenderMessage UniWorX a)
|
||||
=> a -> Maybe a -> Maybe Widget -> (Bool, Widget) -> Widget
|
||||
maybeTable' _ Nothing _ (False, _ ) = mempty
|
||||
maybeTable' _ (Just nodata) _ (False, _ ) =
|
||||
[whamlet|
|
||||
<div .container>
|
||||
_{nodata}
|
||||
|]
|
||||
maybeTable' hdr _ mbRemark (True ,tbl) =
|
||||
[whamlet|
|
||||
<div .container>
|
||||
<h2> _{hdr}
|
||||
<div .container>
|
||||
^{tbl}
|
||||
$maybe remark <- mbRemark
|
||||
<em>_{MsgProfileRemark}
|
||||
\ ^{remark}
|
||||
|]
|
||||
@ -1,47 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2025 Steffen Jost <s.jost@fraport.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
module Utils.Company where
|
||||
|
||||
import Import.NoFoundation
|
||||
import Foundation.Type
|
||||
import Foundation.DB
|
||||
|
||||
-- import Database.Esqueleto.Experimental ((:&)(..))
|
||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||
import qualified Database.Esqueleto.Utils as E
|
||||
-- import qualified Database.Esqueleto.PostgreSQL as E
|
||||
|
||||
import Handler.Utils.Memcached
|
||||
|
||||
|
||||
-- DB Queries related to firms and supervision that are used in several places
|
||||
|
||||
|
||||
-- | check if a user is NOT associated with a company; false if company is null
|
||||
usrDoesNotBelong :: E.SqlExpr (E.Value UserId) -> E.SqlExpr (E.Value (Maybe CompanyId)) -> E.SqlExpr (E.Value Bool)
|
||||
usrDoesNotBelong uid fsh = E.isJust fsh E.&&. E.notExists (do
|
||||
uc <- E.from $ E.table @UserCompany
|
||||
E.where_ $ uc E.^. UserCompanyUser E.==. uid
|
||||
E.&&. uc E.^. UserCompanyCompany E.=?. fsh
|
||||
)
|
||||
|
||||
-- | given a supervisionship, true if supervisor is NOT associated with the supervisionship-company
|
||||
missingCompanySupervisor :: E.SqlExpr (Entity UserSupervisor) -> E.SqlExpr (E.Value Bool)
|
||||
missingCompanySupervisor us = (us E.^. UserSupervisorSupervisor) `usrDoesNotBelong` (us E.^. UserSupervisorCompany)
|
||||
|
||||
-- | given a supervisionship, true if client is NOT associated with the supervisionship-company
|
||||
missingCompanyClient :: E.SqlExpr (Entity UserSupervisor) -> E.SqlExpr (E.Value Bool)
|
||||
missingCompanyClient us = (us E.^. UserSupervisorUser) `usrDoesNotBelong` (us E.^. UserSupervisorCompany)
|
||||
|
||||
-- | once per day, check if there are supervisionships where supervisor or client are not associated witht the supervisionship-company
|
||||
areThereInsaneCompanySupervisions :: HandlerFor UniWorX (Bool, UTCTime)
|
||||
areThereInsaneCompanySupervisions = $(memcachedByHere) (Just . Right $ 22 * diffHour) [st|isane-company-supervision|] $ do
|
||||
now <- liftIO getCurrentTime
|
||||
res <- runDBRead $ E.selectExists $ do
|
||||
us <- E.from $ E.table @UserSupervisor
|
||||
E.where_ $ E.isJust (us E.^. UserSupervisorCompany)
|
||||
E.&&. (missingCompanySupervisor us E.||. missingCompanyClient us)
|
||||
$logInfoS "sanity" [st|Are there insane company supervisions: #{tshow res}|]
|
||||
return (res,now)
|
||||
@ -1,27 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Yesod.Form.Types.Instances
|
||||
() where
|
||||
|
||||
import ClassyPrelude.Yesod
|
||||
-- import Yesod.Form.Types
|
||||
|
||||
-- import Data.Default
|
||||
import Data.Binary
|
||||
|
||||
instance Default (FieldSettings site) where
|
||||
def = ""
|
||||
|
||||
deriving instance (Show a) => Show (Option a)
|
||||
|
||||
-- to memcache Option Text and Option Textarea
|
||||
deriving instance Generic (Option Text)
|
||||
deriving instance Binary (Option Text)
|
||||
|
||||
deriving newtype instance Binary Textarea
|
||||
deriving instance Generic (Option Textarea)
|
||||
deriving instance Binary (Option Textarea)
|
||||
@ -1,11 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Keine momentan offene Prüfung gefunden für _{MsgTableCourse} #{csh}.
|
||||
<p>
|
||||
^{mkExamCreateBtn}
|
||||
@ -1,11 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
<section>
|
||||
<p>
|
||||
No currently open exam found for _{MsgTableCourse} #{csh}.
|
||||
<p>
|
||||
^{mkExamCreateBtn}
|
||||
@ -1,32 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
$maybe tbl <- tableDaily
|
||||
<section>
|
||||
<p>
|
||||
^{tbl}
|
||||
<p>
|
||||
^{consistencyBtn}
|
||||
<section .profile>
|
||||
<h3>Hinweise zu den Formularspalten
|
||||
<dl .deflist.profile-dl>
|
||||
<dt .deflist__dt>
|
||||
_{MsgTutorialDrivingPermit}, _{MsgTutorialEyeExam}, _{MsgTutorialNote}
|
||||
<dd .deflist__dd>
|
||||
Pro Kurs und Teilnehmer wird je ein Wert gespeichert.
|
||||
<dt .deflist__dt>
|
||||
_{MsgTutorialDayAttendance mempty}, _{MsgTutorialDayNote mempty}
|
||||
<dd .deflist__dd>
|
||||
Pro Tag, Kurs und Teilnehmer wird je ein Wert gespeichert.
|
||||
<dt .deflist__dt>
|
||||
_{MsgTableUserParkingToken mempty}
|
||||
<dd .deflist__dd>
|
||||
Pro Tag und Teilnehmer wird ein Wert gespeichert, d.h. unabhängig vom Kurs.
|
||||
\ Daraus folgt, dass die Parkmarke immer in allen Zeilen des gleichen Teilnehmers geändert werden muss.
|
||||
$nothing
|
||||
<section>
|
||||
An diesem Tag sind zur Zeit keine Kurse eingetragen.
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
$maybe tbl <- tableDaily
|
||||
<section>
|
||||
<p>
|
||||
^{tbl}
|
||||
<p>
|
||||
^{consistencyBtn}
|
||||
<section .profile>
|
||||
<h3>Note how form data is saved
|
||||
<dl .deflist.profile-dl>
|
||||
<dt .deflist__dt>
|
||||
_{MsgTutorialDrivingPermit}, _{MsgTutorialEyeExam}, _{MsgTutorialNote}
|
||||
<dd .deflist__dd>
|
||||
For each course and participant pairing, one value is stored each.
|
||||
<dt .deflist__dt>
|
||||
_{MsgTutorialDayAttendance mempty}, _{MsgTutorialDayNote mempty}
|
||||
<dd .deflist__dd>
|
||||
For each day, course and participant, one value is stored each.
|
||||
<dt .deflist__dt>
|
||||
_{MsgTableUserParkingToken mempty}
|
||||
<dd .deflist__dd>
|
||||
For each day and participant, one value is stored, i.e., indipendant of the course.
|
||||
\ This requires that a change is performed in all rows of the same participant.
|
||||
$nothing
|
||||
<section>
|
||||
No courses are currently scheduled on this day.
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2025 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Gezeigt werden E‑Learning Logins, welche für Qualifikation #{qsh} an FRADrive zurückgemeldet wurden #
|
||||
und von FRADrive nicht mehr zugeordnet werden können. #
|
||||
|
||||
Normalerweise löscht das LMS beendete E‑Learning Logins selbstständig. #
|
||||
In manchen Fällen passiert dies aus unbekanntem Grund jedoch nicht. #
|
||||
Wenn jedoch ein Grund bekannt sein sollte, wie zum Beispiel ein manueller Neustart des E‑Learnings, #
|
||||
wird dieser in Spalte "_{MsgLmsOrphanReason}" angezeigt. #
|
||||
|
||||
<p>
|
||||
Verwaiste Logins werden beim nächsten Abruf der E‑Learning Logins von FRADrive zur Löschung durch das LMS gemeldet. #
|
||||
Die Auswahl, ob ein E‑Learning Login zur Löschung gemeldet wird, hängt von folgenden Kriterien ab: #
|
||||
<ul>
|
||||
<li>"_{MsgLmsOrphanSeenFirst}" liegt mindestens #{lmsOrphanDeletionDays} Tage zurück.
|
||||
<li>"_{MsgLmsOrphanSeenLast}" liegt höchstens #{lmsOrphanRepeatHours} Stunden zurück.
|
||||
<li>"_{MsgLmsOrphanDeletedLast}", d.h. der letzte Löschantrag für diesen Login ist älter als #{lmsOrphanRepeatHours} Stunden #
|
||||
oder wurde noch gar nicht gestellt.
|
||||
<li>Der E‑Learning Login ist auch unter keiner anderen Qualifikation in FRADrive bekannt.
|
||||
<p>
|
||||
Es werden jedoch pro Abruf nur #{lmsOrphanDeletionBatch} E‑Learning Logins zur Löschung an das LMS gemeldet. #
|
||||
Dabei werden Logins bevorzugt welche noch gar nicht oder vor der längsten Zeit gemeldet wurden ("_{MsgLmsOrphanDeletedLast}"), #
|
||||
sollte davon es jeweils mehrere Kandidaten geben, dann werden diejenigen ausgewählt, welche kürzlich zurückgemeldet wurden ("_{MsgLmsOrphanSeenLast}").
|
||||
|
||||
<section>
|
||||
<p>
|
||||
^{orvTable}
|
||||
@ -1,33 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2025 Steffen Jost <s.jost@fraport.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Displayed are e‑learning logins that have been reported back to FRADrive for qualification #{qsh}, #
|
||||
but which are unknown to FRADrive. #
|
||||
|
||||
Normally, the LMS automatically deletes completed e‑learning logins. #
|
||||
In some cases, however, this does not happen for unknown reasons. #
|
||||
If a reason is known, such as a manual restart of the e‑learning, #
|
||||
this is shown in the column "_{MsgLmsOrphanReason}". #
|
||||
|
||||
<p>
|
||||
Orphaned logins will be reported for deletion by FRADrive to the LMS during the next retrieval of e‑learning logins. #
|
||||
The decision whether an e‑learning login is reported for deletion depends on the following criteria: #
|
||||
<ul>
|
||||
<li>"_{MsgLmsOrphanSeenFirst}" is at least #{lmsOrphanDeletionDays} days ago.
|
||||
<li>"_{MsgLmsOrphanSeenLast}" is at most #{lmsOrphanRepeatHours} hours ago.
|
||||
<li>"_{MsgLmsOrphanDeletedLast}", i.e., the last deletion request for this login is older than #{lmsOrphanRepeatHours} hours #
|
||||
or has not been made yet.
|
||||
<li>The e‑learning login is not associated with any other qualification within FRADrive.
|
||||
<p>
|
||||
However, only #{lmsOrphanDeletionBatch} e‑learning logins are reported for deletion to the LMS per request. #
|
||||
Logins that have not yet been reported for deletion at all or were reported the longest time ago ("_{MsgLmsOrphanDeletedLast}") are preferred, #
|
||||
if there are multiple candidates, those that were most recently reported back ("_{MsgLmsOrphanSeenLast}") will be selected.
|
||||
|
||||
<section>
|
||||
<p>
|
||||
^{orvTable}
|
||||
@ -1,46 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2025 Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Benachrichtigungen für ^{usrWgt} ^{widgetMailPrefPin usr} #
|
||||
$if usrReceives
|
||||
gehen #
|
||||
$maybe _ <- mrtbl
|
||||
ebenfalls an die unten aufgeführten Personen:
|
||||
$nothing
|
||||
nur an diese Person selbst.
|
||||
$else
|
||||
$maybe _ <- mrtbl
|
||||
gehen tatsächlich nur an die unten aufgeführten Personen:
|
||||
$nothing
|
||||
werden momentan an niemanden zugestellt!
|
||||
$maybe (tbl, mbUsrCmps) <- mrtbl
|
||||
<p>
|
||||
^{tbl}
|
||||
<p>
|
||||
$maybe usrCmps <- mbUsrCmps
|
||||
<h4>
|
||||
_{MsgCompany} ^{usrWgt}:
|
||||
<ul .list--inline .list--comma-separated>
|
||||
^{usrCmps}
|
||||
$nothing
|
||||
Für ^{usrWgt} ist momentan keine Firmenzugehörigkeit bekannt.
|
||||
<p>
|
||||
<h4>
|
||||
Hinweis:
|
||||
Mit welchem Passwort PDF Anhänge geschützt werden, hängt vom Nachrichtentyp ab. #
|
||||
|
||||
Zum Beispiel werden Pin Briefe für ablaufende Qualifikationen #
|
||||
$if hasPwd
|
||||
mit dem Passwort von ^{usrWgt} geschützt. #
|
||||
$else
|
||||
nicht geschützt, da kein Pin Passwort gesetzt ist. #
|
||||
|
||||
Für andere Benachrichtigungen wird meist das Passwort des tatsächlichen Empfängers gewählt, sofern eins gesetzt wurde.
|
||||
|
||||
Die Voreinstellung für das PDF Passwort ist die Hauptausweisnummer, inklusive Punkt.
|
||||
@ -1,44 +0,0 @@
|
||||
$newline never
|
||||
|
||||
$# SPDX-FileCopyrightText: 2025 Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||
$#
|
||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Notifications for ^{usrWgt} ^{widgetMailPrefPin usr} #
|
||||
$if usrReceives
|
||||
$maybe _ <- mrtbl
|
||||
are also sent additionally to the following persons:
|
||||
$nothing
|
||||
are received only by them.
|
||||
$else
|
||||
$maybe _ <- mrtbl
|
||||
are only sent to the following persons instead:
|
||||
$nothing
|
||||
are currently not delivered to anyone!
|
||||
$maybe (tbl, mbUsrCmps) <- mrtbl
|
||||
<p>
|
||||
^{tbl}
|
||||
<p>
|
||||
$maybe usrCmps <- mbUsrCmps
|
||||
<h4>
|
||||
_{MsgCompany} ^{usrWgt}:
|
||||
<ul .list--inline .list--comma-separated>
|
||||
^{usrCmps}
|
||||
$nothing
|
||||
^{usrWgt} is currently not affiliated with any company.
|
||||
<p>
|
||||
<h4>
|
||||
Note:
|
||||
The password used to protect PDF attachments depends on the message type. #
|
||||
For example, pin letters for expiring qualifications #
|
||||
$if hasPwd
|
||||
are protected by the password of ^{usrWgt}. #
|
||||
$else
|
||||
are not protected, since ^{usrWgt} has no Pin password set. #
|
||||
|
||||
For other notifications, the password of the actual recipient is usually chosen, if a password has been set.
|
||||
|
||||
The default PDF password is their main ID card number, including the period.
|
||||
7
cbt.sh
Executable file
7
cbt.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2022 Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
cbt_tunnels --username $CBT_USERNAME --authkey $CBT_AUTHKEY
|
||||
@ -2,4 +2,4 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
module.exports = {extends: ['@commitlint/config-conventional']};
|
||||
module.exports = {extends: ['@commitlint/config-conventional']}
|
||||
|
||||
89
compose.yaml
89
compose.yaml
@ -1,86 +1,35 @@
|
||||
services:
|
||||
help:
|
||||
image: docker.io/library/perl:stable
|
||||
pull_policy: if_not_present
|
||||
volumes:
|
||||
- ./utils/makehelp.pl:/mnt/utils/makehelp.pl:ro
|
||||
- ./Makefile:/tmp/Makefile:ro
|
||||
command: /mnt/utils/makehelp.pl /tmp/Makefile
|
||||
|
||||
frontend:
|
||||
# image: registry.uniworx.de/fradrive/fradrive/frontend # TODO: reference to current branch required; how to do that here?
|
||||
# pull_policy: if_not_present
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: ./Dockerfile
|
||||
dockerfile: ./docker/frontend/Dockerfile
|
||||
context: .
|
||||
environment:
|
||||
- PROJECT_DIR=/fradrive
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./frontend
|
||||
target: /fradrive
|
||||
- ./assets:/fradrive/assets:rw
|
||||
- ./static:/fradrive/static:rw
|
||||
- ./well-known:/fradrive/well-known:rw
|
||||
|
||||
- &fradrive-mnt .:/tmp/fradrive
|
||||
backend:
|
||||
# image: registry.uniworx.de/fradrive/fradrive/backend
|
||||
# pull_policy: if_not_present
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: ./Dockerfile
|
||||
environment:
|
||||
PATH: /fradrive/bin:$PATH
|
||||
dockerfile: ./docker/backend/Dockerfile
|
||||
context: ./
|
||||
volumes:
|
||||
- ./backend:/fradrive
|
||||
- ./bin:/fradrive/bin
|
||||
- ./assets:/fradrive/assets:ro
|
||||
- ./static:/fradrive/static:ro
|
||||
- ./well-known:/fradrive/well-known:ro
|
||||
- *fradrive-mnt
|
||||
depends_on:
|
||||
- frontend
|
||||
- postgres
|
||||
- memcached
|
||||
- minio
|
||||
- maildev
|
||||
ports:
|
||||
- "3000:3000" # dev http
|
||||
- "3443:3443" # dev https
|
||||
- "8081:8081" # hoogle
|
||||
# links:
|
||||
# - postgres
|
||||
# - memcached
|
||||
# - minio
|
||||
# - maildev
|
||||
stdin_open: true
|
||||
network_mode: host
|
||||
|
||||
postgres:
|
||||
image: docker.io/library/postgres:12
|
||||
pull_policy: if_not_present
|
||||
database:
|
||||
# image: registry.uniworx.de/fradrive/fradrive/database
|
||||
# pull_policy: if_not_present
|
||||
build: ./docker/database
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
volumes:
|
||||
- ./docker/postgres/pg_hba.conf:/tmp/pg_hba.conf:ro
|
||||
- ./docker/postgres/postgresql.conf:/tmp/postgresql.conf:ro
|
||||
- ./docker/postgres/pgconfig.sh:/docker-entrypoint-initdb.d/_pgconfig.sh:ro
|
||||
- ./docker/postgres/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
|
||||
- "9876:5432"
|
||||
# privileged: true
|
||||
|
||||
memcached:
|
||||
image: docker.io/library/memcached:latest
|
||||
pull_policy: if_not_present
|
||||
ports:
|
||||
- "11211:11211"
|
||||
|
||||
minio:
|
||||
image: docker.io/minio/minio:latest
|
||||
pull_policy: if_not_present
|
||||
command: server `mktemp`
|
||||
ports:
|
||||
- "9000:9000"
|
||||
|
||||
maildev:
|
||||
image: docker.io/maildev/maildev:latest
|
||||
pull_policy: if_not_present
|
||||
ports:
|
||||
- "1025-1026:1025"
|
||||
# driver: local
|
||||
# driver_opts:
|
||||
# type: none
|
||||
# o: bind
|
||||
# device: ./
|
||||
@ -8,7 +8,7 @@
|
||||
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
|
||||
|
||||
|
||||
# DEVELOPMENT ONLY, NOT TO BE USED IN PRODUCTION
|
||||
#DEVELOPMENT ONLY, NOT TO BE USED IN PRODUCTION
|
||||
|
||||
avs-licence-synch:
|
||||
times: [12]
|
||||
@ -16,17 +16,6 @@ avs-licence-synch:
|
||||
reason-filter: "(firm|block)"
|
||||
max-changes: 999
|
||||
|
||||
mail-reroute-to:
|
||||
name: "FRADrive-QA-Umleitungen"
|
||||
email: "FRADrive-TEST-Umleitungen@fraport.de"
|
||||
|
||||
# Enqueue at specified hour, a few minutes later
|
||||
job-lms-qualifications-enqueue-hour: 16
|
||||
job-lms-qualifications-dequeue-hour: 4
|
||||
|
||||
# Using these setting kills the job-workers somehow
|
||||
# job-workers: 5
|
||||
# job-flush-interval: 600
|
||||
# job-stale-threshold: 3600
|
||||
# job-move-threshold: 60
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@ -120,7 +120,7 @@ auth-pw-hash:
|
||||
database:
|
||||
user: "_env:PGUSER:uniworx"
|
||||
password: "_env:PGPASS:uniworx"
|
||||
host: "_env:PGHOST:localhost"
|
||||
host: "_env:PGHOST:host.docker.internal"
|
||||
port: "_env:PGPORT:5432"
|
||||
# See config/test-settings.yml for an override during tests
|
||||
database: "_env:PGDATABASE:uniworx"
|
||||
@ -146,14 +146,12 @@ ldap:
|
||||
ldap-re-test-failover: 60
|
||||
|
||||
lms-direct:
|
||||
upload-header: "_env:LMSUPLOADHEADER:true"
|
||||
upload-delimiter: "_env:LMSUPLOADDELIMITER:"
|
||||
download-header: "_env:LMSDOWNLOADHEADER:true"
|
||||
download-delimiter: "_env:LMSDOWNLOADDELIMITER:,"
|
||||
download-cr-lf: "_env:LMSDOWNLOADCRLF:true"
|
||||
orphan-deletion-days: "_env:LMSORPHANDELETIONDAYS:33"
|
||||
orphan-deletion-batch: "_env:LMSORPHANDELETIONBATCH:12"
|
||||
orphan-deletion-repeat-hours: "_env:LMSORPHANDELETIONREPEATHOURS:24"
|
||||
upload-header: "_env:LMSUPLOADHEADER:true"
|
||||
upload-delimiter: "_env:LMSUPLOADDELIMITER:"
|
||||
download-header: "_env:LMSDOWNLOADHEADER:true"
|
||||
download-delimiter: "_env:LMSDOWNLOADDELIMITER:,"
|
||||
download-cr-lf: "_env:LMSDOWNLOADCRLF:true"
|
||||
deletion-days: "_env:LMSDELETIONDAYS:7"
|
||||
|
||||
avs:
|
||||
host: "_env:AVSHOST:skytest.fra.fraport.de"
|
||||
@ -182,7 +180,7 @@ smtp:
|
||||
limit: "_env:SMTPLIMIT:10"
|
||||
|
||||
widget-memcached:
|
||||
host: "_env:WIDGET_MEMCACHED_HOST:localhost"
|
||||
host: "_env:WIDGET_MEMCACHED_HOST:host.docker.internal"
|
||||
port: "_env:WIDGET_MEMCACHED_PORT:11211"
|
||||
auth: []
|
||||
limit: "_env:WIDGET_MEMCACHED_LIMIT:1024"
|
||||
@ -191,7 +189,7 @@ widget-memcached:
|
||||
expiration: "_env:WIDGET_MEMCACHED_EXPIRATION:3600"
|
||||
|
||||
session-memcached:
|
||||
host: "_env:SESSION_MEMCACHED_HOST:localhost"
|
||||
host: "_env:SESSION_MEMCACHED_HOST:host.docker.internal"
|
||||
port: "_env:SESSION_MEMCACHED_PORT:11211"
|
||||
auth: []
|
||||
limit: "_env:SESSION_MEMCACHED_LIMIT:1024"
|
||||
@ -201,7 +199,7 @@ session-memcached:
|
||||
|
||||
# TODO: this one seems to be required!
|
||||
memcached:
|
||||
host: "_env:MEMCACHED_HOST:localhost"
|
||||
host: "_env:MEMCACHED_HOST:host.docker.internal"
|
||||
port: "_env:MEMCACHED_PORT:11211"
|
||||
auth: []
|
||||
limit: "_env:MEMCACHED_LIMIT:1024"
|
||||
@ -16,4 +16,5 @@ log-settings:
|
||||
auth-dummy-login: true
|
||||
server-session-acid-fallback: true
|
||||
|
||||
job-workers: 20
|
||||
job-cron-interval: null
|
||||
job-workers: 1
|
||||
32
docker/backend/Dockerfile
Normal file
32
docker/backend/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
FROM debian:12.5
|
||||
|
||||
RUN apt-get update && apt-get -y install inotify-tools
|
||||
RUN apt-get -y update && apt-get -y install haskell-stack git
|
||||
RUN apt-get -y update && apt-get -y install alex g++ happy libghc-zlib-dev libpq-dev libsodium-dev locales locales-all pkg-config
|
||||
RUN apt-get -y update && apt-get -y install llvm
|
||||
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
# locally these two should be identical, so that compilation results are written out into the file dir.
|
||||
# in CI-pipelines these two should be different, so that the container caches the compilation results.
|
||||
ARG MOUNT_DIR=/mnt/fradrive
|
||||
ARG PROJECT_DIR=/fradrive
|
||||
|
||||
RUN mkdir -p "${PROJECT_DIR}"
|
||||
RUN if [ "${PROJECT_DIR}" != "${MOUNT_DIR}" ] ; then cp -r "${MOUNT_DIR}"/* "${PROJECT_DIR}" ; fi
|
||||
|
||||
RUN mkdir -p "${PROJECT_DIR}/.stack"
|
||||
ENV STACK_ROOT="${PROJECT_DIR}/.stack"
|
||||
|
||||
WORKDIR "${PROJECT_DIR}/.job-${JOB}"
|
||||
ENV HOME="${PROJECT_DIR}/.job-${JOB}"
|
||||
RUN make -- --dependencies-backend STACK_ROOT=${STACK_ROOT} IN_CONTAINER=true JOB=${JOB}
|
||||
|
||||
RUN cd "${PROJECT_DIR}" && make .job-start
|
||||
RUN cd "${PROJECT_DIR}/.job-start" && STACK_ROOT=${STACK_ROOT} stack install yesod-bin
|
||||
|
||||
ENV FRADRIVE_MAKE_TARGET=--start-backend
|
||||
ENTRYPOINT make -- ${FRADRIVE_MAKE_TARGET} STACK_ROOT="${STACK_ROOT}" IN_CONTAINER=true CONTAINER_FILE="${CONTAINER_FILE}" JOB="${JOB}"
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
EXPOSE 3443/tcp
|
||||
42
docker/fradrive/Dockerfile
Executable file → Normal file
42
docker/fradrive/Dockerfile
Executable file → Normal file
@ -1,29 +1,16 @@
|
||||
ARG FROM_IMG=docker.io/library/debian
|
||||
ARG FROM_TAG=12.5
|
||||
FROM debian:12.5
|
||||
|
||||
FROM ${FROM_IMG}:${FROM_TAG}
|
||||
RUN apt-get -y update
|
||||
|
||||
# Setup locales
|
||||
# RUN apt-get update && apt-get -y install locales locales-all
|
||||
# RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
||||
# locale-gen
|
||||
# ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
|
||||
# setup locales
|
||||
RUN apt-get -y install locales locales-all
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
||||
locale-gen
|
||||
ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
|
||||
|
||||
# FraDrive runtime dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Etc/UTC
|
||||
RUN apt-get update && apt-get -y install libpq-dev
|
||||
RUN apt-get update && apt-get -y install libsodium-dev
|
||||
RUN apt-get update && apt-get -y install fonts-roboto
|
||||
# Binary runtime dependencies
|
||||
# TODO: minimize texlive dependencies, switch to basic schemes where possible
|
||||
RUN apt-get update && apt-get -y install \
|
||||
texlive-full \
|
||||
texlive-luatex \
|
||||
texlive-plain-generic \
|
||||
texlive-fonts-recommended \
|
||||
texlive-fonts-extra \
|
||||
texlive-lang-english \
|
||||
texlive-lang-german
|
||||
RUN apt-get -y install texlive-latex-recommended texlive-latex-extra texlive-luatex texlive-fonts-recommended texlive-fonts-extra texlive-lang-english texlive-lang-german
|
||||
|
||||
# Add uniworx user and directories
|
||||
RUN mkdir -p /var/lib
|
||||
@ -33,17 +20,12 @@ RUN useradd -r -g uniworx -d /var/lib/uniworx -M uniworx --uid 999
|
||||
RUN mkdir -p /var/lib/uniworx && chown -R uniworx:uniworx /var/lib/uniworx
|
||||
RUN mkdir -p /var/log/uniworx && chown -R uniworx:uniworx /var/log/uniworx
|
||||
|
||||
# Install FraDrive binaries
|
||||
# TODO: is this still needed?
|
||||
# RUN install -d -g uniworx -o uniworx -m 0750 /var/lib/uniworx
|
||||
# RUN install -d -g uniworx -o uniworx -m 0755 /var/log/uniworx
|
||||
COPY ./bin/uniworx /usr/bin/uniworx
|
||||
COPY ./bin/uniworxdb /usr/bin/uniworxdb
|
||||
# COPY uniworxload /usr/bin/uniworx
|
||||
RUN chmod -R 777 /usr/bin
|
||||
COPY ./docker/fradrive/fradrive-entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 777 /entrypoint.sh
|
||||
RUN cp /tmp/uniworx-bin/uniworx /usr/bin/uniworx
|
||||
|
||||
USER uniworx
|
||||
ENTRYPOINT /entrypoint.sh
|
||||
ENTRYPOINT fradrive-entrypoint.sh
|
||||
EXPOSE 8080/tcp
|
||||
VOLUME /var/lib/uniworx /var/log
|
||||
@ -4,19 +4,16 @@ cTime=$(date -Is)
|
||||
|
||||
# export LOGDEST=/var/log/uniworx/${cTime}.log # kubernetes prefers log via stdout
|
||||
|
||||
# typeset -a configs
|
||||
typeset -a configs
|
||||
|
||||
configDir=${CONFIG_DIR-/cfg}
|
||||
# configs=()
|
||||
# if [[ -d "${configDir}" ]]; then
|
||||
# while IFS= read -d $'\0' cfg; do
|
||||
# # IMPORTANT: The paths to the settings-yaml-files should not contain spaces, otherwise this might fail runtime!
|
||||
# configs+=("${cfg}")
|
||||
# done < <(find "${configDir}" \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -rz)
|
||||
# fi
|
||||
configs=()
|
||||
if [[ -d "${configDir}" ]]; then
|
||||
while IFS= read -d $'\0' cfg; do
|
||||
configs+=("${(q)cfg}")
|
||||
done < <(find "${configDir}" \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -rz)
|
||||
fi
|
||||
|
||||
cd /var/lib/uniworx
|
||||
|
||||
# exec -- uniworx ${configs}
|
||||
# find "${configDir}" \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -rz | xargs -0 exec -- uniworx
|
||||
find "${configDir}" \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -rz | xargs -0 uniworx
|
||||
exec -- uniworx ${configs}
|
||||
32
docker/frontend/Dockerfile
Normal file
32
docker/frontend/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
FROM debian:12.5
|
||||
|
||||
# Basic dependencies
|
||||
RUN apt-get -y update && apt-get -y install curl npm
|
||||
|
||||
# Build and watch dependencies
|
||||
RUN apt-get -y update && apt-get -y install exiftool
|
||||
RUN apt-get -y update && apt-get -y install imagemagick
|
||||
|
||||
# Test dependencies
|
||||
RUN apt-get -y update && apt-get -y install chromium
|
||||
ENV CHROME_BIN=chromium
|
||||
|
||||
# TODO: use dotenv for npm version?
|
||||
RUN npm install -g n
|
||||
RUN n 20.17.0
|
||||
|
||||
# locally these two should be identical, so that compilation results are written out into the file dir.
|
||||
# in CI-pipelines these two should be different, so that the container caches the compilation results.
|
||||
ARG MOUNT_DIR=/mnt/fradrive
|
||||
ARG PROJECT_DIR=/fradrive
|
||||
RUN mkdir -p ${PROJECT_DIR}
|
||||
RUN if [ "${PROJECT_DIR}" != "${MOUNT_DIR}" ] ; then cp -r ${MOUNT_DIR}/* ${PROJECT_DIR} ; fi
|
||||
WORKDIR ${PROJECT_DIR}
|
||||
ENV HOME=${PROJECT_DIR}
|
||||
|
||||
#RUN make node_modules IN_CONTAINER=true
|
||||
#RUN make well-known IN_CONTAINER=true
|
||||
RUN make -- static
|
||||
|
||||
ENV FRADRIVE_MAKE_TARGET=start-frontend
|
||||
ENTRYPOINT make -- ${FRADRIVE_MAKE_TARGET} IN_CONTAINER=true CHROME_BIN=${CHROME_BIN}
|
||||
33
docker/podman/Dockerfile
Normal file
33
docker/podman/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# Debian-based podman daemon image for building docker images
|
||||
# inside docker containers (e.g. gitlab runners).
|
||||
#
|
||||
# Yoinked with love from:
|
||||
# https://www.redhat.com/sysadmin/podman-inside-container
|
||||
|
||||
FROM debian:12.5
|
||||
|
||||
RUN apt-get -y update
|
||||
|
||||
RUN apt-get -y install make podman podman-compose fuse-overlayfs
|
||||
|
||||
RUN useradd podman; \
|
||||
echo podman:10000:5000 > /etc/subuid; \
|
||||
echo podman:10000:5000 > /etc/subgid;
|
||||
|
||||
VOLUME /var/lib/containers
|
||||
VOLUME /home/podman/.local/share/containers
|
||||
|
||||
ADD https://raw.githubusercontent.com/containers/image_build/main/podman/containers.conf /etc/containers/containers.conf
|
||||
ADD https://raw.githubusercontent.com/containers/image_build/main/podman/podman-containers.conf /home/podman/.config/containers/containers.conf
|
||||
|
||||
RUN chown podman:podman -R /home/podman
|
||||
|
||||
# chmod containers.conf and adjust storage.conf to enable Fuse storage.
|
||||
# RUN chmod 644 /etc/containers/containers.conf; sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' /etc/containers/containers.conf
|
||||
# RUN echo -e '[storage]\ndriver="zfs"\nmount_program="zfs"\nadditionalimage=/var/lib/shared\nmountopt="nodev,fsync=0"' >> /etc/containers/containers.conf
|
||||
RUN chmod 644 /etc/containers/containers.conf
|
||||
RUN echo '[storage]\ndriver="overlay"\n[storage.options.overlay]\nforce_mask="private"\nmount_program="/usr/bin/fuse-overlayfs"\nmountopt="nodev"' >> /etc/containers/containers.conf
|
||||
RUN mkdir -p /root/.config/containers/ && echo '[storage]\ndriver="overlay"\n[storage.options.overlay]\nforce_mask="private"\nmount_program="/usr/bin/fuse-overlayfs"\nmountopt="nodev"' > /root/.config/containers/storage.conf
|
||||
RUN mkdir -p /var/lib/shared/overlay-images /var/lib/shared/overlay-layers /var/lib/shared/vfs-images /var/lib/shared/vfs-layers; touch /var/lib/shared/overlay-images/images.lock; touch /var/lib/shared/overlay-layers/layers.lock; touch /var/lib/shared/vfs-images/images.lock; touch /var/lib/shared/vfs-layers/layers.lock
|
||||
|
||||
ENV _CONTAINERS_USERNS_CONFIGURED=""
|
||||
9
docker/postgres/Dockerfile
Normal file
9
docker/postgres/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM docker.io/postgres:12
|
||||
|
||||
# Allow for connecting to database without password authentication
|
||||
ENV POSTGRES_HOST_AUTH_METHOD=trust
|
||||
|
||||
COPY --chown=postgres:postgres docker/postgres/pg_hba.conf /tmp/pg_hba.conf
|
||||
COPY --chown=postgres:postgres docker/postgres/postgresql.conf /tmp/postgresql.conf
|
||||
COPY docker/postgres/pgconfig.sh /docker-entrypoint-initdb.d/_pgconfig.sh
|
||||
COPY --chown=postgres:postgres docker/postgres/schema.sql /docker-entrypoint-initdb.d/schema.sql
|
||||
14
docker/postgres/initdb.sh
Normal file
14
docker/postgres/initdb.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Init and start the postgres daemon
|
||||
initdb --no-locale
|
||||
pg_ctl start -w -o "-c listen_addresses='*' -c unix_socket_permissions=0700 -c max_connections=9990 -c shared_preload_libraries=pg_stat_statements -c session_preload_libraries=auto_explain -c auto_explain.log_min_duration=100ms"
|
||||
POSTGRID=`cat /var/lib/postgresql/data/postmaster.pid | perl -le '<>=~m#(\d+)# and print $1'`
|
||||
|
||||
# Create uniworx and uniworx_test database
|
||||
psql -f /schema.sql postgres
|
||||
|
||||
# Wait for postgres daemon to terminate
|
||||
while [ -e /proc/$POSTGRID ]; do
|
||||
sleep 0.5;
|
||||
done
|
||||
2
docker/postgres/pg_hba.conf
Normal file
2
docker/postgres/pg_hba.conf
Normal file
@ -0,0 +1,2 @@
|
||||
local all all trust
|
||||
host all all 0.0.0.0/0 trust
|
||||
6
docker/postgres/pgconfig.sh
Executable file
6
docker/postgres/pgconfig.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cat /tmp/pg_hba.conf > /var/lib/postgresql/data/pg_hba.conf
|
||||
cat /tmp/postgresql.conf > /var/lib/postgresql/data/postgresql.conf
|
||||
|
||||
echo "Custom pg_hba.conf and postgresql.conf successfully deployed."
|
||||
6
docker/postgres/postgresql.conf
Normal file
6
docker/postgres/postgresql.conf
Normal file
@ -0,0 +1,6 @@
|
||||
listen_addresses='*'
|
||||
unix_socket_permissions=0700
|
||||
max_connections=9990
|
||||
shared_preload_libraries=pg_stat_statements
|
||||
session_preload_libraries=auto_explain
|
||||
auto_explain.log_min_duration=100ms
|
||||
5
docker/postgres/schema.sql
Normal file
5
docker/postgres/schema.sql
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE USER uniworx WITH SUPERUSER PASSWORD 'uniworx';
|
||||
CREATE DATABASE uniworx_test;
|
||||
GRANT ALL ON DATABASE uniworx_test TO uniworx;
|
||||
CREATE DATABASE uniworx;
|
||||
GRANT ALL ON DATABASE uniworx TO uniworx;
|
||||
@ -7,8 +7,8 @@ import svgPlugin from 'esbuild-plugin-svg-bundle';
|
||||
import { copy } from 'esbuild-plugin-copy';
|
||||
// import manifestPlugin from 'esbuild-plugin-manifest';
|
||||
import manifestPlugin from 'esbuild-plugin-assets-manifest';
|
||||
// import copyWithHashPlugin from '@enonic/esbuild-plugin-copy-with-hash';
|
||||
// import inlineImportPlugin from 'esbuild-plugin-inline-import';
|
||||
import copyWithHashPlugin from '@enonic/esbuild-plugin-copy-with-hash';
|
||||
import inlineImportPlugin from 'esbuild-plugin-inline-import';
|
||||
import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill';
|
||||
|
||||
const staticDir = './static';
|
||||
@ -20,8 +20,8 @@ await esbuild.build({
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
main: './src/main.js',
|
||||
polyfill: './src/polyfill.js',
|
||||
main: './frontend/src/main.js',
|
||||
polyfill: './frontend/src/polyfill.js',
|
||||
},
|
||||
outdir: staticDir,
|
||||
plugins: [
|
||||
@ -48,20 +48,19 @@ await esbuild.build({
|
||||
copy({
|
||||
resolveFrom: 'cwd',
|
||||
assets: {
|
||||
from: [ './robots.txt' ],
|
||||
from: [ './config/robots.txt' ],
|
||||
to: wellKnownDirs,
|
||||
},
|
||||
}),
|
||||
// ...['de-de-formal','en-eu'].map((lang) => manifestPlugin({
|
||||
manifestPlugin({
|
||||
filename: 'manifest.json',
|
||||
path: '.',
|
||||
path: 'config',
|
||||
// metadata: { timestamp: new Date(), module: 'myapp', type: 'esm', },
|
||||
processOutput(assets) {
|
||||
const orderAssets = {
|
||||
main: assets['main'],
|
||||
polyfill: assets['polyfill'],
|
||||
icons: { "svg": assets['']['svg'][0] },
|
||||
...assets
|
||||
};
|
||||
return JSON.stringify(orderAssets, null, ' ');
|
||||
},
|
||||
@ -5,7 +5,7 @@ import babelParser from "@babel/eslint-parser";
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["frontend/src/**/*.js"],
|
||||
files: ["**/*.js"],
|
||||
plugins: {},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2018,
|
||||
6
fixtest.sh
Executable file
6
fixtest.sh
Executable file
@ -0,0 +1,6 @@
|
||||
if [[ ! -d .stack-work-test ]]; then
|
||||
mv -vT .stack-work .stack-work-test
|
||||
[[ -d .stack-work-build ]] && mv -vT .stack-work-build .stack-work
|
||||
else
|
||||
echo "Directory .stack-work-test exists already."
|
||||
fi
|
||||
@ -1,29 +0,0 @@
|
||||
ARG FROM_IMG=docker.io/library/node
|
||||
ARG FROM_TAG=20
|
||||
|
||||
FROM ${FROM_IMG}:${FROM_TAG}
|
||||
|
||||
ENV LANG=de_DE.UTF-8
|
||||
ENV LANGUAGE=de_DE.UTF-8
|
||||
|
||||
# build and watch dependencies
|
||||
RUN apt-get -y update && apt-get -y install exiftool
|
||||
RUN apt-get -y update && apt-get -y install imagemagick
|
||||
|
||||
# test dependencies
|
||||
# RUN apt-get -y update && apt-get -y install chromium
|
||||
# ENV CHROME_BIN=chromium
|
||||
|
||||
# configure npm to use given proxy if specified
|
||||
RUN if [ ! -z "${HTTP_PROXY}" ]; then npm config set proxy ${HTTP_PROXY}; fi
|
||||
RUN if [ ! -z "${FRAPORT_NOPROXY}" ]; then npm config set noproxy "${FRAPORT_NOPROXY}"; fi
|
||||
ENV NODE_EXTRA_CA_CERTS="/etc/ssl/certs/ca-certificates.crt"
|
||||
|
||||
ENV PROJECT_DIR=/fradrive
|
||||
RUN mkdir -p ${PROJECT_DIR}
|
||||
WORKDIR ${PROJECT_DIR}
|
||||
ENV HOME=${PROJECT_DIR}
|
||||
|
||||
RUN if [ ! -z "${NPM_CUSTOM_REGISTRY}" ]; then \
|
||||
printf 'registry=${NPM_CUSTOM_REGISTRY}' > .npmrc \
|
||||
; fi
|
||||
@ -1,23 +0,0 @@
|
||||
.PHONY: all
|
||||
all: dependencies compile ;
|
||||
.PHONY: dependencies
|
||||
dependencies: node_modules assets ;
|
||||
.PHONY: compile
|
||||
compile: static well-known ;
|
||||
|
||||
node_modules: package.json package-lock.json
|
||||
npm install --cache .npm --prefer-offline
|
||||
package-lock.json: package.json
|
||||
npm install --cache .npm --prefer-offline
|
||||
static: node_modules assets esbuild.config.mjs jsconfig.json postcss.config.js
|
||||
echo "$${PROJECT_DIR}"
|
||||
npm run build
|
||||
well-known: static ;
|
||||
|
||||
assets: assets/favicons assets/icons;
|
||||
assets/favicons:
|
||||
./utils/faviconize.pl assets/favicon.svg long assets/favicons
|
||||
assets/icons: node_modules assets/icons-src/fontawesome.json
|
||||
./utils/renamer.pl node_modules/@fortawesome/fontawesome-free/svgs/solid assets/icons-src/fontawesome.json assets/icons/fradrive
|
||||
./utils/renamer.pl node_modules/@fortawesome/fontawesome-free/svgs/regular assets/icons-src/fontawesome.json assets/icons/fradrive
|
||||
-cp assets/icons-src/*.svg assets/icons/fradrive
|
||||
19093
frontend/package-lock.json
generated
19093
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2024-2025 David Mosbach <david.mosbach@uniworx.de>, Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
// SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>, Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: LicenseRef-Fraport-Corporate-Design
|
||||
|
||||
// @import 'env';
|
||||
|
||||
$ico-width: 15px;
|
||||
$ico-width: 30px;
|
||||
|
||||
$icons: new,
|
||||
ok,
|
||||
@ -35,7 +34,6 @@ $icons: new,
|
||||
file-upload,
|
||||
file-zip,
|
||||
file-csv,
|
||||
file-missing,
|
||||
sft-question,
|
||||
sft-hint,
|
||||
sft-solution,
|
||||
@ -96,21 +94,15 @@ $icons: new,
|
||||
trash,
|
||||
reset-tries,
|
||||
company,
|
||||
company-warning,
|
||||
edit,
|
||||
user-edit,
|
||||
placeholder,
|
||||
glasses,
|
||||
user-badge,
|
||||
user-unknown,
|
||||
missing,
|
||||
pin-protect,
|
||||
loading;
|
||||
|
||||
|
||||
@each $name in $icons {
|
||||
.ico-#{$name} {
|
||||
background-image: url('/fradrive/assets/icons/fradrive/#{$name}.svg');
|
||||
background-image: url('/mnt/fradrive/assets/icons/fradrive/#{$name}.svg');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
@ -140,7 +132,6 @@ $icons: new,
|
||||
|
||||
.large-ico {
|
||||
font-size: 2em;
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
.ico-spin {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Johannes Eder <ederj@cip.ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
||||
// SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Johannes Eder <ederj@cip.ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@ -499,14 +499,14 @@ function encrypt(plaintext, key) {
|
||||
if (!key) throw new Error('Cannot encrypt plaintext without a valid key!');
|
||||
|
||||
// TODO use const if possible
|
||||
// let plaintextB = Buffer.from(plaintext);
|
||||
// let cipherB = Buffer.alloc(plaintextB.length + sodium.crypto_secretbox_MACBYTES);
|
||||
// let nonceB = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
|
||||
// let keyB = Buffer.from(key);
|
||||
let plaintextB = Buffer.from(plaintext);
|
||||
let cipherB = Buffer.alloc(plaintextB.length + sodium.crypto_secretbox_MACBYTES);
|
||||
let nonceB = undefined; // Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
|
||||
let keyB = Buffer.from(key);
|
||||
|
||||
// sodium.crypto_secretbox_easy(cipherB, plaintextB, nonceB, keyB);
|
||||
|
||||
const result = null; // cipherB;
|
||||
const result = cipherB;
|
||||
console.log('encrypt result', result);
|
||||
return result;
|
||||
}
|
||||
@ -519,10 +519,10 @@ function decrypt(ciphertext, key) {
|
||||
if (!key) throw new Error('Cannot decrypt ciphertext without a valid key!');
|
||||
|
||||
// TODO use const if possible
|
||||
// let cipherB = Buffer.from(ciphertext);
|
||||
let plaintextB = null; // Buffer.alloc(cipherB.length - sodium.crypto_secretbox_MACBYTES);
|
||||
// let nonceB = undefined; Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
|
||||
// let keyB = Buffer.from(key);
|
||||
let cipherB = Buffer.from(ciphertext);
|
||||
let plaintextB = undefined; Buffer.alloc(cipherB.length - sodium.crypto_secretbox_MACBYTES);
|
||||
let nonceB = undefined; Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
|
||||
let keyB = Buffer.from(key);
|
||||
|
||||
// sodium.crypto_secretbox_open_easy(plaintextB, cipherB, nonceB, keyB);
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
|
||||
/* eslint-disable */
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
//root path location to resolve paths defined in files and exclude
|
||||
@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||
# SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@ -109,10 +109,9 @@ ProblemsDriverSynch1up: Alle gültigen Vorfeld-Fahrberechtigungen 'F' sind im AV
|
||||
ProblemsDriverSynch2: Alle gültigen Rollfeld-Fahrberechtigungen 'R' sind im AVS eingetragen
|
||||
ProblemsRDriversHaveFs: Alle Inhaber einer Rollfeld-Fahrberechtigung besitzen auch eine gültige Vorfeld-Fahrberechtigung
|
||||
ProblemsDriversHaveAvsIds: Alle Inhaber einer Fahrberechtigung konnten einer AVS Identifikationsnummer zugeordnet werden
|
||||
ProblemsUsersAreReachable: Für alle Benutzer ist eine E-Mail oder postalische Adresse bekannt
|
||||
ProblemsUsersAreReachable: Für alle Benutzer ist eine E-Mail oder postalische Adresse bekannt
|
||||
ProblemsNoStalePrintJobs n@Integer: Alle Briefversandaufträge #{pluralDE n "des vergangenen Tages" ("der vergangenen "<> tshow n <> " Tage")} wurden von der Druckerei bestätigt
|
||||
ProblemsNoBadAPCIds: Alle kürzlich empfangenen Druckauftragsbestätigungen waren gültig
|
||||
ProblemsNoInsaneCompanySupervisions: Sind alle Firmen-bezogenen Ansprechpartnerbeziehungen zwischen passenden Firmenangehörigen?
|
||||
ProblemsUnreachableHeading: Unerreichbare Benutzer
|
||||
ProblemsUnreachableBody: Benutzer ohne E-Mail oder Postadresse, welche z.B. bei ablaufenden Berechtigungen nicht benachrichtigt werden können:
|
||||
ProblemsUnreachableButtons: Synchronisation für Unerreichbare starten
|
||||
@ -124,7 +123,6 @@ ProblemsAvsSynchHeading: Synchronisation AVS Fahrberechtigungen
|
||||
ProblemsAvsErrorHeading: Fehlermeldungen
|
||||
ProblemsInterfaceSince: Berücksichtigt werden nur Erfolge und Fehler seit
|
||||
ProblemAvsUsrHadR: Momentan gültiges R im AVS
|
||||
ProblemLastCheckTime t@Text: Letzte Prüfung vor #{t}
|
||||
|
||||
AdminProblemSolved: Erledigt
|
||||
AdminProblemSolver: Bearbeitet von
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user