Compare commits

..

62 Commits

Author SHA1 Message Date
2d045739be Merge 145-build-system-rewrite 2024-11-11 17:33:35 +01:00
7a5c11e457 Merge 145-build-system-rewrite 2024-11-11 17:29:15 +01:00
507c383410 Merge remote-tracking branch 'origin/145-build-system-rewrite' into fradrive/merge-jost-new-build 2024-11-11 16:47:35 +01:00
aebf6ec914 Merge remote-tracking branch 'origin/145-build-system-rewrite' into fradrive/merge-jost-new-build 2024-11-11 14:34:43 +01:00
bccdb2494f fix(msg): minor uni2wok to fradrive message change
Primarly done to the the new build environment start-backend file watcher and see whether a restart enables the website
2024-11-11 13:26:57 +01:00
36b4beb908 Merge remote-tracking branch 'origin/145-build-system-rewrite' into fradrive/merge-jost-new-build 2024-11-11 12:31:38 +01:00
228a6254bc Merge remote-tracking branch 'origin/144-build-system-rewrite' into fradrive/merge-jost-new-build 2024-11-11 11:43:18 +01:00
7294b9731c fix(doc): Update outdated GitLab references to DevOps work items 2024-11-05 11:08:44 +01:00
97a3845f6d chore(firm): add action to add non-avs firm associates 2024-11-04 18:20:43 +01:00
0adc12c828 fix(doc): minor haddock fixes 2024-10-30 17:18:11 +01:00
28c3ee5be1 chore(day): complete form columns for daily view (untested)
unfortunately `make start` does not enter DEVELOPMENT mode currently, so this is not yet testeted.
2024-10-29 18:16:29 +01:00
8c91d6d37a chore(day): make form columns compile eventually 2024-10-29 13:38:23 +01:00
0d6346ef2c chore(icons): add instructions on how to add icons 2024-10-28 17:48:41 +01:00
fddbf8a30b fix(icons): add missing icons 2024-10-28 17:44:17 +01:00
3d63c88c75 chore(day): add missing form columns 2024-10-28 16:11:45 +01:00
c92ddb9081 Merge remote-tracking branch 'origin/fradrive/jost'into 'fradrive/merge-jost-new-build' 2024-10-28 12:05:37 +01:00
1b71137295 chore(tutorial): (WIP) towards #90 write form columns 2024-10-23 16:12:18 +02:00
6fcfe56626 fix(test): fix test problem and add tests for UserEyeExam and UserDrivingPermit 2024-10-23 15:47:20 +02:00
030ddcac66 fix(build) 2024-10-22 14:39:58 +02:00
36a0bd9edc chore(tutorial): show additional columns for #90
columns are distinguished by user and the entities given in parenthesis:
- driving permit (tutorial)
- eye exam (tutrial)
- tutorial note (tutorial)
- attendance (tutorial & day)
- attendance-note (tutorial & day)
- parking permit (day)
2024-10-22 12:39:34 +02:00
06fa34c938 chore(tutorial): build model for #90 2024-10-21 15:59:32 +02:00
d4d511a02f fix(room): deduplicate room column and fix order 2024-10-17 16:48:09 +02:00
ec2b09b20b chore(daily): show rooms for tutorial lessons 2024-10-15 17:48:36 +02:00
7d57a30be7 refactor(TH): minor code clean up 2024-10-15 11:03:01 +02:00
01c4225da4 refactor(TH): add sqlMIXproj' using reify on TableExpr for more comfort 2024-10-14 19:16:36 +02:00
4fc6f54b32 chore(TH): add sqlMIXproj to improve dbTable usage, also add card-nos to DayTask Table 2024-10-14 18:27:44 +02:00
8506c4d7e0 refactor(memcached): checking memcached key security mechanisms
RESULTS:

Keys for memcached use their Binary representation!

This means that the following three are all interchangeable as a key:
      newtype Foo1 = Foo1 { someInt1 :: Int } deriving newtype (Binary)
      data    Foo2 = Foo2 { someInt2 :: Int } deriving         (Binary)
      type    Foo3 = Int
Therefore it is best to use $(memcachedHere) or $(memcachedByHere) if possible or add another type
2024-10-11 11:23:29 +02:00
ed44edc199 chore(daily): show course associated qualifications 2024-10-09 18:11:22 +02:00
ab46577b7e fix(avs): fix #225 by skipping firm updates entirely if AVS FirmInfo is unchanged for previously seen values for AVS User to be updated 2024-10-09 12:21:31 +02:00
be7fc2e540 fix(avs): avs firm update no longer may update wrong company
Note: noticed while working on #225
2024-10-09 11:47:46 +02:00
3960931bb5 fix(avs): fix #224 repeated superior changes no longer occur
furthermore AdminProblems are only inserted if the same problem does not exist unsolved
2024-10-08 17:47:46 +02:00
56c2be7b79 refactor(occurrences): fold RoomReference into Occurrences, completed 2024-10-08 13:01:44 +02:00
4e171a7a1a fix(memcached): using memcachedHere did not compile due to staging problems 2024-10-08 10:08:04 +02:00
f642b9cccf fix(occurrences): room occurrence form works now 2024-10-07 18:31:02 +02:00
72b2b6876b fix(test): add arbitrart instances and adjust argument changes to tests 2024-10-07 12:58:22 +02:00
c9ecb30542 fix(build): occurrences no longer have a READ instance 2024-10-04 16:16:32 +02:00
8ddf38b904 chore(build): limit max compile cpu cores to 5 2024-10-04 16:13:40 +02:00
21592347b4 chore(occurrences): workaround provide simple room field with least recent suggestions 2024-10-04 16:13:01 +02:00
e625dca6ea refactor(memcached): remove ARC cache and LRU logic some more
more leftover dead code was removed, especially cache prewarm options that no longer had an effect on a non-existing ARC cache
2024-10-04 12:19:27 +02:00
f17d89c21e chore(occurrences): add GIN index for JSONB columns 2024-10-02 15:52:08 +02:00
5c7b4cff93 refactor(occurrences): fix migration 2024-09-30 16:05:33 +02:00
83fe750b15 refactor(occurrences): remove RoomReference from model and add migration 2024-09-30 13:56:45 +02:00
e29e6f3db8 refactor(occurrences): fold RoomReference into Occurrences (WIP)
Each Occurrence now has its own RoomReference, i.e. Mondays may have a different Room assigned than Tuesdays

WIP Problem: occurrencesAFrom does not work, always insists on Room missing
2024-09-24 17:15:15 +02:00
6dd27eb848 fix(build): minor 2024-09-24 13:10:14 +02:00
4c2baa4e9f fix(occurrences): occurringLessons had an erroneously inverted condition 2024-09-24 13:05:16 +02:00
384c39b9ec chore(occurrences): add datatype LessonTime for dealing timetable intervals 2024-09-24 11:21:33 +02:00
a262921a7d refactor(memcached): remove ARC cache entirely
NOTE: this was a crude surgery, removing everything ARC related; some dead code artifacts may have remained.

Especially check PrewarmCacheConf

Reason for removall: adding `memcachedInvalidateClass` was difficult to implement with ARC active; ARC was known to be problematic; removal was easier (see #2 2024-09-23)
2024-09-23 18:52:26 +02:00
05638c2b51 chore(memcached): add key classes for easy invalidation 2024-09-23 17:09:47 +02:00
3d7df8066d refactor(daily): factor our tutorial selection function 2024-09-18 18:03:49 +02:00
6c9d92475e fix(firm): filtering by active supervisor working 2024-09-17 17:59:58 +02:00
78c645cf21 fix(lpr): print log sorting works now 2024-09-17 17:58:52 +02:00
e8b276851c fix(build) 2024-09-17 12:58:13 +02:00
e16baedfce refactor(model): move JSONB instance to proper module 2024-09-17 12:57:31 +02:00
d19266e918 chore(lpr): improve lpr log display 2024-09-17 12:56:49 +02:00
53c68638da chore(daily): make company a property of TutorialParticipant, towards #90 2024-09-16 17:16:19 +02:00
6e3dd1c1f3 chore(daily): add more columns #90 2024-09-13 18:03:41 +02:00
ba0fd21c8f chore(daily): add page actions #90 2024-09-13 16:18:38 +02:00
d0eb3ddf92 refactor(jsonb): change DB using JSONB, to improve stub #90 2024-09-13 13:39:38 +02:00
5307350b0b chore(daily): improve stub #90 change DB to JSONB (WIP) 2024-09-12 17:46:38 +02:00
1a954e037f chore(daily): create stub in preparation for #90 2024-09-11 17:44:09 +02:00
faaaa18247 refactor(map): clarify some unnecessarily obfuscated code
also, using Map.fromList is more efficient if the list happens to be ordered
2024-09-11 17:43:56 +02:00
2e0455a154 chore(config): add config/develop-settings.yml only active if DEVELOPMENT
Ensure that certain settings are NOT seen in production, but automatically active in development without using environment variables.
2024-09-11 13:11:31 +02:00
1311 changed files with 22724 additions and 26054 deletions

View File

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

View File

@ -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.)

View File

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

View File

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

View File

@ -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)'

View File

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

View File

@ -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
View 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
View 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
View File

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

View File

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

View File

@ -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}}'
};

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ELearning 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 ELearning 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 ELearnings, #
wird dieser in Spalte "_{MsgLmsOrphanReason}" angezeigt. #
<p>
Verwaiste Logins werden beim nächsten Abruf der ELearning Logins von FRADrive zur Löschung durch das LMS gemeldet. #
Die Auswahl, ob ein ELearning 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 ELearning Login ist auch unter keiner anderen Qualifikation in FRADrive bekannt.
<p>
Es werden jedoch pro Abruf nur #{lmsOrphanDeletionBatch} ELearning 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}

View File

@ -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 elearning logins that have been reported back to FRADrive for qualification #{qsh}, #
but which are unknown to FRADrive. #
Normally, the LMS automatically deletes completed elearning logins. #
In some cases, however, this does not happen for unknown reasons. #
If a reason is known, such as a manual restart of the elearning, #
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 elearning logins. #
The decision whether an elearning 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 elearning login is not associated with any other qualification within FRADrive.
<p>
However, only #{lmsOrphanDeletionBatch} elearning 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}

View File

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

View File

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

View File

@ -2,4 +2,4 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = {extends: ['@commitlint/config-conventional']};
module.exports = {extends: ['@commitlint/config-conventional']}

View File

@ -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: ./

View File

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

View File

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

View File

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

View File

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

View 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
View 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=""

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

View 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
View 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."

View 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

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

View File

@ -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, ' ');
},

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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);

View File

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

View File

View File

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