From 6c3cb67bad6d0873cbdb27ab5edba5b1fb325cc9 Mon Sep 17 00:00:00 2001 From: Paul Ambrose Date: Wed, 3 Feb 2021 13:46:52 -0800 Subject: [PATCH] 1.10.0 * Move REDIS pool init to after properties are read * Add guard to make sure that Property values are not read before being initialized * Rearrnage starting order for assigning Kotlin script variables * Adjust Property getters to deal with access before init * Change ReadingBatContent.cacheChallenges to be computed * Fix Property initialization in tests * Limit the length of user input strings * Add dbms.maxLifetimeMins property * Update HttpClient to use expectSuccess * Fix cypress issue by resetting dbms * Fix cypress issue by catch geoinfo exception * Remove dbreset from cypress script * Turn off sql error stacktrace in request logging * Add caller version * Upgrade jars --- .gitignore | 3 + Makefile | 2 +- README.md | 2 +- build.gradle | 23 ++-- docs/notes.md | 13 +- gradle.properties | 24 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- package.json | 6 +- readingbat-core/build.gradle | 11 +- readingbat-core/src/main/kotlin/Content.kt | 6 +- .../com/github/readingbat/common/EnvVar.kt | 1 + .../com/github/readingbat/common/Limiter.kt | 1 + .../com/github/readingbat/common/Message.kt | 2 + .../com/github/readingbat/common/Property.kt | 106 +++++++++++++--- .../readingbat/common/SessionActivites.kt | 6 +- .../com/github/readingbat/common/User.kt | 9 +- .../com/github/readingbat/dsl/Challenge.kt | 3 +- .../github/readingbat/dsl/ChallengeGroup.kt | 1 + .../com/github/readingbat/dsl/ContentDsl.kt | 1 + .../github/readingbat/dsl/DslExceptions.kt | 2 - .../github/readingbat/dsl/LanguageGroup.kt | 1 + .../readingbat/dsl/ReadingBatContent.kt | 9 +- .../github/readingbat/pages/ChallengePage.kt | 1 - .../readingbat/pages/ClassSummaryPage.kt | 1 + .../readingbat/pages/CreateAccountPage.kt | 4 +- .../github/readingbat/pages/DbmsDownPage.kt | 3 +- .../com/github/readingbat/pages/ErrorPage.kt | 3 +- .../readingbat/pages/InvalidRequestPage.kt | 3 +- .../github/readingbat/pages/NotFoundPage.kt | 3 +- .../com/github/readingbat/pages/PageUtils.kt | 2 + .../readingbat/pages/PasswordResetPage.kt | 19 +-- .../github/readingbat/pages/PrivacyPage.kt | 3 +- .../readingbat/pages/StudentSummaryPage.kt | 4 +- .../readingbat/pages/SystemAdminPage.kt | 42 +++---- .../pages/SystemConfigurationPage.kt | 6 +- .../readingbat/pages/js/AdminCommandsJs.kt | 11 +- .../readingbat/pages/js/CheckAnswersJs.kt | 17 ++- .../readingbat/pages/js/LikeDislikeJs.kt | 7 +- .../github/readingbat/posts/ChallengePost.kt | 8 +- .../readingbat/posts/CreateAccountPost.kt | 26 ++-- .../readingbat/posts/PasswordResetPost.kt | 21 ++-- .../readingbat/server/ConfigureFormAuth.kt | 1 + .../com/github/readingbat/server/GeoInfo.kt | 1 + .../com/github/readingbat/server/Installs.kt | 10 +- .../github/readingbat/server/Intercepts.kt | 14 ++- .../readingbat/server/PostgresTables.kt | 1 + .../readingbat/server/ReadingBatServer.kt | 117 ++++++------------ .../github/readingbat/server/ScriptPools.kt | 4 +- .../github/readingbat/server/ServerUtils.kt | 1 + .../readingbat/server/routes/AdminRoutes.kt | 6 +- .../readingbat/server/routes/UserRoutes.kt | 15 +-- .../readingbat/server/ws/ClassSummaryWs.kt | 16 +-- .../github/readingbat/server/ws/ClockWs.kt | 1 + .../readingbat/server/ws/StudentSummaryWs.kt | 7 +- .../test_content/RedisSessionStorage.kt | 1 + .../com/github/readingbat/utils/RedisAdmin.kt | 1 + .../com/github/readingbat/utils/ScriptTest.kt | 4 + .../src/main/resources/application-dev.conf | 1 + .../resources/static/x-ray-glasses-large.jpg | Bin 0 -> 49238 bytes .../resources/static/x-ray-glasses-small.jpg | Bin 0 -> 21840 bytes readingbat-core/src/test/kotlin/TestData.kt | 2 +- .../readingbat/test_content/InfiniteLoop.kt | 8 +- .../github/readingbat/kotest/TestSupport.kt | 11 +- settings.gradle | 17 --- 64 files changed, 344 insertions(+), 313 deletions(-) create mode 100644 readingbat-core/src/main/resources/static/x-ray-glasses-large.jpg create mode 100644 readingbat-core/src/main/resources/static/x-ray-glasses-small.jpg diff --git a/.gitignore b/.gitignore index e528c5325..d73431ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ .vscode .gradle .idea +.run out build *.iml *.ipr *.iws +node_modules/ + cypress/videos cypress/screenshots cypress/examples diff --git a/Makefile b/Makefile index bbfd18358..f0d23b72a 100644 --- a/Makefile +++ b/Makefile @@ -50,4 +50,4 @@ depends: ./gradlew dependencies upgrade-wrapper: - ./gradlew wrapper --gradle-version=6.7.1 --distribution-type=bin \ No newline at end of file + ./gradlew wrapper --gradle-version=6.8.1 --distribution-type=bin \ No newline at end of file diff --git a/README.md b/README.md index 2a93660c1..072a5bdf0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Release](https://jitpack.io/v/readingbat/readingbat-core.svg)](https://jitpack.io/#readingbat/readingbat-core) [![Build Status](https://travis-ci.org/readingbat/readingbat-core.svg?branch=master)](https://travis-ci.org/readingbat/readingbat-core) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8a5c67f5892042559490559142af30ec)](https://www.codacy.com/gh/readingbat/readingbat-core?utm_source=github.com&utm_medium=referral&utm_content=readingbat/readingbat-core&utm_campaign=Badge_Grade) -[![ReadingBat](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/g5z7vz/1.9.0&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/g5z7vz/runs) +[![ReadingBat](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/g5z7vz&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/g5z7vz/runs) [![Kotlin](https://img.shields.io/badge/%20language-Kotlin-red.svg)](https://kotlinlang.org/) The framework used to render ReadingBat content. diff --git a/build.gradle b/build.gradle index 17f09fe5b..c77f2fc26 100644 --- a/build.gradle +++ b/build.gradle @@ -2,19 +2,18 @@ plugins { id 'java' id 'application' id 'maven' // Required for jitpack.io to do a ./gradlew install - id 'org.jetbrains.kotlin.jvm' version '1.4.21' apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.21' apply false + id 'org.jetbrains.kotlin.jvm' version '1.4.30' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.30' apply false id "com.github.ben-manes.versions" version '0.36.0' apply false id 'com.github.johnrengelman.shadow' version '6.1.0' apply false id 'com.github.gmazzo.buildconfig' version '2.0.2' apply false - id "org.flywaydb.flyway" version "7.3.1" + id "org.flywaydb.flyway" version "7.5.2" } ext { libraries = [ kotlin_reflect : "org.jetbrains.kotlin:kotlin-reflect", kotlinx_coroutines_core: "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version", - //kotlin_scripting_compiler: "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable", serialization : "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version", @@ -90,7 +89,7 @@ ext { allprojects { description = 'ReadingBat Core' group 'com.github.readingbat' - version '1.9.0' + version '1.10.0' apply plugin: 'application' apply plugin: 'maven' // required for jitpack.io install @@ -101,10 +100,10 @@ allprojects { apply plugin: 'com.github.gmazzo.buildconfig' repositories { - maven { url 'https://kotlin.bintray.com/ktor' } - maven { url 'https://kotlin.bintray.com/kotlinx' } - maven { url 'https://kotlin.bintray.com/kotlin-js-wrappers' } - maven { url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' } + maven { url = 'https://kotlin.bintray.com/ktor' } + maven { url = 'https://kotlin.bintray.com/kotlinx' } + maven { url = 'https://kotlin.bintray.com/kotlin-js-wrappers' } + maven { url = 'https://maven-central.storage-download.googleapis.com/repos/central/data/' } jcenter() maven { url = 'https://jitpack.io' } } @@ -118,8 +117,9 @@ subprojects { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - // This is required for the ExperimentalCoroutinesApi arg below - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm" + // These are required for the annotation args below + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutines_version}" + implementation "io.ktor:ktor-locations:${ktor_version}" implementation "io.github.microutils:kotlin-logging:$logging_version" implementation "ch.qos.logback:logback-classic:$logback_version" @@ -156,7 +156,6 @@ subprojects { kotlinOptions.jvmTarget = '1.8' kotlinOptions.freeCompilerArgs += ['-Xuse-experimental=kotlin.time.ExperimentalTime', '-Xuse-experimental=io.ktor.util.KtorExperimentalAPI', - '-Xuse-experimental=io.ktor.locations.KtorExperimentalLocationsAPI', '-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi', '-Xuse-experimental=kotlin.ExperimentalStdlibApi', '-XXLanguage:+InlineClasses'] diff --git a/docs/notes.md b/docs/notes.md index 591b29868..85204df33 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -6,9 +6,10 @@ ## Running from *readingbat-core* Setup: -* VM Options: -Dlogback.configurationFile=src/test/resources/logback-test.xml -* Program Arguments: -config=src/main/resources/application-dev.conf -* Environment Variables: SENDGRID_API_KEY=**value** +* Main class: TestMain +* VM Options: -Dlogback.configurationFile=testresources/logback-test.xml +* Program Arguments: -config=readingbat-core/src/main/resources/application-dev.conf +* Environment Variables: IPGEOLOCATION_KEY=**value**;GITHUB_OAUTH=**Value** ## Heroku * Switch shell to Java8 to get jvisualvm to work on an OSX client @@ -80,6 +81,8 @@ Setup: ## Cypress.io * tab plugin: https://github.com/Bkucera/cypress-plugin-tab -* To start locally: ~/node_modules/.bin/cypress open -* To run: ~/node_modules/.bin/cypress run --record --key 5ee5de19-1e84-4807-a199-5c70fda2fe5d +* Start test server with: `./testdata.sh` +* Clear data with: `make dbreset` +* To start locally from repo root: `~/node_modules/.bin/cypress open` +* To run: `~/node_modules/.bin/cypress run --record --key 5ee5de19-1e84-4807-a199-5c70fda2fe5d` * https://levelup.gitconnected.com/what-ive-learnt-using-cypress-io-for-the-past-three-weeks-c1597999cd2f \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 132de63e6..4d7d1a436 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,23 +8,23 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro cloud_version=1.2.0 commons_version=1.9 coroutines_version=1.4.2 -css_version=1.0.0-pre.131-kotlin-1.4.21 -exposed_version=0.28.1 +css_version=1.0.0-pre.144-kotlin-1.4.21 +exposed_version=0.29.1 flexmark_version=0.62.2 -github_api_version=1.117 +github_api_version=1.122 gson_version=2.8.6 -guava_version=29.0-jre -hikari_version=3.4.5 +guava_version=30.0-jre +hikari_version=4.0.1 java_script_version=2.0.0 -kotest_version=4.3.2 -ktor_version=1.4.2 +kotest_version=4.4.0 +ktor_version=1.5.0 logback_version=1.2.3 logging_version=2.0.4 pgjdbc_version=0.8.4 postgres_version=42.2.18 -prometheus_version=0.9.0 -proxy_version=68e629e -redis_version=3.4.0 +prometheus_version=0.10.0 +proxy_version=1.8.8 +redis_version=3.5.1 serialization_version=1.0.1 -sendgrid_version=4.7.0 -utils_version=1.6.0 +sendgrid_version=4.7.1 +utils_version=4aa1b9d diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d9ca1649..28ff446a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/package.json b/package.json index 9e26dfeeb..8de762193 100644 --- a/package.json +++ b/package.json @@ -1 +1,5 @@ -{} \ No newline at end of file +{ + "devDependencies": { + "cypress": "6.2.1" + } +} diff --git a/readingbat-core/build.gradle b/readingbat-core/build.gradle index ec8c09bd0..31934d4fa 100644 --- a/readingbat-core/build.gradle +++ b/readingbat-core/build.gradle @@ -12,7 +12,6 @@ application { dependencies { implementation libraries.kotlin_reflect implementation libraries.kotlinx_coroutines_core - //implementation libraries.kotlin_scripting_compiler implementation libraries.serialization @@ -64,6 +63,8 @@ dependencies { implementation libraries.pgjdbc implementation libraries.socket + runtimeOnly libraries.postgres + implementation libraries.gson implementation libraries.guava @@ -74,8 +75,6 @@ dependencies { implementation libraries.github - runtime libraries.postgres - testImplementation libraries.ktor_server_tests testImplementation libraries.ktor_server_test_host @@ -88,9 +87,9 @@ dependencies { } buildConfig { - buildConfigField('String', 'APP_NAME', "\"${project.name}\"") - buildConfigField('String', 'APP_VERSION', "\"${project.version}\"") - buildConfigField('String', 'APP_RELEASE_DATE', "\"12/15/20\"") + buildConfigField('String', 'CORE_NAME', "\"${project.name}\"") + buildConfigField('String', 'CORE_VERSION', "\"${project.version}\"") + buildConfigField('String', 'CORE_RELEASE_DATE', "\"2/3/21\"") } // Include build uberjars in heroku deploy diff --git a/readingbat-core/src/main/kotlin/Content.kt b/readingbat-core/src/main/kotlin/Content.kt index 2a07bb4b0..80c13a41a 100644 --- a/readingbat-core/src/main/kotlin/Content.kt +++ b/readingbat-core/src/main/kotlin/Content.kt @@ -104,7 +104,7 @@ val dslContent = python { //repo = GitHubRepo(Organization, "readingbat", "readingbat-core") - //branchName = "1.9.0" + //branchName = "1.10.0" repo = FileSystemSource("./") srcPath = "python" @@ -125,7 +125,7 @@ val dslContent = java { repo = GitHubRepo(Organization, "readingbat", "readingbat-core") srcPath = "readingbat-core/src/test/java" - branchName = "1.9.0" + branchName = "1.10.0" group("Java Tests") { packageName = "com.github.readingbat.test_content" @@ -141,7 +141,7 @@ val dslContent = kotlin { repo = GitHubRepo(Organization, "readingbat", "readingbat-core") srcPath = "readingbat-core/src/test/kotlin" - branchName = "1.9.0" + branchName = "1.10.0" group("Kotlin Tests") { packageName = "com.github.readingbat.test_content" diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/EnvVar.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/EnvVar.kt index ccbc4d16b..74c3a833f 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/EnvVar.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/EnvVar.kt @@ -52,6 +52,7 @@ enum class EnvVar(val maskFunc: EnvVar.() -> String = { getEnv(UNASSIGNED) }) { fun getEnv(default: Boolean) = System.getenv(name)?.toBoolean() ?: default + @Suppress("unused") fun getEnv(default: Int) = System.getenv(name)?.toInt() ?: default fun getRequiredEnv() = getEnvOrNull() ?: error("Missing $name value") diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Limiter.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Limiter.kt index 17dee1672..8b9e3fdcd 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Limiter.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Limiter.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.runBlocking class Limiter(private val maxConcurrencySize: Int = 1) { + @Suppress("unused") class Token(private val limiter: Limiter) private val channel = Channel(maxConcurrencySize) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Message.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Message.kt index 1d471a407..086c88368 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Message.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Message.kt @@ -27,6 +27,8 @@ import com.github.readingbat.common.Constants.WRONG_COLOR val color get() = if (isError) WRONG_COLOR else CORRECT_COLOR fun isAssigned() = this != EMPTY_MESSAGE + + @Suppress("unused") fun isUnassigned() = this == EMPTY_MESSAGE override fun toString() = value diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Property.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Property.kt index 8aae80274..28e8b4ce5 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/Property.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/Property.kt @@ -19,7 +19,6 @@ package com.github.readingbat.common import com.github.pambrose.common.redis.RedisUtils import com.github.pambrose.common.util.isNotNull -import com.github.pambrose.common.util.isNull import com.github.pambrose.common.util.obfuscate import com.github.readingbat.common.Constants.UNASSIGNED import com.github.readingbat.common.PropertyNames.AGENT @@ -32,9 +31,10 @@ import com.github.readingbat.common.PropertyNames.SITE import io.ktor.application.* import io.ktor.config.* import mu.KLogging +import java.util.concurrent.atomic.AtomicBoolean enum class Property(val propertyValue: String, - val maskFunc: Property.() -> String = { getProperty(UNASSIGNED) }) { + val maskFunc: Property.() -> String = { getProperty(UNASSIGNED, false) }) { KOTLIN_SCRIPT_CLASSPATH("kotlin.script.classpath"), @@ -58,7 +58,7 @@ enum class Property(val propertyValue: String, XFORWARDED_ENABLED("$READINGBAT.$SITE.xforwardedHeaderSupportEnabled"), // These are assigned to ReadingBatContent vals - ANALYTICS_ID("$READINGBAT.$SITE.googleAnalyticsId", { getPropertyOrNull() ?: UNASSIGNED }), + ANALYTICS_ID("$READINGBAT.$SITE.googleAnalyticsId", { getPropertyOrNull(false) ?: UNASSIGNED }), MAX_HISTORY_LENGTH("$READINGBAT.$CHALLENGES.maxHistoryLength"), MAX_CLASS_COUNT("$READINGBAT.$CLASSES.maxCount"), KTOR_PORT("ktor.deployment.port"), @@ -74,9 +74,9 @@ enum class Property(val propertyValue: String, CONTENT_CACHING_ENABLED("$READINGBAT.$SITE.contentCachingEnabled"), AGENT_ENABLED("$AGENT.enabled"), - PINGDOM_BANNER_ID("$READINGBAT.$SITE.pingdomBannerId", { getPropertyOrNull() ?: UNASSIGNED }), - PINGDOM_URL("$READINGBAT.$SITE.pingdomUrl", { getPropertyOrNull() ?: UNASSIGNED }), - STATUS_PAGE_URL("$READINGBAT.$SITE.statusPageUrl", { getPropertyOrNull() ?: UNASSIGNED }), + PINGDOM_BANNER_ID("$READINGBAT.$SITE.pingdomBannerId", { getPropertyOrNull(false) ?: UNASSIGNED }), + PINGDOM_URL("$READINGBAT.$SITE.pingdomUrl", { getPropertyOrNull(false) ?: UNASSIGNED }), + STATUS_PAGE_URL("$READINGBAT.$SITE.statusPageUrl", { getPropertyOrNull(false) ?: UNASSIGNED }), PROMETHEUS_URL("$READINGBAT.prometheus.url"), GRAFANA_URL("$READINGBAT.grafana.url"), @@ -85,14 +85,15 @@ enum class Property(val propertyValue: String, KOTLIN_SCRIPTS_POOL_SIZE("$READINGBAT.scripts.kotlinPoolSize"), PYTHON_SCRIPTS_POOL_SIZE("$READINGBAT.scripts.pythonPoolSize"), - KOTLIN_EVALUATOR_POOL_SIZE("$READINGBAT.evaluators.kotlinPoolSize"), - PYTHON_EVALUATOR_POOL_SIZE("$READINGBAT.evaluators.pythonPoolSize"), + KOTLIN_EVALUATORS_POOL_SIZE("$READINGBAT.evaluators.kotlinPoolSize"), + PYTHON_EVALUATORS_POOL_SIZE("$READINGBAT.evaluators.pythonPoolSize"), DBMS_DRIVER_CLASSNAME("$DBMS.driverClassName"), DBMS_URL("$DBMS.jdbcUrl"), DBMS_USERNAME("$DBMS.username"), - DBMS_PASSWORD("$DBMS.password", { getPropertyOrNull()?.obfuscate(1) ?: UNASSIGNED }), + DBMS_PASSWORD("$DBMS.password", { getPropertyOrNull(false)?.obfuscate(1) ?: UNASSIGNED }), DBMS_MAX_POOL_SIZE("$DBMS.maxPoolSize"), + DBMS_MAX_LIFETIME_MINS("$DBMS.maxLifetimeMins"), REDIS_MAX_POOL_SIZE(RedisUtils.REDIS_MAX_POOL_SIZE), REDIS_MAX_IDLE_SIZE(RedisUtils.REDIS_MAX_IDLE_SIZE), @@ -114,15 +115,21 @@ enum class Property(val propertyValue: String, fun configValueOrNull(application: Application) = application.environment.config.propertyOrNull(propertyValue) - fun getProperty(default: String) = System.getProperty(propertyValue) ?: default + fun getProperty(default: String, errorOnNonInit: Boolean = true) = + (System.getProperty(propertyValue) + ?: default).also { if (errorOnNonInit && !initialized.get()) error(notInitialized(this)) } - fun getProperty(default: Boolean) = System.getProperty(propertyValue)?.toBoolean() ?: default + fun getProperty(default: Boolean) = (System.getProperty(propertyValue)?.toBoolean() + ?: default).also { if (!initialized.get()) error(notInitialized(this)) } - fun getProperty(default: Int) = System.getProperty(propertyValue)?.toIntOrNull() ?: default + fun getProperty(default: Int) = (System.getProperty(propertyValue)?.toIntOrNull() + ?: default).also { if (!initialized.get()) error(notInitialized(this)) } - fun getPropertyOrNull(): String? = System.getProperty(propertyValue) + fun getPropertyOrNull(errorOnNonInit: Boolean = true): String? = + System.getProperty(propertyValue).also { if (errorOnNonInit && !initialized.get()) error(notInitialized(this)) } - fun getRequiredProperty() = getPropertyOrNull() ?: error("Missing $propertyValue value") + fun getRequiredProperty() = (getPropertyOrNull() + ?: error("Missing $propertyValue value")).also { if (!initialized.get()) error(notInitialized(this)) } fun setProperty(value: String) { System.setProperty(propertyValue, value) @@ -134,9 +141,74 @@ enum class Property(val propertyValue: String, setProperty(configValue(application, default)) } - fun isDefined() = getPropertyOrNull().isNotNull() - fun isNotDefined() = getPropertyOrNull().isNull() + fun isDefined() = System.getProperty(propertyValue).isNotNull() + fun isNotDefined() = !isDefined() - companion object : KLogging() + companion object : KLogging() { + private val initialized = AtomicBoolean(false) + + fun assignInitialized() = initialized.set(true) + + private fun notInitialized(prop: Property) = "Property ${prop.name} not initialized" + + internal fun Application.assignProperties() { + + val agentEnabled = + EnvVar.AGENT_ENABLED.getEnv(AGENT_ENABLED.configValue(this, default = "false").toBoolean()) + AGENT_ENABLED.setProperty(agentEnabled.toString()) + PROXY_HOSTNAME.setPropertyFromConfig(this, "") + + IS_PRODUCTION.also { it.setProperty(it.configValue(this, "false").toBoolean().toString()) } + + DBMS_ENABLED.also { it.setProperty(it.configValue(this, "false").toBoolean().toString()) } + REDIS_ENABLED.also { it.setProperty(it.configValue(this, "false").toBoolean().toString()) } + + SAVE_REQUESTS_ENABLED.also { it.setProperty(it.configValue(this, "true").toBoolean().toString()) } + + MULTI_SERVER_ENABLED.also { it.setProperty(it.configValue(this, "false").toBoolean().toString()) } + + CONTENT_CACHING_ENABLED.also { it.setProperty(it.configValue(this, "false").toBoolean().toString()) } + + DSL_FILE_NAME.setPropertyFromConfig(this, "src/Content.kt") + DSL_VARIABLE_NAME.setPropertyFromConfig(this, "content") + + ANALYTICS_ID.setPropertyFromConfig(this, "") + + PINGDOM_BANNER_ID.setPropertyFromConfig(this, "") + PINGDOM_URL.setPropertyFromConfig(this, "") + STATUS_PAGE_URL.setPropertyFromConfig(this, "") + + PROMETHEUS_URL.setPropertyFromConfig(this, "") + GRAFANA_URL.setPropertyFromConfig(this, "") + + JAVA_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") + KOTLIN_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") + PYTHON_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") + + KOTLIN_EVALUATORS_POOL_SIZE.setPropertyFromConfig(this, "5") + PYTHON_EVALUATORS_POOL_SIZE.setPropertyFromConfig(this, "5") + + DBMS_DRIVER_CLASSNAME.setPropertyFromConfig(this, "com.impossibl.postgres.jdbc.PGDriver") + DBMS_URL.setPropertyFromConfig(this, "jdbc:pgsql://localhost:5432/readingbat") + DBMS_USERNAME.setPropertyFromConfig(this, "postgres") + DBMS_PASSWORD.setPropertyFromConfig(this, "") + DBMS_MAX_POOL_SIZE.setPropertyFromConfig(this, "10") + DBMS_MAX_LIFETIME_MINS.setPropertyFromConfig(this, "30") + + REDIS_MAX_POOL_SIZE.setPropertyFromConfig(this, "10") + REDIS_MAX_IDLE_SIZE.setPropertyFromConfig(this, "5") + REDIS_MIN_IDLE_SIZE.setPropertyFromConfig(this, "1") + + KTOR_PORT.setPropertyFromConfig(this, "0") + KTOR_WATCH.also { it.setProperty(it.configValueOrNull(this)?.getList()?.toString() ?: UNASSIGNED) } + + SENDGRID_PREFIX.also { + it.setProperty(EnvVar.SENDGRID_PREFIX.getEnv(it.configValue(this, + "https://www.readingbat.com"))) + } + + assignInitialized() + } + } } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/SessionActivites.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/SessionActivites.kt index 2b9a927af..efb808316 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/SessionActivites.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/SessionActivites.kt @@ -72,12 +72,12 @@ internal object SessionActivites : KLogging() { measureTimedValue { val count = Count(Users.id) val maxDate = Max(created, DateColumnType(true)) - val elems = arrayOf(session_id, fullName, email, ip, city, state, country, isp, flagUrl, userAgent) + val elems = arrayOf(fullName, email, ip, city, state, country, isp, flagUrl, userAgent) (ServerRequests innerJoin BrowserSessions innerJoin Users innerJoin GeoInfos) - .slice(*elems, count, maxDate) + .slice(session_id, *elems, count, maxDate) .select { created greater dateTimeExpr("now() - interval '${min(dayCount, 14)} day'") } - .groupBy(*elems) + .groupBy(*(arrayOf(session_id) + elems)) .orderBy(maxDate, SortOrder.DESC) .map { row -> QueryInfo(row[session_id], diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/common/User.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/common/User.kt index cee0ef9ed..1a743b384 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/common/User.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/common/User.kt @@ -19,6 +19,7 @@ package com.github.readingbat.common import com.github.pambrose.common.util.isNotNull import com.github.pambrose.common.util.isNull +import com.github.pambrose.common.util.maxLength import com.github.pambrose.common.util.md5Of import com.github.pambrose.common.util.newStringSalt import com.github.pambrose.common.util.randomId @@ -206,7 +207,7 @@ import kotlin.time.measureTime .insert { row -> row[userRef] = userDbmsId row[Classes.classCode] = classCode.value - row[description] = classDesc + row[description] = classDesc.maxLength(256) } } @@ -399,7 +400,7 @@ import kotlin.time.measureTime .firstOrNull() ?: EMPTY_RESET_ID } - fun savePasswordResetId(email: Email, previousResetId: ResetId, newResetId: ResetId) { + fun savePasswordResetId(email: Email, newResetId: ResetId) { transaction { PasswordResets .upsert(conflictIndex = passwordResetsIndex) { row -> @@ -624,8 +625,8 @@ import kotlin.time.measureTime Users .insertAndGetId { row -> row[userId] = user.userId - row[fullName] = name.value - row[Users.email] = email.value + row[fullName] = name.value.maxLength(128) + row[Users.email] = email.value.maxLength(128) row[enrolledClassCode] = DISABLED_CLASS_CODE.value row[defaultLanguage] = defaultLanguageType.languageName.value row[Users.salt] = salt diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/Challenge.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/Challenge.kt index 3ada5eedf..03bc13313 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/Challenge.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/Challenge.kt @@ -159,7 +159,7 @@ sealed class Challenge(val challengeGroup: ChallengeGroup<*>, return measureParsing(file.content) } - if (content.cacheChallenges || isTesting()) + if (content.cacheChallenges) content.functionInfoMap.computeIfAbsent(challengeId) { parseCode() } else parseCode() @@ -170,6 +170,7 @@ sealed class Challenge(val challengeGroup: ChallengeGroup<*>, error(""""$challengeName" is empty""") } + @Suppress("unused") private fun Any?.prettyQuote(capitalizePythonBooleans: Boolean = true, useDoubleQuotes: Boolean = false) = when { this is String -> if (languageType.useDoubleQuotes || useDoubleQuotes) toDoubleQuoted() else toSingleQuoted() diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ChallengeGroup.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ChallengeGroup.kt index 6a13e690f..49a68fa01 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ChallengeGroup.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ChallengeGroup.kt @@ -41,6 +41,7 @@ import java.io.File import kotlin.reflect.KProperty @ReadingBatDslMarker +@Suppress("unused") class ChallengeGroup(/*internal*/ val languageGroup: LanguageGroup, internal val groupNameSuffix: GroupName) { /*internal*/ val challenges = mutableListOf() diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ContentDsl.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ContentDsl.kt index 57b9dd597..f4a7c509c 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ContentDsl.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ContentDsl.kt @@ -163,4 +163,5 @@ private suspend fun evalDsl(code: String, sourceName: String) = throw e } +@Suppress("unused") object ContentDsl : KLogging() diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/DslExceptions.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/DslExceptions.kt index 95c6dd5ea..633fa88f9 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/DslExceptions.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/DslExceptions.kt @@ -23,8 +23,6 @@ internal class InvalidRequestException(msg: String) : Exception(msg) internal class MissingBrowserSessionException(msg: String) : Exception(msg) -internal class InvalidConfigurationException(msg: String) : Exception(msg) - internal class RedisUnavailableException(msg: String) : Exception(msg) internal class DataException(val msg: String) : JedisException(msg) \ No newline at end of file diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/LanguageGroup.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/LanguageGroup.kt index 165b13f76..3afbf35f7 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/LanguageGroup.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/LanguageGroup.kt @@ -91,6 +91,7 @@ class LanguageGroup(internal val content: ReadingBatContent, } @ReadingBatDslMarker + @Suppress("unused") fun include(challengeGroup: ChallengeGroup) { addGroup(challengeGroup) } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ReadingBatContent.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ReadingBatContent.kt index 5553590b8..a1b8f78b7 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ReadingBatContent.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/dsl/ReadingBatContent.kt @@ -67,10 +67,9 @@ class ReadingBatContent { val languages by lazy { listOf(python, java, kotlin) } - // User properties - // Default value is isProduction() - var cacheChallenges = isProduction() + val cacheChallenges get() = isProduction() || isTesting() + // User properties // These are defaults and can be overridden in language specific section //var repo: ContentRoot = defaultContentRoot # Makes repo a required value var repo: ContentRoot = FileSystemSource("./") @@ -127,6 +126,7 @@ class ReadingBatContent { internal fun validate() = languageList.forEach { it.validate() } + @Suppress("unused") internal fun functionInfoByMd5(md5: String) = functionInfoMap.asSequence().firstOrNull { it.component2().challengeMd5.value == md5 }?.value @@ -140,6 +140,7 @@ class ReadingBatContent { fun kotlin(block: LanguageGroup.() -> Unit) = kotlin.run(block) @ReadingBatDslMarker + @Suppress("UNCHECKED_CAST") operator fun LanguageGroup.unaryPlus() { val languageGroup = this@ReadingBatContent.findLanguage(languageType) as LanguageGroup challengeGroups.forEach { languageGroup.addGroup(it) } @@ -174,7 +175,7 @@ class ReadingBatContent { challengeGroup.challenges .forEach { challenge -> if (useWebApi) { - HttpClient(CIO) + HttpClient(CIO) { expectSuccess = false } .use { httpClient -> withHttpClient(httpClient) { val url = pathOf(prefix, CONTENT, challenge.path) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ChallengePage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ChallengePage.kt index 213f482bd..cd8fd14ac 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ChallengePage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ChallengePage.kt @@ -384,7 +384,6 @@ internal object ChallengePage : KLogging() { val languageName = challenge.languageType.languageName val groupName = challenge.groupName - val challengeName = challenge.challengeName val displayStr = classCode.toDisplayString() h3 { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ClassSummaryPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ClassSummaryPage.kt index cd79afd2b..861ba70fc 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ClassSummaryPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ClassSummaryPage.kt @@ -398,5 +398,6 @@ internal object ClassSummaryPage : KLogging() { private fun UL.dropdownHeader(text: String) = li { style = "font-size:120%"; classes = setOf("dropdown-header"); +text } + @Suppress("unused") private fun UL.divider() = li { classes = setOf("divider") } } \ No newline at end of file diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/CreateAccountPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/CreateAccountPage.kt index 906c870cd..9d8d59d6f 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/CreateAccountPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/CreateAccountPage.kt @@ -26,7 +26,6 @@ import com.github.readingbat.common.FormFields.PASSWORD_PARAM import com.github.readingbat.common.FormFields.RETURN_PARAM import com.github.readingbat.common.Message import com.github.readingbat.common.Message.Companion.EMPTY_MESSAGE -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.clickButtonScript @@ -48,8 +47,7 @@ internal object CreateAccountPage { private const val createButton = "CreateAccountButton" - fun PipelineCall.createAccountPage(content: ReadingBatContent, - defaultFullName: FullName = EMPTY_FULLNAME, + fun PipelineCall.createAccountPage(defaultFullName: FullName = EMPTY_FULLNAME, defaultEmail: Email = EMPTY_EMAIL, msg: Message = EMPTY_MESSAGE) = createHTML() diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/DbmsDownPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/DbmsDownPage.kt index 727f4e466..35e9fb94b 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/DbmsDownPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/DbmsDownPage.kt @@ -21,7 +21,6 @@ import com.github.pambrose.common.util.pathOf import com.github.readingbat.common.CSSNames.CENTER import com.github.readingbat.common.Constants.DBMS_DOWN import com.github.readingbat.common.Endpoints.STATIC_ROOT -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.headDefault @@ -36,7 +35,7 @@ import kotlinx.html.stream.createHTML internal object DbmsDownPage { - fun dbmsDownPage(content: ReadingBatContent) = + fun dbmsDownPage() = createHTML() .html { head { headDefault() } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ErrorPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ErrorPage.kt index 82562c4c0..a6e45a65d 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ErrorPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/ErrorPage.kt @@ -21,7 +21,6 @@ import com.github.pambrose.common.util.pathOf import com.github.readingbat.common.CSSNames import com.github.readingbat.common.CSSNames.CENTER import com.github.readingbat.common.Endpoints.STATIC_ROOT -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.headDefault @@ -38,7 +37,7 @@ import kotlinx.html.stream.createHTML internal object ErrorPage { - fun errorPage(content: ReadingBatContent) = + fun errorPage() = createHTML() .html { head { headDefault() } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/InvalidRequestPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/InvalidRequestPage.kt index 876d76b0e..76f891f04 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/InvalidRequestPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/InvalidRequestPage.kt @@ -21,7 +21,6 @@ import com.github.pambrose.common.util.pathOf import com.github.readingbat.common.CSSNames import com.github.readingbat.common.CSSNames.CENTER import com.github.readingbat.common.Endpoints.STATIC_ROOT -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.headDefault @@ -31,7 +30,7 @@ import kotlinx.html.stream.createHTML internal object InvalidRequestPage { - fun invalidRequestPage(content: ReadingBatContent, uri: String, msg: String) = + fun invalidRequestPage(uri: String, msg: String) = createHTML() .html { head { headDefault() } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/NotFoundPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/NotFoundPage.kt index 6cd8caa01..11de6e5bb 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/NotFoundPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/NotFoundPage.kt @@ -21,7 +21,6 @@ import com.github.pambrose.common.util.pathOf import com.github.readingbat.common.CSSNames import com.github.readingbat.common.CSSNames.CENTER import com.github.readingbat.common.Endpoints.STATIC_ROOT -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.headDefault @@ -39,7 +38,7 @@ import kotlinx.html.stream.createHTML internal object NotFoundPage { - fun notFoundPage(content: ReadingBatContent, uri: String) = + fun notFoundPage(uri: String) = createHTML() .html { head { headDefault() } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PageUtils.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PageUtils.kt index a648fee3f..44ba3842f 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PageUtils.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PageUtils.kt @@ -179,6 +179,7 @@ internal object PageUtils { return " - $studentCount ${"student".pluralize(enrollees.count())} enrolled" } + @Suppress("unused") fun BODY.confirmingButton(text: String, endpoint: String, msg: String) { form { style = "margin:0" @@ -220,6 +221,7 @@ internal object PageUtils { fun HTMLTag.rawHtml(html: String) = unsafe { raw(html) } + @Suppress("unused") fun Route.getAndPost(path: String, body: PipelineInterceptor) { get(path, body) post(path, body) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PasswordResetPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PasswordResetPage.kt index b3a7341d3..ec39f2490 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PasswordResetPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PasswordResetPage.kt @@ -30,7 +30,6 @@ import com.github.readingbat.common.FormFields.RETURN_PARAM import com.github.readingbat.common.FormFields.UPDATE_PASSWORD import com.github.readingbat.common.Message import com.github.readingbat.common.Message.Companion.EMPTY_MESSAGE -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.clickButtonScript @@ -59,11 +58,9 @@ internal object PasswordResetPage : KLogging() { private const val formName = "pform" private const val passwordButton = "UpdatePasswordButton" - fun PipelineCall.passwordResetPage(content: ReadingBatContent, - resetId: ResetId, - msg: Message = EMPTY_MESSAGE) = + fun PipelineCall.passwordResetPage(resetId: ResetId, msg: Message = EMPTY_MESSAGE) = if (resetId.isBlank()) - requestPasswordResetPage(content, msg) + requestPasswordResetPage(msg) else { try { val email = @@ -84,16 +81,15 @@ internal object PasswordResetPage : KLogging() { } } - changePasswordPage(content, Email(email), resetId, msg) + changePasswordPage(Email(email), resetId, msg) } catch (e: ResetPasswordException) { logger.info { e } - requestPasswordResetPage(content, Message(e.message ?: "Unable to reset password", true)) + requestPasswordResetPage(Message(e.message ?: "Unable to reset password", true)) } } - private fun PipelineCall.requestPasswordResetPage(content: ReadingBatContent, - msg: Message = EMPTY_MESSAGE) = + private fun PipelineCall.requestPasswordResetPage(msg: Message = EMPTY_MESSAGE) = createHTML() .html { head { headDefault() } @@ -144,10 +140,7 @@ internal object PasswordResetPage : KLogging() { } } - private fun PipelineCall.changePasswordPage(content: ReadingBatContent, - email: Email, - resetId: ResetId, - msg: Message) = + private fun PipelineCall.changePasswordPage(email: Email, resetId: ResetId, msg: Message) = createHTML() .html { head { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PrivacyPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PrivacyPage.kt index 68ebb104f..4ae5205f0 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PrivacyPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/PrivacyPage.kt @@ -19,7 +19,6 @@ package com.github.readingbat.pages import com.github.readingbat.common.CSSNames.INDENT_1EM import com.github.readingbat.common.FormFields.RETURN_PARAM -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.PageUtils.backLink import com.github.readingbat.pages.PageUtils.bodyTitle import com.github.readingbat.pages.PageUtils.headDefault @@ -37,7 +36,7 @@ import kotlinx.html.stream.createHTML internal object PrivacyPage { - fun PipelineCall.privacyPage(content: ReadingBatContent) = + fun PipelineCall.privacyPage() = createHTML() .html { head { headDefault() } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/StudentSummaryPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/StudentSummaryPage.kt index 92c354f59..e937e238a 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/StudentSummaryPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/StudentSummaryPage.kt @@ -71,7 +71,7 @@ internal object StudentSummaryPage : KLogging() { fun PipelineCall.studentSummaryPage(content: ReadingBatContent, user: User?): String { val p = call.parameters val languageName = p[LANG_TYPE_QP]?.let { LanguageName(it) } ?: throw InvalidRequestException("Missing language") - val student = p[USER_ID_QP]?.let { it.toUser() } ?: throw InvalidRequestException("Missing user id") + val student = p[USER_ID_QP]?.toUser() ?: throw InvalidRequestException("Missing user id") val classCode = p[CLASS_CODE_QP]?.let { ClassCode(it) } ?: throw InvalidRequestException("Missing class code") val activeClassCode = queryActiveClassCode(user) @@ -148,7 +148,7 @@ internal object StudentSummaryPage : KLogging() { } private fun BODY.displayChallengeGroups(content: ReadingBatContent, - classCode: ClassCode, + @Suppress("UNUSED_PARAMETER") classCode: ClassCode, languageName: LanguageName) = div(classes = INDENT_2EM) { table(classes = INVOC_TABLE) { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemAdminPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemAdminPage.kt index 0586cbb0b..c6de90cb1 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemAdminPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemAdminPage.kt @@ -22,9 +22,6 @@ import com.github.readingbat.common.Endpoints.ADMIN_PREFS_ENDPOINT import com.github.readingbat.common.Endpoints.DELETE_CONTENT_IN_REDIS_ENDPOINT import com.github.readingbat.common.Endpoints.GARBAGE_COLLECTOR_ENDPOINT import com.github.readingbat.common.Endpoints.LOAD_ALL_ENDPOINT -import com.github.readingbat.common.Endpoints.LOAD_JAVA_ENDPOINT -import com.github.readingbat.common.Endpoints.LOAD_KOTLIN_ENDPOINT -import com.github.readingbat.common.Endpoints.LOAD_PYTHON_ENDPOINT import com.github.readingbat.common.Endpoints.LOGGING_ENDPOINT import com.github.readingbat.common.Endpoints.RESET_CACHE_ENDPOINT import com.github.readingbat.common.Endpoints.RESET_CONTENT_DSL_ENDPOINT @@ -115,23 +112,23 @@ internal object SystemAdminPage : KLogging() { "Are you sure you want to run the garbage collector?") } - p { - this@body.adminButton("Load Java Challenges", - LOAD_JAVA_ENDPOINT, - "Are you sure you want to load all the Java challenges? (This can take a while)") - } - - p { - this@body.adminButton("Load Python Challenges", - LOAD_PYTHON_ENDPOINT, - "Are you sure you want to load all the Python challenges? (This can take a while)") - } - - p { - this@body.adminButton("Load Kotlin Challenges", - LOAD_KOTLIN_ENDPOINT, - "Are you sure you want to load all the Kotlin challenges? (This can take a while)") - } +// p { +// this@body.adminButton("Load Java Challenges", +// LOAD_JAVA_ENDPOINT, +// "Are you sure you want to load all the Java challenges? (This can take a while)") +// } +// +// p { +// this@body.adminButton("Load Python Challenges", +// LOAD_PYTHON_ENDPOINT, +// "Are you sure you want to load all the Python challenges? (This can take a while)") +// } +// +// p { +// this@body.adminButton("Load Kotlin Challenges", +// LOAD_KOTLIN_ENDPOINT, +// "Are you sure you want to load all the Kotlin challenges? (This can take a while)") +// } p { this@body.adminButton("Load All Challenges", @@ -141,12 +138,12 @@ internal object SystemAdminPage : KLogging() { Property.GRAFANA_URL.getPropertyOrNull() ?.also { - if (it.isNotBlank()) p { +"Grafana Dashboard is "; a { href = it; target = "_blank"; +"here" } } + //if (it.isNotBlank()) p { +"Grafana Dashboard is "; a { href = it; target = "_blank"; +"here" } } } Property.PROMETHEUS_URL.getPropertyOrNull() ?.also { - if (it.isNotBlank()) p { +"Prometheus Dashboard is "; a { href = it; target = "_blank"; +"here" } } + //if (it.isNotBlank()) p { +"Prometheus Dashboard is "; a { href = it; target = "_blank"; +"here" } } } } else { @@ -156,6 +153,7 @@ internal object SystemAdminPage : KLogging() { p { textArea { id = msgs + readonly = true rows = "20" cols = "120" +"" diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemConfigurationPage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemConfigurationPage.kt index ffa354008..0a4cc17ab 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemConfigurationPage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/SystemConfigurationPage.kt @@ -75,9 +75,13 @@ internal object SystemConfigurationPage { div(classes = INDENT_1EM) { table { tr { - td { +"Version:" } + td { +"Core version:" } td { +ReadingBatServer::class.versionDesc() } } + tr { + td { +"Caller version:" } + td { +ReadingBatServer.callerVersion } + } tr { td { +"Server started:" } td { +ReadingBatServer.timeStamp } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/AdminCommandsJs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/AdminCommandsJs.kt index 3bd0b25a4..afd945144 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/AdminCommandsJs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/AdminCommandsJs.kt @@ -37,9 +37,8 @@ internal object AdminCommandsJs { var re = new XMLHttpRequest(); function $ADMIN_FUNC(msg, endPoint) { - if (confirm(msg)) { - var data = "$LOG_ID=$logId"; + let data = "$LOG_ID=$logId"; //re.onreadystatechange = checkLogHandleDone; re.open("POST", endPoint, true); re.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); @@ -56,10 +55,10 @@ internal object AdminCommandsJs { document.getElementById('$SUCCESS_ID').innerText = ''; } else if(re.readyState == 4) { // done - var success = true; - var results = eval(re.responseText); - for (var i = 0; i < results.length; i++) { - var x = document.getElementById("$FEEDBACK_ID"+i); + let success = true; + let results = eval(re.responseText); + for (let i = 0; i < results.length; i++) { + let x = document.getElementById("$FEEDBACK_ID"+i); if (results[i][0] == 0) { x.style.backgroundColor = '$NO_ANSWER_COLOR'; success = false; diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/CheckAnswersJs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/CheckAnswersJs.kt index f03df35fa..f0100ff39 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/CheckAnswersJs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/CheckAnswersJs.kt @@ -51,20 +51,19 @@ internal object CheckAnswersJs { var re = new XMLHttpRequest(); function $PROCESS_USER_ANSWERS_FUNC(event, cnt) { - // event will equal null on button press if (event != null && (event.keyCode != 13 && event.keyCode != 9)) return 1; - var data = "$SESSION_ID=${sessionCounter.incrementAndGet()}&$LANG_SRC=$languageName&$GROUP_SRC=$groupName&$CHALLENGE_SRC=$challengeName"; + let data = "$SESSION_ID=${sessionCounter.incrementAndGet()}&$LANG_SRC=$languageName&$GROUP_SRC=$groupName&$CHALLENGE_SRC=$challengeName"; try { - for (var i = 0; i < cnt; i++) { - var x = document.getElementById("$FEEDBACK_ID"+i); + for (let i = 0; i < cnt; i++) { + let x = document.getElementById("$FEEDBACK_ID"+i); x.style.backgroundColor = "white"; document.getElementById("$HINT_ID"+i).innerText = ''; - var ur = document.getElementById("$RESP"+i).value; + let ur = document.getElementById("$RESP"+i).value; data += "&$RESP" + i + "=" + encodeURIComponent(ur); } } @@ -88,10 +87,10 @@ internal object CheckAnswersJs { document.getElementById('$NEXTPREVCHANCE_ID').style.display = "none"; } else if(re.readyState == 4) { // done - var success = true; - var results = eval(re.responseText); - for (var i = 0; i < results.length; i++) { - var x = document.getElementById("$FEEDBACK_ID"+i); + let success = true; + let results = eval(re.responseText); + for (let i = 0; i < results.length; i++) { + let x = document.getElementById("$FEEDBACK_ID"+i); if (results[i][0] == ${NOT_ANSWERED.value}) { x.style.backgroundColor = '$NO_ANSWER_COLOR'; success = false; diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/LikeDislikeJs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/LikeDislikeJs.kt index f5eb95627..d4fca8e1f 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/LikeDislikeJs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/pages/js/LikeDislikeJs.kt @@ -46,8 +46,7 @@ internal object LikeDislikeJs { var re = new XMLHttpRequest(); function $LIKE_DISLIKE_FUNC(desc) { - - var data = "$SESSION_ID=${sessionCounter.incrementAndGet()}&$LANG_SRC=$languageName&$GROUP_SRC=$groupName&$CHALLENGE_SRC=$challengeName"; + let data = "$SESSION_ID=${sessionCounter.incrementAndGet()}&$LANG_SRC=$languageName&$GROUP_SRC=$groupName&$CHALLENGE_SRC=$challengeName"; data += "&$LIKE_DESC=" + encodeURIComponent(desc); re.onreadystatechange = likeDislikeHandleDone; @@ -57,13 +56,13 @@ internal object LikeDislikeJs { return 1; } - function likeDislikeHandleDone(){ + function likeDislikeHandleDone() { if(re.readyState == 1) { // starting document.getElementById('$LIKE_SPINNER_ID').innerHTML = ''; document.getElementById('$LIKE_STATUS_ID').innerText = 'Setting like/dislike...'; } else if(re.readyState == 4) { // done - var results = eval(re.responseText); + let results = eval(re.responseText); document.getElementById('$LIKE_SPINNER_ID').innerText = ''; document.getElementById('$LIKE_STATUS_ID').innerText = ''; diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/ChallengePost.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/ChallengePost.kt index 0a58d6cd4..06fd1bfba 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/ChallengePost.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/ChallengePost.kt @@ -19,6 +19,7 @@ package com.github.readingbat.posts import com.github.pambrose.common.util.encode import com.github.pambrose.common.util.isNotNull +import com.github.pambrose.common.util.maxLength import com.github.pambrose.common.util.md5Of import com.github.pambrose.common.util.pathOf import com.github.pambrose.common.util.toDoubleQuoted @@ -76,6 +77,7 @@ import org.joda.time.DateTimeZone internal data class StudentInfo(val studentId: String, val firstName: String, val lastName: String) +@Suppress("unused") internal data class ClassEnrollment(val sessionId: String, val students: List = mutableListOf()) @@ -93,6 +95,7 @@ internal class LikeDislikeInfo(val userId: String, fun toJson() = Json.encodeToString(serializer(), this) } +@Suppress("unused") @Serializable internal class DashboardInfo(val userId: String, val complete: Boolean, @@ -304,7 +307,7 @@ internal object ChallengePost : KLogging() { throw RedirectException("$path?$MSG=${"Answers cleared".encode()}") } - suspend fun PipelineCall.likeDislike(content: ReadingBatContent, user: User?) { + suspend fun PipelineCall.likeDislike(user: User?) { val paramMap = call.paramMap() val names = ChallengeNames(paramMap) //val challenge = content.findChallenge(names.languageName, names.groupName, names.challengeName) @@ -342,8 +345,7 @@ internal object ChallengePost : KLogging() { val invokeList = userResponses.indices .map { i -> - val userResponse = - paramMap[RESP + i]?.trim() ?: error("Missing user response") + val userResponse = paramMap[RESP + i]?.trim()?.maxLength(256) ?: error("Missing user response") funcInfo.invocations[i] to userResponse } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/CreateAccountPost.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/CreateAccountPost.kt index a5186d141..e7f04eec7 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/CreateAccountPost.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/CreateAccountPost.kt @@ -29,7 +29,6 @@ import com.github.readingbat.common.Message.Companion.EMPTY_MESSAGE import com.github.readingbat.common.User.Companion.createUser import com.github.readingbat.common.UserPrincipal import com.github.readingbat.common.browserSession -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.pages.CreateAccountPage.createAccountPage import com.github.readingbat.server.Email import com.github.readingbat.server.Email.Companion.getEmail @@ -72,7 +71,7 @@ internal object CreateAccountPost : KLogging() { else -> EMPTY_MESSAGE } - suspend fun PipelineCall.createAccount(content: ReadingBatContent): String { + suspend fun PipelineCall.createAccount(): String { val params = call.receiveParameters() val fullName = params.getFullName(FULLNAME_PARAM) val email = params.getEmail(EMAIL_PARAM) @@ -80,22 +79,16 @@ internal object CreateAccountPost : KLogging() { val confirmPassword = params.getPassword(CONFIRM_PASSWORD_PARAM) return when { - fullName.isBlank() -> createAccountPage(content, defaultEmail = email, msg = EMPTY_NAME_MSG) - email.isBlank() -> createAccountPage(content, defaultFullName = fullName, msg = EMPTY_EMAIL_MSG) + fullName.isBlank() -> createAccountPage(defaultEmail = email, msg = EMPTY_NAME_MSG) + email.isBlank() -> createAccountPage(defaultFullName = fullName, msg = EMPTY_EMAIL_MSG) email.isNotValidEmail() -> - createAccountPage(content, - defaultFullName = fullName, - defaultEmail = email, - msg = INVALID_EMAIL_MSG) + createAccountPage(defaultFullName = fullName, defaultEmail = email, msg = INVALID_EMAIL_MSG) else -> { val passwordError = checkPassword(password, confirmPassword) if (passwordError.isNotBlank) - createAccountPage(content, - defaultFullName = fullName, - defaultEmail = email, - msg = passwordError) + createAccountPage(defaultFullName = fullName, defaultEmail = email, msg = passwordError) else - createAccount(content, fullName, email, password) + createAccount(fullName, email, password) } } } @@ -109,15 +102,12 @@ internal object CreateAccountPost : KLogging() { .first() > 0 } - private fun PipelineCall.createAccount(content: ReadingBatContent, - name: FullName, - email: Email, - password: Password): String { + private fun PipelineCall.createAccount(name: FullName, email: Email, password: Password): String { createAccountLimiter.acquire() // may wait // Check if email already exists return if (emailExists(email)) { - createAccountPage(content, msg = Message("Email already registered: $email")) + createAccountPage(msg = Message("Email already registered: $email")) } else { // Create user diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/PasswordResetPost.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/PasswordResetPost.kt index 27ee5fffd..4f82439b5 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/posts/PasswordResetPost.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/posts/PasswordResetPost.kt @@ -33,7 +33,6 @@ import com.github.readingbat.common.User.Companion.isNotRegisteredEmail import com.github.readingbat.common.User.Companion.queryUserByEmail import com.github.readingbat.common.isNotValidUser import com.github.readingbat.common.isValidUser -import com.github.readingbat.dsl.ReadingBatContent import com.github.readingbat.dsl.isDbmsEnabled import com.github.readingbat.pages.PasswordResetPage.passwordResetPage import com.github.readingbat.posts.CreateAccountPost.checkPassword @@ -62,7 +61,7 @@ internal object PasswordResetPost : KLogging() { private val unknownUserLimiter = RateLimiter.create(0.5) // rate 2.0 is "2 permits per second" private val unableToSend = Message("Unable to send password reset email -- missing email address", true) - suspend fun PipelineCall.sendPasswordReset(content: ReadingBatContent): String { + suspend fun PipelineCall.sendPasswordReset(): String { val email = call.receiveParameters().getEmail(EMAIL_PARAM) val remoteStr = call.request.origin.remoteHost logger.info { "Password change request for $email - $remoteStr" } @@ -70,15 +69,15 @@ internal object PasswordResetPost : KLogging() { return when { user.isNotValidUser() -> { unknownUserLimiter.acquire() - passwordResetPage(content, EMPTY_RESET_ID, Message("Invalid user: $email", true)) + passwordResetPage(EMPTY_RESET_ID, Message("Invalid user: $email", true)) } - email.isBlank() -> passwordResetPage(content, EMPTY_RESET_ID, unableToSend) + email.isBlank() -> passwordResetPage(EMPTY_RESET_ID, unableToSend) email.isNotValidEmail() -> { - passwordResetPage(content, EMPTY_RESET_ID, Message("Invalid email address: $email", true)) + passwordResetPage(EMPTY_RESET_ID, Message("Invalid email address: $email", true)) } isNotRegisteredEmail(email) -> { unknownUserLimiter.acquire() - passwordResetPage(content, EMPTY_RESET_ID, Message("Unknown user: $email", true)) + passwordResetPage(EMPTY_RESET_ID, Message("Unknown user: $email", true)) } else -> { try { @@ -86,8 +85,8 @@ internal object PasswordResetPost : KLogging() { // Lookup and remove previous value if it exists val user2 = queryUserByEmail(email) ?: throw ResetPasswordException("Unable to find $email") - val previousResetId = user2.userPasswordResetId() - user2.savePasswordResetId(email, previousResetId, newResetId) + //val previousResetId = user2.userPasswordResetId() + user2.savePasswordResetId(email, newResetId) logger.info { "Sending password reset email to $email - $remoteStr" } try { @@ -110,13 +109,13 @@ internal object PasswordResetPost : KLogging() { } catch (e: ResetPasswordException) { logger.info { e } val msg = Message("Unable to send password reset email to $email", true) - passwordResetPage(content, EMPTY_RESET_ID, msg) + passwordResetPage(EMPTY_RESET_ID, msg) } } } } - suspend fun PipelineCall.updatePassword(content: ReadingBatContent): String = + suspend fun PipelineCall.updatePassword(): String = try { val params = call.receiveParameters() val resetId = params.getResetId(RESET_ID_PARAM) @@ -155,7 +154,7 @@ internal object PasswordResetPost : KLogging() { throw RedirectException("/?$MSG=${"Password reset for $email".encode()}") } catch (e: ResetPasswordException) { logger.info { e } - passwordResetPage(content, e.resetId, Message(e.msg)) + passwordResetPage(e.resetId, Message(e.msg)) } class ResetPasswordException(val msg: String, val resetId: ResetId = EMPTY_RESET_ID) : Exception(msg) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ConfigureFormAuth.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ConfigureFormAuth.kt index 3fcca86ea..4e4088d40 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ConfigureFormAuth.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ConfigureFormAuth.kt @@ -99,6 +99,7 @@ internal object ConfigureFormAuth : KLogging() { * * This is related to the configureAuthCookie method by virtue of the common `PrincipalType` object. */ + @Suppress("unused") fun Authentication.Configuration.configureSessionAuth() { session(AuthName.SESSION) { challenge { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/GeoInfo.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/GeoInfo.kt index 2cd63485f..0246f517b 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/GeoInfo.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/GeoInfo.kt @@ -40,6 +40,7 @@ import java.util.concurrent.ConcurrentHashMap @Suppress("UNCHECKED_CAST") private val map = if (json.isNotBlank()) gson.fromJson(json, Map::class.java) as Map else emptyMap() + @Suppress("unused") private val ip by map private val continent_code by map private val continent_name by map diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/Installs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/Installs.kt index 9d021a8ab..2b572442a 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/Installs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/Installs.kt @@ -179,21 +179,21 @@ import java.util.concurrent.atomic.AtomicLong exception { cause -> logger.info { "InvalidRequestException caught: ${cause.message}" } respondWith { - invalidRequestPage(ReadingBatServer.content.get(), call.request.uri, cause.message ?: UNKNOWN) + invalidRequestPage(call.request.uri, cause.message ?: UNKNOWN) } } exception { cause -> logger.info { "IllegalStateException caught: ${cause.message}" } respondWith { - errorPage(ReadingBatServer.content.get()) + errorPage() } } exception { cause -> logger.info(cause) { "RedisUnavailableException caught: ${cause.message}" } respondWith { - dbmsDownPage(ReadingBatServer.content.get()) + dbmsDownPage() } } @@ -201,14 +201,14 @@ import java.util.concurrent.atomic.AtomicLong exception { cause -> logger.info(cause) { "Throwable caught: ${cause.simpleClassName}" } respondWith { - errorPage(ReadingBatServer.content.get()) + errorPage() } } status(HttpStatusCode.NotFound) { //call.respond(TextContent("${it.value} ${it.description}", Plain.withCharset(UTF_8), it)) respondWith { - notFoundPage(ReadingBatServer.content.get(), call.request.uri.replaceAfter("?", "").replace("?", "")) + notFoundPage(call.request.uri.replaceAfter("?", "").replace("?", "")) } } } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/Intercepts.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/Intercepts.kt index a4abe1ff4..cde88bcf3 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/Intercepts.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/Intercepts.kt @@ -18,6 +18,7 @@ package com.github.readingbat.server import com.github.pambrose.common.util.isNotNull +import com.github.pambrose.common.util.maxLength import com.github.readingbat.common.BrowserSession.Companion.findSessionDbmsId import com.github.readingbat.common.Constants.STATIC import com.github.readingbat.common.Constants.UNKNOWN_USER_ID @@ -48,10 +49,11 @@ import kotlin.time.TimeSource import kotlin.time.hours import kotlin.time.minutes - internal object Intercepts : KLogging() { val clock = TimeSource.Monotonic val requestTimingMap = ConcurrentHashMap() + + @Suppress("unused") val timer = timer("requestTimingMap admin", false, 1.minutes.toLongMilliseconds(), 1.minutes.toLongMilliseconds()) { requestTimingMap @@ -81,7 +83,6 @@ internal fun Application.intercepts() { // Phase for features. Most features should intercept this phase if (!isStaticCall()) try { - println("**** Attributes: ${call.attributes.allKeys.map { it.toString() }}") val browserSession = call.browserSession if (isSaveRequestsEnabled() && browserSession.isNotNull()) { val request = call.request @@ -111,15 +112,16 @@ internal fun Application.intercepts() { row[userRef] = userDbmsId row[geoRef] = geoDbmsId row[ServerRequests.verb] = verb - row[ServerRequests.path] = path - row[ServerRequests.queryString] = queryString - row[ServerRequests.userAgent] = userAgent + row[ServerRequests.path] = path.maxLength(256) + row[ServerRequests.queryString] = queryString.maxLength(256) + row[ServerRequests.userAgent] = userAgent.maxLength(256) row[duration] = 0 } } } } catch (e: Throwable) { - logger.warn(e) {} +// logger.warn(e) {} + logger.info { "Failure saving request: ${e.message}" } } } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/PostgresTables.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/PostgresTables.kt index d376574f9..c167fe6ed 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/PostgresTables.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/PostgresTables.kt @@ -108,6 +108,7 @@ internal object UserAnswerHistory : LongIdTable("user_answer_history") { val historyJson = text("history_json") } +@Suppress("unused") internal object Classes : LongIdTable("classes") { val created = datetime("created") val updated = datetime("updated") diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ReadingBatServer.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ReadingBatServer.kt index 84560744a..edcc8dc71 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ReadingBatServer.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ReadingBatServer.kt @@ -24,15 +24,14 @@ import com.github.pambrose.common.util.Version.Companion.versionDesc import com.github.pambrose.common.util.getBanner import com.github.pambrose.common.util.isNotNull import com.github.pambrose.common.util.isNull -import com.github.pambrose.common.util.maskUrlCredentials import com.github.pambrose.common.util.randomId import com.github.readingbat.common.Constants.REDIS_IS_DOWN -import com.github.readingbat.common.Constants.UNASSIGNED import com.github.readingbat.common.Constants.UNKNOWN_USER_ID import com.github.readingbat.common.Endpoints.STATIC_ROOT import com.github.readingbat.common.EnvVar import com.github.readingbat.common.Metrics import com.github.readingbat.common.Property +import com.github.readingbat.common.Property.Companion.assignProperties import com.github.readingbat.common.User.Companion.createUnknownUser import com.github.readingbat.common.User.Companion.userExists import com.github.readingbat.dsl.* @@ -40,6 +39,7 @@ import com.github.readingbat.readingbat_core.BuildConfig import com.github.readingbat.server.Installs.installs import com.github.readingbat.server.Locations.locations import com.github.readingbat.server.ReadingBatServer.adminUsers +import com.github.readingbat.server.ReadingBatServer.assignKotlinScriptProperty import com.github.readingbat.server.ReadingBatServer.content import com.github.readingbat.server.ReadingBatServer.contentReadCount import com.github.readingbat.server.ReadingBatServer.logger @@ -73,9 +73,10 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import kotlin.time.TimeSource import kotlin.time.measureTime +import kotlin.time.minutes import kotlin.time.seconds -@Version(version = BuildConfig.APP_VERSION, date = BuildConfig.APP_RELEASE_DATE) +@Version(version = BuildConfig.CORE_VERSION, date = BuildConfig.CORE_RELEASE_DATE) object ReadingBatServer : KLogging() { private val startTime = TimeSource.Monotonic.markNow() internal val serverSessionId = randomId(10) @@ -85,6 +86,8 @@ object ReadingBatServer : KLogging() { internal val contentReadCount = AtomicInteger(0) /*internal*/ val metrics by lazy { Metrics() } internal var redisPool: JedisPool? = null + private const val CALLER_VERSION = "callerVersion" + internal var callerVersion = "" internal val dbms by lazy { Database.connect( HikariDataSource( @@ -106,13 +109,14 @@ object ReadingBatServer : KLogging() { maximumPoolSize = Property.DBMS_MAX_POOL_SIZE.getRequiredProperty().toInt() isAutoCommit = false transactionIsolation = "TRANSACTION_REPEATABLE_READ" + maxLifetime = Property.DBMS_MAX_LIFETIME_MINS.getRequiredProperty().toInt().minutes.toLongMilliseconds() validate() })) } internal val upTime get() = startTime.elapsedNow() - fun assignKotlinSciptProperty() { + fun assignKotlinScriptProperty() { // If kotlin.script.classpath property is missing, set it based on env var SCRIPT_CLASSPATH // This has to take place before reading DSL val scriptClasspathProp = Property.KOTLIN_SCRIPT_CLASSPATH.getPropertyOrNull() @@ -128,11 +132,18 @@ object ReadingBatServer : KLogging() { } } - fun configEnviroment(arg: String) = commandLineEnvironment(withConfigArg(arg)) + @Suppress("unused") + fun configEnvironment(arg: String) = commandLineEnvironment(withConfigArg(arg)) - fun withConfigArg(arg: String) = deriveArgs(arrayOf("-config=$arg")) + private fun withConfigArg(arg: String) = deriveArgs(arrayOf("-config=$arg")) - fun deriveArgs(args: Array): Array { + private fun callerVersion(args: Array) = + args.asSequence() + .filter { it.startsWith("-$CALLER_VERSION=") } + .map { it.replaceFirst("-$CALLER_VERSION=", "") } + .firstOrNull() ?: "None specified" + + private fun deriveArgs(args: Array): Array { // Grab config filename from CLI args and then try ENV var val configFilename = args.asSequence() @@ -151,34 +162,20 @@ object ReadingBatServer : KLogging() { args.toMutableList().apply { add("-config=$configFilename") }.toTypedArray() } - fun start(args: Array) { + fun start(callerVersion: String, args: Array) { + start(args + arrayOf("-$CALLER_VERSION=$callerVersion")) + } + + fun start(args: Array) { logger.apply { info { getBanner("banners/readingbat.txt", this) } info { ReadingBatServer::class.versionDesc() } + callerVersion = callerVersion(args) + info { "Caller Version: $callerVersion" } } - logger.info { "${EnvVar.REDIS_URL}: ${EnvVar.REDIS_URL.getEnv(UNASSIGNED).maskUrlCredentials()}" } - - assignKotlinSciptProperty() - val environment = commandLineEnvironment(deriveArgs(args)) - - // Reference these to load them - ScriptPools.javaScriptPool - ScriptPools.pythonScriptPool - ScriptPools.kotlinScriptPool - - redisPool = - if (isRedisEnabled()) - try { - RedisUtils.newJedisPool().also { logger.info { "Created Redis pool" } } - } catch (e: JedisConnectionException) { - logger.error { "Failed to create Redis pool: $REDIS_IS_DOWN" } - null // Return null - } - else null - embeddedServer(CIO, environment).start(wait = true) } } @@ -210,12 +207,22 @@ internal fun Application.readContentDsl(fileName: String, variableName: String, contentReadCount.incrementAndGet() } -/*internal*/ fun Application.module(testing: Boolean = false) { +/*internal*/ fun Application.module() { assignProperties() adminUsers.addAll(Property.ADMIN_USERS.configValueOrNull(this)?.getList() ?: emptyList()) + ReadingBatServer.redisPool = + if (isRedisEnabled()) + try { + RedisUtils.newJedisPool().also { logger.info { "Created Redis pool" } } + } catch (e: JedisConnectionException) { + logger.error { "Failed to create Redis pool: $REDIS_IS_DOWN" } + null // Return null + } + else null + // Only run this in production if (isProduction() && isRedisEnabled()) PubSubCommandsWs.initThreads() @@ -242,6 +249,8 @@ internal fun Application.readContentDsl(fileName: String, variableName: String, // This is done *after* AGENT_LAUNCH_ID is assigned because metrics depend on it metrics.init { content.get() } + assignKotlinScriptProperty() + val dslFileName = Property.DSL_FILE_NAME.getRequiredProperty() val dslVariableName = Property.DSL_VARIABLE_NAME.getRequiredProperty() @@ -280,53 +289,3 @@ internal fun Application.readContentDsl(fileName: String, variableName: String, static(STATIC_ROOT) { resources("static") } } } - -private fun Application.assignProperties() { - - val agentEnabled = - EnvVar.AGENT_ENABLED.getEnv(Property.AGENT_ENABLED.configValue(this, default = "false").toBoolean()) - Property.AGENT_ENABLED.setProperty(agentEnabled.toString()) - Property.PROXY_HOSTNAME.setPropertyFromConfig(this, "") - - Property.IS_PRODUCTION.setProperty(Property.IS_PRODUCTION.configValue(this, "false").toBoolean().toString()) - - Property.DBMS_ENABLED.setProperty(Property.DBMS_ENABLED.configValue(this, "false").toBoolean().toString()) - Property.SAVE_REQUESTS_ENABLED.setProperty(Property.SAVE_REQUESTS_ENABLED.configValue(this, "true").toBoolean() - .toString()) - Property.MULTI_SERVER_ENABLED.setProperty(Property.MULTI_SERVER_ENABLED.configValue(this, "false").toBoolean() - .toString()) - Property.CONTENT_CACHING_ENABLED.setProperty(Property.CONTENT_CACHING_ENABLED.configValue(this, "false").toBoolean() - .toString()) - - Property.DSL_FILE_NAME.setPropertyFromConfig(this, "src/Content.kt") - Property.DSL_VARIABLE_NAME.setPropertyFromConfig(this, "content") - - Property.ANALYTICS_ID.setPropertyFromConfig(this, "") - - Property.PINGDOM_BANNER_ID.setPropertyFromConfig(this, "") - Property.PINGDOM_URL.setPropertyFromConfig(this, "") - Property.STATUS_PAGE_URL.setPropertyFromConfig(this, "") - - Property.PROMETHEUS_URL.setPropertyFromConfig(this, "") - Property.GRAFANA_URL.setPropertyFromConfig(this, "") - - Property.JAVA_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") - Property.KOTLIN_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") - Property.PYTHON_SCRIPTS_POOL_SIZE.setPropertyFromConfig(this, "5") - - Property.DBMS_DRIVER_CLASSNAME.setPropertyFromConfig(this, "com.impossibl.postgres.jdbc.PGDriver") - Property.DBMS_URL.setPropertyFromConfig(this, "jdbc:pgsql://localhost:5432/readingbat") - Property.DBMS_USERNAME.setPropertyFromConfig(this, "postgres") - Property.DBMS_PASSWORD.setPropertyFromConfig(this, "") - Property.DBMS_MAX_POOL_SIZE.setPropertyFromConfig(this, "10") - - Property.REDIS_MAX_POOL_SIZE.setPropertyFromConfig(this, "10") - Property.REDIS_MAX_IDLE_SIZE.setPropertyFromConfig(this, "5") - Property.REDIS_MIN_IDLE_SIZE.setPropertyFromConfig(this, "1") - - Property.KTOR_PORT.setPropertyFromConfig(this, "0") - Property.KTOR_WATCH.setProperty(Property.KTOR_WATCH.configValueOrNull(this)?.getList()?.toString() ?: UNASSIGNED) - - Property.SENDGRID_PREFIX.setProperty( - EnvVar.SENDGRID_PREFIX.getEnv(Property.SENDGRID_PREFIX.configValue(this, "https://www.readingbat.com"))) -} diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ScriptPools.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ScriptPools.kt index d9b2b5bef..45b868740 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ScriptPools.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ScriptPools.kt @@ -44,12 +44,12 @@ internal object ScriptPools : KLogging() { } internal val pythonEvaluatorPool by lazy { - PythonExprEvaluatorPool(Property.PYTHON_EVALUATOR_POOL_SIZE.getProperty(5)) + PythonExprEvaluatorPool(Property.PYTHON_EVALUATORS_POOL_SIZE.getProperty(5)) .also { logger.info { "Created Python evaluator pool with size ${it.size}" } } } internal val kotlinEvaluatorPool by lazy { - KotlinExprEvaluatorPool(Property.KOTLIN_EVALUATOR_POOL_SIZE.getProperty(5)) + KotlinExprEvaluatorPool(Property.KOTLIN_EVALUATORS_POOL_SIZE.getProperty(5)) .also { logger.info { "Created Kotlin evaluator pool with size ${it.size}" } } } } \ No newline at end of file diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ServerUtils.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ServerUtils.kt index a4c61eb9a..74fc6c19b 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ServerUtils.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ServerUtils.kt @@ -55,6 +55,7 @@ import redis.clients.jedis.Jedis typealias PipelineCall = PipelineContext +@Suppress("unused") internal object ServerUtils : KLogging() { fun getVersionDesc(asJson: Boolean = false): String = ReadingBatServer::class.versionDesc(asJson) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/AdminRoutes.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/AdminRoutes.kt index afdec5903..52f6e638f 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/AdminRoutes.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/AdminRoutes.kt @@ -155,7 +155,11 @@ import java.time.ZoneId if (isSaveRequestsEnabled()) { val ipAddress = call.request.origin.remoteHost - lookupGeoInfo(ipAddress) + try { + lookupGeoInfo(ipAddress) + } catch (e: Throwable) { + logger.warn(e) {} + } } logger.debug { "Created browser session: ${browserSession.id} - ${call.request.origin.remoteHost}" } diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/UserRoutes.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/UserRoutes.kt index bb8507719..9a0c9060b 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/UserRoutes.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/routes/UserRoutes.kt @@ -104,6 +104,7 @@ import io.ktor.sessions.* import kotlinx.coroutines.withTimeout import kotlin.time.Duration +@Suppress("unused") fun Route.routeTimeout(time: Duration, callback: Route.() -> Unit): Route { // With createChild, we create a child node for this received Route val routeWithTimeout = this.createChild(object : RouteSelector(1.0) { @@ -159,7 +160,7 @@ fun Route.routeTimeout(time: Duration, callback: Route.() -> Unit): Route { } get(PRIVACY_ENDPOINT) { - respondWith { privacyPage(contentSrc()) } + respondWith { privacyPage() } } get(ABOUT_ENDPOINT) { @@ -179,7 +180,7 @@ fun Route.routeTimeout(time: Duration, callback: Route.() -> Unit): Route { } post(LIKE_DISLIKE_ENDPOINT) { - metrics.measureEndpointRequest(LIKE_DISLIKE_ENDPOINT) { likeDislike(contentSrc(), fetchUser()) } + metrics.measureEndpointRequest(LIKE_DISLIKE_ENDPOINT) { likeDislike(fetchUser()) } } post(CLEAR_GROUP_ANSWERS_ENDPOINT) { @@ -191,11 +192,11 @@ fun Route.routeTimeout(time: Duration, callback: Route.() -> Unit): Route { } get(CREATE_ACCOUNT_ENDPOINT, metrics) { - respondWith { createAccountPage(contentSrc()) } + respondWith { createAccountPage() } } post(CREATE_ACCOUNT_ENDPOINT) { - respondWithSuspendingRedirect { createAccount(contentSrc()) } + respondWithSuspendingRedirect { createAccount() } } get(ADMIN_PREFS_ENDPOINT) { @@ -261,16 +262,16 @@ fun Route.routeTimeout(time: Duration, callback: Route.() -> Unit): Route { } post(PASSWORD_CHANGE_ENDPOINT) { - respondWithSuspendingRedirect { updatePassword(contentSrc()) } + respondWithSuspendingRedirect { updatePassword() } } post(PASSWORD_RESET_ENDPOINT) { - respondWithSuspendingRedirect { sendPasswordReset(contentSrc()) } + respondWithSuspendingRedirect { sendPasswordReset() } } // RESET_ID is passed here when user clicks on email URL get(PASSWORD_RESET_ENDPOINT, metrics) { - respondWith { passwordResetPage(contentSrc(), ResetId(queryParam(RESET_ID_PARAM))) } + respondWith { passwordResetPage(ResetId(queryParam(RESET_ID_PARAM))) } } get(LOGOUT_ENDPOINT, metrics) { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClassSummaryWs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClassSummaryWs.kt index a26e91efc..1f05bed01 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClassSummaryWs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClassSummaryWs.kt @@ -69,17 +69,16 @@ internal object ClassSummaryWs : KLogging() { val content = contentSrc.invoke() val p = call.parameters - val languageName = - p[LANGUAGE_NAME]?.let { LanguageName(it) } ?: throw InvalidRequestException("Missing language") + val langName = p[LANGUAGE_NAME]?.let { LanguageName(it) } ?: throw InvalidRequestException("Missing language") val groupName = p[GROUP_NAME]?.let { GroupName(it) } ?: throw InvalidRequestException("Missing group name") val classCode = p[CLASS_CODE]?.let { ClassCode(it) } ?: throw InvalidRequestException("Missing class code") - val challenges = content.findGroup(languageName, groupName).challenges + val challenges = content.findGroup(langName, groupName).challenges val user = fetchUser() ?: throw InvalidRequestException("Null user") //val email = user.email //val remote = call.request.origin.remoteHost //val desc = "${pathOf(WS_ROOT, CLASS_SUMMARY_ENDPOINT, languageName, groupName, classCode)} - $remote - $email" - validateContext(languageName, groupName, classCode, null, user) + validateContext(langName, groupName, classCode, null, user) incoming .consumeAsFlow() @@ -90,9 +89,9 @@ internal object ClassSummaryWs : KLogging() { for (challenge in challenges) { val funcInfo = challenge.functionInfo() val challengeName = challenge.challengeName - val numCalls = funcInfo.invocationCount - var likes = 0 - var dislikes = 0 + //val numCalls = funcInfo.invocationCount + //var likes = 0 + //var dislikes = 0 for (enrollee in enrollees) { var incorrectAttempts = 0 @@ -100,7 +99,7 @@ internal object ClassSummaryWs : KLogging() { val results = mutableListOf() for (invocation in funcInfo.invocations) { transaction { - val historyMd5 = md5Of(languageName, groupName, challengeName, invocation) + val historyMd5 = md5Of(langName, groupName, challengeName, invocation) if (enrollee.historyExists(historyMd5, invocation)) { results += enrollee.answerHistory(historyMd5, invocation) @@ -155,6 +154,7 @@ internal object ClassSummaryWs : KLogging() { } } + @Suppress("unused") @Serializable class ClassSummary(val userId: String, val challengeName: String, diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClockWs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClockWs.kt index 22768e16f..db865991a 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClockWs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/ClockWs.kt @@ -38,6 +38,7 @@ import kotlin.math.max import kotlin.time.TimeSource import kotlin.time.seconds +@Suppress("unused") internal object ClockWs : KLogging() { private val clock = TimeSource.Monotonic private val wsConnections = Collections.synchronizedSet(LinkedHashSet()) diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/StudentSummaryWs.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/StudentSummaryWs.kt index f5a6bf3e2..d7a7103bf 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/StudentSummaryWs.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/server/ws/StudentSummaryWs.kt @@ -91,9 +91,9 @@ internal object StudentSummaryWs : KLogging() { val funcInfo = challenge.functionInfo() val groupName = challengeGroup.groupName val challengeName = challenge.challengeName - val numCalls = funcInfo.invocationCount - var likes = 0 - var dislikes = 0 + //val numCalls = funcInfo.invocationCount + //var likes = 0 + //var dislikes = 0 var incorrectAttempts = 0 var attempted = 0 @@ -157,6 +157,7 @@ internal object StudentSummaryWs : KLogging() { } @Serializable + @Suppress("unused") class StudentSummary(val groupName: String, val challengeName: String, val results: List, diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/test_content/RedisSessionStorage.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/test_content/RedisSessionStorage.kt index 962dbf762..39d666a82 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/test_content/RedisSessionStorage.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/test_content/RedisSessionStorage.kt @@ -23,6 +23,7 @@ import kotlin.time.Duration import kotlin.time.seconds +@Suppress("unused") class RedisSessionStorage(val redis: Jedis, private val prefix: String = "session_", private val ttl: Duration = 3600.seconds) : SimplifiedSessionStorage() { diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/utils/RedisAdmin.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/utils/RedisAdmin.kt index bbb514374..7c9146d99 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/utils/RedisAdmin.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/utils/RedisAdmin.kt @@ -23,6 +23,7 @@ import com.github.pambrose.common.redis.RedisUtils.withRedis import com.github.pambrose.common.util.isNotNull import redis.clients.jedis.exceptions.JedisDataException +@Suppress("unused") internal object RedisAdmin { internal const val docean = "" diff --git a/readingbat-core/src/main/kotlin/com/github/readingbat/utils/ScriptTest.kt b/readingbat-core/src/main/kotlin/com/github/readingbat/utils/ScriptTest.kt index cc35d0d5b..fb79e3a14 100644 --- a/readingbat-core/src/main/kotlin/com/github/readingbat/utils/ScriptTest.kt +++ b/readingbat-core/src/main/kotlin/com/github/readingbat/utils/ScriptTest.kt @@ -49,6 +49,7 @@ fun main() { } } +@Suppress("unused") fun javaTest() { val correctAnswers = mutableListOf() val script = """ @@ -93,6 +94,7 @@ fun javaTest() { } +@Suppress("unused") fun pythonTest() { val correctAnswers = mutableListOf() @@ -133,6 +135,8 @@ answers.add(less_than(11, 28)) val ScriptEngine.bindings: Bindings get() = getBindings(ScriptContext.ENGINE_SCOPE) + +@Suppress("unused") val ScriptContext.bindings: Bindings get() = getBindings(ScriptContext.ENGINE_SCOPE) fun ScriptEngine.reset(scope: Int = ScriptContext.ENGINE_SCOPE) { diff --git a/readingbat-core/src/main/resources/application-dev.conf b/readingbat-core/src/main/resources/application-dev.conf index 2eafd7f7f..8d04c1a82 100644 --- a/readingbat-core/src/main/resources/application-dev.conf +++ b/readingbat-core/src/main/resources/application-dev.conf @@ -74,6 +74,7 @@ dbms { username = "postgres" password = "docker" maxPoolSize = 5 + maxLifetimeMins = 10 } agent { diff --git a/readingbat-core/src/main/resources/static/x-ray-glasses-large.jpg b/readingbat-core/src/main/resources/static/x-ray-glasses-large.jpg new file mode 100644 index 0000000000000000000000000000000000000000..814d7931f76cf9bc3e11e192bda84edfce0bbbe2 GIT binary patch literal 49238 zcma&Mbx>U2vM4-*yX)W*+})YMHMj%~n!!D|ySpR=7=}OwmoP|z1($^25Fj|gNg%j> z{LXjIz4wp%UcFVdtJdz_-K)D-udd#``f=fL9YCcV;N%DZsH<}UumJx(9uEP;N`7`O zfdC)??FlRc06eav@^*E9`0PB~_ycS_`33m|_yIEV0iHH?E)Kqo zwhphH++~@6f9_^xbh4LaHWAep(DqbxfI6uKc{>;cJvX!qa^lnIauaPxF? z@U>wKaC3F{kqVGy{ugtpr};l%erCphq4>JUGDH4ZmC;mNmr>Ef+ksJ(PmI@2P(*-H zT#`>vOh80bkcUx7Kv0}tK$KrVj8{NVN~QTJL5kb zZEQXKd}W!RB>n$YaP!pG{x8P=tF_$R{?Y4S*gn4c4*xC2|BCHn80hK1ukYaF;pc7V z@Py9tAL1w5{ofb*2k}W7DJ2g(KQ{+=Uv(u}<|hcBy_3C^qJXfHxP*j=l7f(mprEpZ zq=dMHqN0SPh@hmfq_~3ce`wX+eSB@)?Hv9?>-68W!v9xVDMfDw8($A^Lk|zv|LmSF z)Wg@q2kPO;sAwR{C&;L6W9Q`l59A-?{##ZDZ>QG|_A1^UZjAr3j+E2?B7u;yn5ek0 zgt(*xMEps5A&9uLkff-%g0d1sNkT|ekon)V_Wz6K|E7J?`2Rr5|0E3mKU@1hw)#Jy zC;$0p`d^Ow^zvV>@8JIA@7_<2{df!@_?No^?0cRL5b$vc5CgzKLqkJH!$3#Jz{Yrb zaIr8juyFBkaBy*O@QCpKHHh#C2#E*@@JLC?$Vf?PX=rF@8UAYkF)=am@$jjLh^Rp1 z#N?p=`tbicdF%lYV*%GtB2a%Dlzb}ACUAUA36{S{O|LR0z^ea$G`+&Jwg8` z{2xtFP>BFf-%(Lf&{5FPQBlzVm`^YOF&YyIx*!BYkJQGCSqL+dEVERBrSXftE%~H3 z>$)(-pQvY8CyHPU>ypixMh4gd#!Hq7}vb(t7)dfs6ZFEkY-CUO=O-Xn2_*l-Dpot`m zy%X3YCbl_U-sRU0Y=+`ALC5a6bd~)d0S|qXak)fwBDl@G%_vE)d8J)k!g5HMQe8E1 zvmeL^&#up_qAq(m`COw7{?u}31qv-$QD7FTUET>XyKBv1zyn(f{Pb?AurUx9b6EIMX0HI=Pa!B z07=WsY}~n6RYvEXt`EZgb4|~pruvbKE7I1aR?mSl)9d-hc2045#zmFwJn3-2yk|Y{ z=VQhsHlD`oaTeCx``!hs+xQRTaa7}vfW+Y_ZM%&7MH&|^L9+r2iJ2Okc-z%;kv`If zwqwy>#wg4!tmw>_6;uuPX;hS^*;lB_JQnV_xKu=|v&w*Pn;mF-Eg>AkQc#$ambRg{ zd}@fIi1kI3@T$jY1!%-aK3bwZS`4piDHA4zW9kC@_Q0~!w)e)sH4}ju)_H!U_@d@F z)mEc!M-oo7oy$3Vv@#3B3cfm;jF;)gjW6?)gPO&R&lzcSj4^%r4l z%EF0hyFVo1OHSl5ifvbs{DHa_g@5MBLcJW`X~MHScH)nPsD4L=KI539W#88RHVT2J zZM0h+0sH#Cq8WePY}#m|_y~LEpf*##X7CGo-(r*}nTBrrhowlWcL zpl)Q?43zI%FhLHAHp=1*j#?)GiNF{;g<;S)qlmztWrjj!6fLs7{C?b6oe}h_NvqAq z&OJm$`~XX*Ln`5{xNg&(%&@_viGbBu| zjBt-q*O6`0b-4yQgO~>T8Lp9$n3|X5WThIpEoG(hr|fiHKN7-!$A9P* zz4dkS)}i2NF*HnMGMe^GI>O80_e-7YIT5v?)91|8xFU`aH@?nCfEhHL{hJpR|3{$M zftPdSR~^ZrB&CdvM(!m|T~p0#0V0|U|5lG!5oyA0*O>NXT&vA>Z9>)SGG4>uUVV?b zvl*N)*B$cYkp$V#PUW#ssgier7(>u;l)UaK4;zmJM8(iA0&m^^mz5DTEkA>TCL}R` zg}syv6lg7PZrbIAaPf~XSJTDgQ`eW#VP8-5M_+$`6VKptqIdf&eGYGun?`k){k#B9h*Np5q+&i7Bt0M^?d%$F=){Zf%0n-0$CY#*t z=6a9`NtnO*M&=fJ8tmA+m;^0iC=Qb>V&&zE#q^)Sc?1;cj;rQBNMEA4h#lR}WT2zh zQ^|<4Lx;vK>u{qOEaiMbcrtkCgI)9nO%@B$pUs4gxzi7a{A|_dwpJpx2aCXbLCJ_# z=gNqc-afZ`Z3B#2>d91uErmXK|H}eRp{poo$|dJU+Z$aeA^gHt>&gQK$?mKNu_A!0 z+NH3dO(=i?l(b`!PMIBdgm-cn)_r5_Rc`)ax?E82y^2+tju{XMVMnpU>jeXm$Am)~ z1kr1Ort-+G#%HOT--xr=m~W;CqOX!i1+MC;wz|Wg#dwpZlzRA_AYIU{Gq21jMJDQ7SFf|n<$!iO!od5gB3N=gVnW}H;+UFy+<54=bn zxK^V2OVZX+!vOAz?U}0Jwz*cYsBsOk(xJL_OITui>2_9pZclqKA~P=Mjy0&x&Q0e= zmiS>ReD(lhe)Tsq<=SV%zEQwi%@%OBf;knQi>bQtCXTG&@4=gNZsliPlXeEEOW1Se zk*)Zj*WJSWP+heLbMTw)EjiF*zwN?o-N0k5nz&B&gGt1}R!NL~?#-%^WLSfdExbVh zUT<`i3DaBuD?POS&z5H`ISTjB<{JUA*SeA|`;Zy>W}H?jf*As!O|9t%yF{}ug;4n! zD6w(xjACY#cHD<@)RxA3KRE4DSW#6v@wq8Yx>;$zrL*oj+Z>|QPf(?a>}|=E zrd|+hMCr)(lcOf5Bs)Ou)CxOnck#Q+BOsN0s3Tqa@@`y3q3hgHny~khJk`j&CiR>S z{97AEulB)rQdk<>OVEyhS0qA!zN{1dr6m*(pMy5Fz?Y2jE?i=dC>EA@d1T;)z+H)u zd~Qqpmes;n*Dqjxx*WlAN+pvrEEZ?0aD!^h@;;#TQ1Uz7(58^rq__KA2EG~tXol*! zV!A7zimQB^Qor?jFiwtZKuT=?AB$Y#Xqm6WKuR~A6GY9|H&Ye-4bf1>c(m}jgxEnl znf4nLjk;}=q_(Z?ZzG$7c!I}+sWVx?uD^?sd7HElG7WK^)+1D_9EuKxXh(0JCWqD| zrgS}SU+z#SpD_G{55+Id;l4lOI5ncp;SdV?c2_$_G?7|Tz6{9Zer-n>w>$Ds5GQs39R_Db1jaEK(m{dMglptlM&b1j6QPH6}? zq~(%b8{ShK6^*}jRd9McY3rq#)~OfK$T(s5tT&sCw{;M6N|eQVs=Tz|y6zPgPr#eM za6TA@Sogt;naak%M?irRk0iAk%3|TP9(+??DzJh!`S{ZoJeJ1VlA^B0*{l0LGZ3fF z1ca#g@>2)0oYeW0lHmRwiXEZ4oKjmTq>0ovJgk?!3oT-9I;El4?hG^Xdz(#aEB8zxd@;63wv;~8Q) z@6^JF#abtWPIXV$XbfZBZV8F@LYD)nEJ}0uQq_Hiaz%2CN2W_Mx1~qBoTU;|Qy*XC z$LSxZ%!H;yuMg5u=6au+L1C)#g^8|~pM|GQOYW7Pav$ioR}PYm$&JVxfqg9avp`JA z%#o8u)}4^HHNem@qk&RA&FOraJIha+_nQ?IVYliPY3J8?sv`l%dIr!YFQwt|BT7-# zaW<1i%A@9z2$)Oyw-C8kglxjE(v%o)LJjGrX%Bx1ZMwG$Rw$HM_U((1ie)a9Yf!&L zAE}8eK;h#6oXsI@3Q#|2B%z4Fpa`~P`Xw7>%-)2jymC<&qZzMoCE~Y@4+KiTqanns z7hCK22rxofxiDY8xYh*~lE^}7ahLU;)0Lwv?RSn`8AbQ*16-DMB{DR^N+!(&ko@*UgEaRB-3 z%#z}jKe}#}QQgSCCjC0JusA;eOVgUIEuX=qR5yqFB~4#0OJ$A6m#4ZK`_Rf+S!Zuk z86FDJO_?jpDw7%qE=)hHGBD8Grei$Vb9d*2yd4{sX$=--{>+zIQDmff;DI5{;#oAF6s$mh5_Qg@ zMgC?})bJv-7c#<$MJ9B56`Afvzh`E@k!Oc-7P6_3G${fMkZJD?a%zrsq7@U^BGWEB z;9~UfccrCk%l6gHYIdFg@tN$EcPXpSHr`OZ(I&|{xMu*_?&q^Gu+WWa#bTXTa274z zn8ETBzj_Dg2N$#pDJ`as_S4XRnsC1A9$PVrC=Xo?DFhyqED{!Bl*;pZEf13Pjcg22 zKsuMER`SnlF+R493ug{#y_3}Q$Nx&vAGx5PlFm6eLh@1Qo~efQ93lL-sP*TET4e>8 z>6XJw$VXJK8(kgA(@&hQSMISzu-Pn9M%R8swzG6vYN(7B73A4Mj+6q%ztC*F*Hs?M zD0QKsarz6p@fo;A|J6-*QqOex%(OExJ=k0qn)@X%C4BKTu%O3$F*D*pyc%BA{8pS! zk@JCU@{9t9=ZA%ro%n2i40`t=xzf%{Y*b5UGnOu7Mjs~`-l#SzbN_)dsi_lq8W%*A zKYfRo#6l9UgO_`jGzk6l4!~nBe_;wW81SAm*hi+pndkLWDaBlu$3W@ z^*#GKI4A=APzO&E)kl8>4dt)ZztXSv2H9bSkK?W|%tqX32MMDM zrJXoQuwS%qq=}g8LmC(eNPK;l@i+K2UjIk4pHh0FuZ9BNdAB(c4e}pLwCc+`;|6s2 zcuyeR5Wf&wi%`CfcXtGezBZe*5j!iQLQ~)3cgY4~@7c zIrYRsj?5FAOS)RR+Fc%P{Fg#)?@qGWKciw=QkhS|_l?;pp`jxdl{QsNA5O}YOOgn2 zH4_mjBx{$gjMd@rk0StZJFcNJsY_s{-!|;dvMOOrdpxesC6*V(J{ly(`DC#@#@kC>rfB zYAu7%{57m~;B=FTcxRAacD}Q|9PO|J4{wGA$0$!-u9Wg6hwySdogC{Dt7UB5Q8Up3 z5x*U{YE>JhvYkN+n*a=_=7Z9(>~~^%+!24pOQXzM-|}WvF^RFh(J|NKE)J~fpf?!h z6l1FQNDsn05M-%Muf;Swsb{@x>t+Z94Pu0tu-7(|`?|jZsHr`a21Q0eXCOOL$4=l6a|LL{C*6R34nXD6R04*z2FB0yupG+IhJ+tlH78N3 zDy+R%janvkT6T;T6`a(lpn`laO9hO6szW5@GZsfUfiCIS;Daq8 z&X64W!!JcjnFGdWk)_DUN@h%k#B{b-J)<#TGNz%1^Q*UW!nLp2>Bj9g3s~(li5A5u zx+b`KlKbpil-#(gIne9p5N$&nKj%1xvCMB{mf&~F=y0!88aK>a9zM)qU30~leb}BY zUl;=pnrZ5{!Rkx%_A3L_uGNd4dnZ5>2g=5N#YGjzH5GAFArwFZVs7w4kzmi&-uij} z{@8FI`Zk>~RXOLQy;hl516r!c!D5-#BS3o7v~g6+tF{0Tv#L{s_%yon=ZDAHW~EXi z0YFR?-()kO=A%n4XF~J^aou8E9eaFw&k%PKddt1O-&~Bzz_2o2bynjDPlEf`uwLip zqDR2E5SwCL>{WBq+US*S>{^p3>` z&>n62@y2nx!6|HE{o%!nw68hRV5y6Vx!5_%EqYEP)~pXNIESaJq>5uRT37p1WC`= z*CkLostK~{n1ZWc(#^la{ip5`)VZDbsQz;^3N%Rs@mJOon}NOp$4m73hh8T~N{Es? z!Oi4vKC&=yurTR?lHK_^ym>`3V-sjJOgxZLs6MsyY2tO8y(DdzoW{MzNpkB@IlyAE z1_jIS@wSXMX$d_F!wP#UDC0Xv>>f-p;H#=TUXp7BSb9Ni$Iw2#iZr$ogVaL}=M z!JhY*R@HY12Auwdb6KvQomMJ48?Wj8gP;l1vi2s_ryG|)@b>^W_Zje~C|6&rM*x?w zPAbLP&tN@UnZx6hN|z#T|66NZ`c+xn$Tr%b7GHHBg%=IQW5@lkfRoh*F6%hth!O{a8V-SvqnLh}k%&A@^(SE?BZax-VM3{L)HI%Clc;gF4AUr^Ag3C_gL$G9iR z(bPKaqKkUz5UEj{cJgWFneE!iTyL?d5#Me4oL|FPr*XN_-v?l`yO*i zWy5Pt%=z|>WZCy}NTBlOv_zu;^-}pGU>3r{O^U70gyW1cFIOWq8<`hdX4&mr1gxr< z7D@PFx~K>jAR~coJtKB7U!uw{*3Px}P$E(F;w*?60*&YPCNO}T8>A^vO*FXS{YR^R@Vi7tQ zE|Q%ynD%Bl3Po3rNsXH`;3XIidWMK$i=eA7N8h8=dmvX#u8l=YP&b}j zn<$9|a+9O+MWluqLLzFCR1M~G6`S)Z_g0z+*SPgD;b5*W%(5`)23E#4)Y{+9W#6c~ z;x=o)0jr}h`YxOdr7AXNFdE~KH}uU43*Q zEXwk;9qnhclp%R_Vk+sFiw1Qvrq~31RJ7!Gyb#;P7Yr(0@FTqn8a5g0W%+a5UF91+ zy+uC;E#h#m)JPG^71>0dAAjQJSXqhxf+j`hI!rJWv9WlPG-B^L-p)PDP|1FTF>-?n z;y2Tk4B%%NNMMSuX6y`M>$X`UT!l;CjB^(Hp2_?e7Q77pqGK?92J=E%a?qg6pxc3C zkIGRFS7LnIVCj4U1k}%AW>qpoX`LLhJZjqWdW)r6%_d!Mi;;r4A#H~ri72*_6lXAz zmzkE?nm-VTDZ9o-?w2=7b{czk!K=CN0JzQ`2U^74R~g35=^kg9bmYUwi&c`^82N-?_})$;h~j6x1jhP(#I zCSbQe?0D?sJ*(-Ah{UqRRFvEle(mbDp=s_d=1_z3=k|Rl%5wrIh;0Jx%_Cq>SFmuS zv4UxMS)8p2v3Fw}>%qNw`XxPCfZ-9~%@;Md2hq+Qa@hEGG)8UkQu&^s8Bc`7v}M|6 zX3mH%Zx#jB8I$qNM(Eh8%2g6>2!w1Nuzd8i@5DF(#gyEPAN?Lg6#d)bIg3ecf0n{e z(|A@Z6wI&Y;3kl}OVkhi5&f20*6QUBqh_k)Zh@5RZ#vZ7J+pcSBML$tZVCSz`uGx^9L&tv&vAZ~prR^l8oUK55(Vvtijbo!y{M5|Tnph~`K6f39GlDM zw|dg8frpvx>4Fm+Fc6cr6oChBEyon!9uE;#PnU@s3olJQ-^bUOF&b0{Fnn$!&oy6v zS#__2Yy}w1&s7=s^<9}{Ip$(4leG20frk5{#?_HQswa} za_omY2PnIIm;7}b%3MRETr<7kUOCBI!7SejAyR_%daIcp`l<9b>$x+19qdf7o;Uu0 zN)}^}I$3#315LDb~tbfQPn+2A(kaO+CY@t2}HK-xOf5l5ChCT0n>k9qXkT5rCD1A1vfa z+@T!WUGMjW1&Qv#(kmuowNXFy!IFb(KHX7;AU1Px%N%hAqxVk{w%L%NxbG;8cZ2PD zY(p^Ltuq&+zxBJx)p`Eu4EELTDe|SspB=!Y^KiDLFloH!LY*I z*4|lNjr7tP>myNMEMrvAMHOoZA}Sdgerc|gt4yujaqC;HDH-^q^jusUff*?}QEIqnQaRA7 zqFyJZ(x&x9?-`;ci8E-fBx!n`@#q3ysG<1oEqzM~taNM>v>&D@3Y>`-(}#OL4;HTH?z1 zm5$urXS+uw7#{?2;)B#TmWPHn)u-)~R8Ok8OpNXZn!@SUn^>d=9@gupOmudPo3_|C zKAK>}L(#lsGM3p2Htcxnw$5}3?~IqS~r zO7T@hQ0XZ^W!*&gz`QLzzlKaxxIqWpKUfa3gr0oDwp#-Fyz~?Ic+t4p-7%B=d zYmO)KZ^`8|dJPk4WTcZ5eJOI_HHActu?Rn(i|(;r2k&QtH9-rx z2MfAnuYbCq5dyuy(mG+qIWu3ll1=qq{N_3>mX7Tg(g+vhwi#o}TI^Hf^A{dNy^4Z7iM6(7LYK_7&?3{ddJAO46S;vzKoSL4^1l*L+eY zgWn^{m;j^*9-V7LhuMHK5<9F-2K~^Tnc{OqGRM)(f#H`dLXFae#3ehWhEqQzkppB7 z@r-#!4|ib_jUWf0&IU6_;X>kMk~nkslZ#am(G)o)s)5-GWK`b1uD}$>NOw~L-1t^u z$(|j7dCSaeRT69OImi>3BaDZh&!7=mMONklX`DZ57koEk0t1I;1Xk;gK#>EZL+#H7Co8K2-W8jL{bqW%Kr(bS^In;nY9t+R{Y-rIpmfhU zmBPxJ;?%CLHATyQmj_dW8&@DxLhFrYxLf%vxlb2x!|yojY35tMh;5RpIeeDsc<**c zyNCb8KLVHs22%*{-VhN#0s>*(r)dm?r15Xep78`QRcVx3rvUb?2Y4*GX{q$-u2&-$Y?zev zN6K_UtY-qNjRGd1Tn(m8sx))Z#vE-L?h&gpNXkFZm5PjH>RR2S*ySi=3E*_81V!$F z)vAa9JkmJ8g(Y%OKZ#cS9j`HFl!!g9OX(v(&1WYi_Rr_1g%R298(7hJp#IY1B zTv(nqppgG7p{0N_tqHPRCr)WrOCSsqG`=OK}-%x zg+OWc<{}|T6intb6y1+j)gtEZa?Fvflp^?M5o}WCohmtZ`OP>rMKpSUkQgS?Tw0D9 z9G||r&!1UEovTQXNU9Pv2}NnrwM_MG>Q}{H|A|&c@8tjRr>3gC{l$mu1-#G^>{j>A zDuj^z$bIxWJYPDI(Zy#fWnT4USe0hu4Y8&}j#5*r`>Gpp>R5*QFAKvuzM9EVz>!_B z?(>~%$);ah-mF!ouj0sy27l((?GM_Ny*X z=^4S%#>%+)yWm%xv6}sV}_Rb8u8Lkwpvu{UZ?INwR&Xpx)~IG7y)=Qo9c89$ygKg~kAA zjx-`ha|td!e|cIhVcT+}Jl;&G^4{+^IG7jgf|Ge{_4O_Kc_;8G|3sjxW18W0=LZ&x z;pwm2G=|*bX2SHs&8k`QhAUqD8hP{PSH@~E&E0@&i)K~O0%{Vvv_J}q7JUmqe`~H z3kun)UH|1*T-bZCduO(CHkZNASop1)D=Ee4Sw#>WR~kg89ye)@p96~Vf@$R6iex+w zrZ;agMp_JMa#e4XMPAqi3UZ;vaKG-X_0V_T%_hw1w+OWqZJ{O*OUY^7&OoIZ`wrn$UpSx(Gnw1YU$640nPexsnp5SBKQ^P_6NAz@aBSN2{N2PB_nX;OF`2LevTJT7SkSc5J~jb%1Yr_@;kjskF^o7OCD z^xo;#r>irtU_O)6OFFeH)9g&I>V!D@mkH!w?wArb73F%s;=lnrWofn3Lxs7~7yG z{(dziOx7SbDj6Jsv1aiaimffeFc?EwSKgvRUO-b|6XGr)l;BKfwrI#4vLdEIWU}58hC%0M zgoFMKrO7D>C50FqlnRYTxi~K+h@smpbr8SO z-P|7mgl2gBIe|+&KDH-)K9YjBWv#X|g3&g)BDFl9-xupte)rLsnod6ndnFt71qgCq z{?$A5!7Lh*c=cWO=r*3X!MXzf_{nN#Lt3AVvQ$$RiSW&lEgZw6V zH{)no6tf&bfTFy3;j$k_Mp9aIZbCWpql-*&FKeD_NVFe9AWX(5weWNU&aNFCY6@@Q%=R=dlAc)9lI zR91_T2S#W=8nJphmvPPR_p00^IU+wM6K)!)mdI15&JM0zp2OU=4ugqmu%(Ekrj;EX z?UjUZ(|VqILquNv)nn)3_p>X#(tQ<3NW9+kq9RYR^IZ`v(#AQ)NPWHQ88tt1bIk|6 z;FU6k%m7*rBLOcWi#!KzTlv&|ENYpt;W$xcoHa5QO&px-rgrQ|^g`Puz0xYW_?Yjb zEUzPykDY5ieA!%>K)m`aA=U8vFRU;*1(ht;eH5Erb74Y#BwL!4W_7vR1BrcuU{)9B z|N7_6)p(qkZ*y?E@p;WOZ8!_vS`jSc#YAp+!GWHo`H4Ht_YI2)01?m7=^La z#nkiZ9-(DZw|}eSFbkP4Ws4J$2&tflkl1N8MIs#LW|Y1n8n3C8d>ZZBfpK~4niF<` zb@U;4QLg4RuNU55O&GZ+wsQ0KNUfG*!UqTWU!!q5x$I(kqYFs21%&@f*b->nh4i90*HewIwC#=zuPf3wocB^#8{$j z=*YMI5kP9pm)mfRpOM>DBh8=zCs3!ks8@ajfS5Y_5&FJ#i1#L*U_3A#uM$YjNF%LC z`4t2^tTuRBv1-eDGB#EBb2A*{MhPmEfT zrU-eR`rarwbip0>92go#j#ZOIZJJf3t&*{|DP+%!lKLdx4Qc-L@T5 z4}rv#+7|dJiI=12*(7lGTS=A5M`JQU@4X}HE?B*L&xE{cO`bB)nr~NyHp8y_?#pCOG>Q~ldZeeT3Abe2)d{__p7+L9I2T5onQ+8d<0?GW)fvSb zRiBUv?Z>B}Spa?iN;$p|^Dg@5Io0}#99_Y4cx<8OYuF!_$~`5K!_vg(Irx{|&iF}j zIJ}7S+6o!`a|Z4SDo6dIOB(aL-M1mQ&d_?PCB52VimRZBK)ph+Tiu6s@Fw%`PrK4a zXcUpdo1Fs}3lbOu#6@1v(RR`r_&Je?YPI>0F@p8HZl*DEDqVR0ME%E0aHoA)a#jGV4|+L>E*qL zw&Ea1v8X=5`XiwI!AMK!qz_aA`*;TM??kKPf3wv#5>t>Zv~1;h^DR+7l)lMp2CD-bd67Loz;MMPMPuE8pkPRM|iW0C@$rr;zPYo5uOpP$NPw| zTCqvoTe}IGJ8dlt=0e}#@Yz?_LDR&n^}5PP`9D6M#d|2RlB3;^B z5S>g%Nb&Y8!8Gi5>BMA^7A>yi!({8NPPrCJ05xiZBkt~}vkJ`op?ABd0G)G!=ra+* z@RLN@27NzyLBrsufTrH^%K_~MA043HqfQbh%t<+4ONvWK%92q!cdyf9WEi)$z0Ave z^iZZW^Fl%<=Ix*|>%P=OE+zh!U5A)S%lf7HTx9A!I??no4|p$>APK4 z?t#XcD2pYhL(CwYyYd(c-4;6h@6K#6F&PuKOM}JoD(DcNlRX7SMIBsB!`gj(OMbKx z@zvvl`zjX*;zA6jrlxAUs%MSgr;FE>buOd(8fa?AYPxlVjQsr;e4Iwe12FyviGr1&R|YR=s-UoE?KtyO_P-FBB*l)c}4Jn!?Tc6RhDkRAT= zJ!69(IMY$Ia58#;!Aq9TT!m@h((@uu%bLV4Fyvb1Y6x%5Om~BmO0!y6{VP?ImJKbi zQWg&MwAx@EPFJ2=*Z$k`IHKw=;9J}1{+0s*x7;iOV40zVjzMXdNr_i*k*ND^LxGl3P8xdNcFbx zG~EST4D{QK#ndxLNuP1Y4XuD5XgU{G+K_I8>8Y4k6twPw`KVMP%h~Lg?=1}l&lqq- zLL0J^+OZ z0UoVn1+J7+K1gm`#1jgtz5Rn*r4l|9w3y)+Mmf?TsYM=lTLOJSR($>f@h+d5LK>T; z%~;;U#<<)>W+AgKE9(1M-6t!Uns+I7M;p%bpF?xWiO(Wgx=Tyr$q1Sii#}QX44#JE zD&gj`{NgD115b8(DEZtv98P#2NV+aug9~rvRXW+F1K$)bLH9Ix>_JiO`y~^j_>wbbgFLI3 z+50bfiz`uoX~x=W+9u(dVO#;mPx}rqK_sK|N}*Ua^R--h8&KKhRaZtl=A~M3Qs}5ze@I63mKVQh;rF zgR>07at!3+@r^?_q`0H1lARYfV~19o4a4&~^>CWzq}e>nfFr8(9u%VSKfmI8-W zPnd!=LT-PN^J9qlX)6=AHLKd1Wf@x!`dO9WLPwrI1BRnYaQxRKYuC+eyUIp{sC??( zir$!NuM}x|&i2g8txj)68TcS<`L7!f|NsnLc`~ia|H29G_~sx2aaXtFPlh zC@59<9Rts@b^Ls8Gjh#_0$jJXVe%>A{Dh z?4tvGMX(0Hd|qCOwu)8zbYSN(V>|&ZHGHaL0hOddD0SwlXs)e)%h-ugBKH8!qW3m2BL2}=9gqHFPM)p-8i^}CFY6io`X3_3F9?zDl+@cPpR(TKlL8B z!fL9xR0S!Jb_|b)llzNs&X(7>~w0j9t)p+d2hI>Y|QeH3Wb(QYRa(%RvACPG!EbPz2?$OGl zp_khZ&NMFL-R_bUr_8)fdGVrQ+pV$JZ)thP^nE%zQ>3wvT`662nI>MCDl@sw@lqgV zcb~?$(vz`Y)yJ1%FVCjl7UUD=oML;HV}V+7MFn4_{t9%cm(gUjL4aHqk;KBd#iS#R zjderEODCerr-~?T84;kL9NgFR!4%XK<0#c`jDD>_ICADAkp&MESt^m=dtSpn6#j$` zDqS;m9=-6W-{)W1NV%DZ@0X&l7FojstPT%ClrI@15Z`@9_v2OiW5i2yF+T zH6YJR(}M}}0=7ARYBxXipHLMIUi0$~Wi~1FKowoee=B=P@Q=uZK}`Qf#@4#sS5;d- z0-{TBTC&)kg^hmDQknQCl(;%$mMbM)OI&o$#%n)uFGqD78ioLtX@HM_T9Vj?H?9%M zeDOO9(^{%6@NRQ8LLD++_x;F07`g=RZ`{m zXBAQ*;i($T5&U{@_pDM0RoX`hT#zsu*$tvKYfO>}Kq^_EsLq_V4Z3?=a+ zNPu_MY<1y+HfrxW>RnV0sofSrpz+Cjf zBb@8wxBdn!<1K$hoE4Nn`BD!adOJQ@O^JN_V0<@#oxR!TSXHqRgC z+yR5R`%g2L45Xy?Be`4e5X`LlS7S3y*YIn1%U^BujqMc+=;Na#B%v?V$FQ(-b$if!%2HU zv0}d}w`sgN?tRat2o=Grl>D^uED8#pc8}U@dCP@IC1NECTiUA0TAL-kIDR7|;qZPhT^k`Q8Iq&$2b^h^6z zi({eTnSFuP*pddEw!66iP=EX!7Ej6WK_j6iixVghM9AYf!-xA(PmZ(&zPx?i^!_us zN_6>tT%M|T75v+5{VoT+KQLnI>5m8Ny4tg=)9p_soW~U>nI^N0^YAGrRmevNcMfTk zrn|UDtVFFwovs)pj!^or@|GI6w~nUF8?A*DqULFWZx&EK)Ezi;B>pH!Jp8EAt$UMr zAC=myj)qc*juXAa{4M+9NJ{2lO zsd7%!inDudt5Ycsv(K!A)3E*Ia(-DR9g8T(Juqw@G!|0hLtm_@sCsr|nS*P(Fz3eY zf0bm)P;{TtQxXz~*M0OJv??}maqU0^LosB21n}~kq4p@VO4HK7L#57H{o&%vCLFAc z(57Uk1LqknuaeRDpQj@l3(GDZ=b+@+Kj?bf9olCdz2}00b25*B0dGl*!`N7biX}?B zi6wL0e)*vXhU7j+K8^9;S){J8(9a%0+!Y%-R6-+IKR6UL0err&?H>!v_0L$|v8+-4?Zyud zjyqQnaXDDhKwYZH!2)*ob)s46t4))|Nh|IWx+!B=5y&{6W^Tk2lKfbBEjE*cE!iul zSZrZ>bf^?rKoIxS7gl>}p_Q^QF3vzdlNvQ#26lIwio(p$Avx{PNrT@%L-UE*rMWl6 zzgAI^>4;-UQ)S^hCAZTSITJg$nH@~`J`@f_JE0Bar5B1i;mr|1jx36FLNJRi0y|Q| zY5Hs^*tk}i3}&5itOfeUM!+YrBCtIlC&(j)6S=XyixMa- zu>6W;dt|S;r}~FbPfDWQrpU;~_qgI&mO!MA+HR|(L4lKxna8e58%%=J$J{XMO%cSw zw+0t2p!iiQMdAKMClQuC^L$Ay5C@{`)KMGXB;ujwc&3^yB~iXN7S%^%LC%(9<&}qC zfOHnprAqPisDgnWMcgi=iUN}M%a_J{_E7nr9{pm5T&(gbV%2)$M{nfUpRPxzM%B|Dzm1gm78H=1;)?@hQf*^ zlWp%=I){LCPmRn)PuUb~GNbNPuT^b9vojR0=-pg7`ZzR{rTP#8d_fli}JsZC2F#)X@p ztcpiFrDN)ES)*(CmT2PhToqxYr)BE-M?v$cm9n7`i!@H?PtT=jJL8cFVU;XBK3^J! zt?*)&DM<^eouvBT(zhCpU-<$E-^>J3KE#)9=k(pBO0JK^cwdY7+&FxM`H{_#LP->b zhth3Q(-$1CgSA5GYKe+oOY=`9Fm>Z$ zFa|;{dh0_A_{*N;d45kX7m=0RmERfkwzjCa*~o=%RDC_DfGg`pK)?=^5#+ghnkXAx zC=^HZ(GBg?f!nPVEqbdPG#^N}PfF7#RY|&zr=g-#(yXbDfd$xELH_^~M1pH=x2}|E zs~a*EQ@2YCcAzaR0-M`Q&?s3( zgq<#GqI~no$L7VK5iO3#{{SHeOP;hA?UNM9xex-$DHgYD`qhAma<&B9()FR%KOgg} zpT#B|`ppRT$n{V^dr8RT{iCS`wEk5@ zY^a-zi`?7pa#;zYoqE(rW=vdeI}K;ZK_@9V0w}uJpM@1)o5hSE)5?9^*;4Ss*J(X! zF0(U3$nj(MSzyRz2W&mZ=|qT&n+C+2Ap3GX?1!))3O4~4H-x23@(5P;YxT8CqMORG zf+)p>rvQ-5NA&i!Wey%uvg1&&JYUoc3e*omn4k%a&L{g>4^6#qR89j4;oTIVu{O8B zP(&NHlKNPm;YMkPA&%uVs}gwp+3_byVsfP%F#z=|t*AQJjq|C?V8QON_lRL4e0Bc- zDkX})BH>>WX)++U?ati?P)OE@sE%W6cd1b!9hh96np9G&kjK)3CUlGw;M!J(FUT76 zNZ0y6qfCpFx`H%~tpMA-K_Um*NbG18o9dwRNT`L6Krcj7<>nlQ78_J_7pN@!W*jCt zlH{m!6gP=acC6G|c$}zw*S0@_9*k`6c8fX-KjQNkZzjTMRfXLRDcqbZ}fA%K67J^mG7_v4u3hrv9XGu;06 zKxH1fT-CtWg+kE&B)(>|`UkQ>8R#e0LN>ij2#D zcUhL*PjBH^HmyG>37-dv#J1%2%R2l=N-T41#1X~?h#=SxK}yoK!6vba@{0;!I9vOI zh@bnSsH9vK7NRb=jEJ-GTNTCadxPKOL|Ng(6ft`c5Vq7U@AIvBB66U$>`wLwpmx7{ z(9n3UM4W#y6tN%ezxLz={{X1k^$l;l$B&02jBMS%-^UBKPWuVz{OF7XhAV$?T}Hpc zgK^@#Yn%a|ZFxrLW1+Cmga587wRB%C$OlZj|KC2xMh`g;y}BLo|}fX zU>-IGrkw%pQmVf*%eee8`)nL!e`stDMue`F zkySsK@=~!UXaxN#IuDgy60v;Ai~j)RKp89r#WY_*J~m=5Mk{+@7P1X`n!2a3^Kxbm zxe2zy!%JS8$xJ_5KDLd64})|R(D$*mS;zXmuTsW>>&5vwa5K_iNJ79k?LpOHccf4KTupn(;)3(+w*6C`7~U@i`py{L;% z8P0L#Tx`T>vjU^yPLxMs^IuHj{(8Sz{iMtXpqJg(%O;*3}IvZKMBs3 zK;0em3ZuC+4kDXe+Thp@^amo|!?_?(8z~L&9ViJ#?I=7KvuQwH(FXpVK2#ptdJMld z9WydEwxDjm%ur6n+Xs!~EO(JE#Fqa6{EgOu#6mb&gAfkV4ZD(cs0TkBpB}Vl`<~Nm zy-|t$Y9y1ye4-+^WhVCYaCd(SuzT_DHZ%S-=gPhE6Wk>)@VEI;CxpQ!PvL$-8|}1? zZQg-Aau(10Q!WY*?ngU^9l=dU@urODjPU&2r+Q(?RJPY*q%MZ~UZUeq=lpre$K&Qu zN3y#l0BQ-MIVY6y(qhiLzy4>|r?;UMD_YNp!K|sb7N8Z=S_e0hE!v3Helx;M{{SQX zqueXRX;Nj7(xdnCwe+CA!d6 zc|kl+jq%tK$!v~7%IG?h);iP?y!Lm+;y5oD#=pCk%Cf5WI$r+(#;g%PA+*eRZws5k0%@H~#<{%T9HrssSWlN(T^JiVI#oX<%*fu^$fAM!yR$_jqN{ z+G`WHxU~@z$9Z{|Hf$Wze{h2lZl5Y9w6BQe%JwWvTlrAb$avDI1SHJ4>s1rHtBSqzBNt*u;4V$NP-o%=qc3iUZ9uoZ13ADhJ4nLkqWr!!VHh59 z_sQv@2CGbrhgvCyHlS2DVSbm^vLu+m1TKTUK@&D*ia(12G@z$y4-T{n#?)nFi{}~dLwy^;%q3~AETA#^8ait5I3}uOsI3q%D(|B%SOSfvr)k@Sh*$A(?!LF3cQ6 z_Z^Q?J``cN;(VV6ot{GiIAzs~i)(AtqQ!>43d)g?b)Xw}pl#Hjl@Q4`Qoqwy2Fa}e zHlW@(49jkTKtDPKRkH(bln!VJGzYj)df=J|0Q8_1)Z4WK%6bDpEuo;y@@fA7K-c2j9F&{L_=(YyCMQBP>OuZh1(n4pCe>m( zzD6((1Z(tyRS5O|s?ctHBrF~zvbz0N?NCVZ-=NC~Cc*BY`dX^8xtJF7)rF4W@(0tk z5v}LkM+8{w)MB2{ zV64#-g_HhU^4BGqkc7vMPqN3LE1Bv{^C4#bYAI-nRJCtLF^KjFU+r`<;su^EDbp4r?kv#`F#>rF9)>EHR}l6-fg1 zq-lJ2C-&JW4UYFv!2bZI^%+`u{{RV*#^mAhaVS_p+z8X^W4V8o5v}2sxeO>XcG@>p zE`13dD&nsr!Vx{1_vk_R(-2T$%a53*JC#GL-@P$gg$tX6LXb-(zgtxpwqI@KiN1rn z!G0p34V46LRND=J{-Ob^7BC$BgGg`p52y61tf3>l6wy&YAX>5=gCRU<$?bq!GYXO8oD@<%+_!YF5N?QGWe8tK}prZ|rg<@0CSxaV$! zl5NzA((AkNo)gNC6y$Maag2r#A%yqRh|hTE0Gu2W!zdrQxVavds4R=yO3HzyAW$}C z(%|bvC%9P9dtv@G4KTA-!(>z&xX3yUdw#W`V1^f6SN@bonCVd8n8{|e1nMLxd`a;ps-B zt2P`Nu*IK(R%qq^^Dytb`e?3o=O2zoB=To*>+NS>>&F8|tk{lE%~jh zwqe-xsH)2gfz;Gg%nmO%KOr&kA!%lFwN*jtYNO~+0P!4eAK;|HrY_ zfLFy}@$O1z$CG{=>UUb=OJ2a$g3}u=Kv1c;T~$x}z7!T1f& zL0y@Oi{~&+ZOyZBBmFk&XbZ|D{{YLu+D@|%;c-T450%BqNzZWWiGOXR24V>Uz}1Tn zd9MSN$a$v_`0@L&Lc67I-$}hyO6I(7T;D#z=ET55hN_<5z;&QANJ5c&>GGgwgp-RI+3{n9Ik!IQfHw_EH85t+ME>!@8x`B;pVFq+LEZ-rL|ouRZtbI zf$v2qG%5Jr)fuavJCI8eDcaWnAB7R#^E_zzM}R{F1AOIFdRs0Dh6ui>5+@Lr%nqB~AYTZRm@6WB&lhW^r}te~)Sknd7yz>pbr zNC)`PeHR&$e+=Rpb+ed7iS;)f{uQWJUsfjO`$-ismY>CCQjHD%v^OK?-l>c~=6I4m zdm7E4h#UNLp@H!)#=Orv$DHTdm_XZis3P?j9fyk?XBEX`;LjofhZ>}1`X5@>qNB-q z-wlwNb0>pjCu~+XI_h+(uxDrdt>K(}Y%wBSiBt<{K+>*^&p-KtpXQxJE&FUmAO!^6 zkBvr~{8?k46Bva!a0wr&ojk!@?D1Pd*fK~Z`Xe>zZnenQ3^wxz(@DhG_SM8RV{ zIuD7`pl=%#Nj=~0^#1^LXl1`8;&L$JV)9sWI{wN{^D18D0l7L-2mb&e@wnXo0EY3r z=NoCVF#_bQpk1~Bs6VKpX*q5#A3XeJkICb`xeh%0x*M=~#SJfwWa0VegGt2lWmA*H zjH(vEsp{Bnd^U}u@{v+iCmxYMN5)b{s{UK<}Gv;0!ox<`jJduNikj0}b_O9-W zMxy)i*B9hmegyNmFT_ZKNI}-xnk!Mp@;nzWykky}ks12cO_&kZl_EA@1Ba08xzMgr zB#`o`EL5E}sH!ZF$KE*7ZWu`zxjI->!5=r4@jnoNoOOy!@qcX)0>JjU6-F6<`Dc*l z$;viV$&zFbi5{SO=r2Ll_{>T2xOhh>Ny`cu)b%fYXezg~Gi2n8sU$%Hk$aGJpt;A$ zLXKl%F2Dou9ZdwRSz~Ol=TEBk_)tU2$aM0^q#CJ98~*^Nf?q9_$KvH<{_)j!G4_jh zW73I7kvKe#EJaI6BYid+4KGMVcRl1Y@jUhpD@M$@ECugkH&s+_bDht?^NueDMJtJt zgg5({wChASn=d7a`9A{zWk2!xKsM4ey{u}QW=1@B<_~G_?B{Ms)SClug$*;u#@s*V ze3%>`V>lh4ofxp`Ls;*~o;4OHn&nBb2&ErN5vBX<@TpB}zZuAp@$+#YDy)~2U_FTI zS+!Z;Jn=W=Fr@vwWKWF8V7G3h)HTN*GK^95bv7g7Dw=G^tub_)P3mIk)N4}`q$-AZn>FDOxFMR|$*L~3V{1@26eu?r z7U}VzdyZ)1mgl&z`Orb~m{v)nnOmam1N=<|BASA;mLw_G;HlHoN{vPbo+VuE(0h7( z=&Y*)yGqbdwi?!IYCdBT2_24xqbi`LiZoJ9hhEec0}fb>O)3=z#^$UVE>8esaBCajiwy(R@75o%q*{m7E8O0a7}8Rfx{Yq9^)*2-mX? z=Bnb)#qxw1yPxf4H5Hfq*q{DK7F*xkj97dxMP<1E0LY$EfyACK7bM4z@as@`a(+Rb z2f02}(r#-71lWHHm5Ogaii;l{%`ENiHj0g@y{}MF$OB(WVz%FmymJGSc3IHT7bVLM=6hm z^6V`n#&l`z*J-h!mH7*ckDB0dxtulk7@(LH)Rkd#)6$j>f8x2>c--6%iV1Q`I0F9w zOK5LctvcT=gO4rDcK5=7K<{yLdd&K@&-q`)ye}o0?ni7^Z@mY0^{uka&-ha`WXzFD z3~BW#J=e7sD0%P2y!RLp(jziD?jc3UsalMzxD&$rB|{kXlS_DBge!G3JBGSAOUI%DAcPE+7UtN z4wN3mf9?idO0}&(zDX1(8D&GPX2fv?FCj?+^Ci^pd0Dlg4f492c7acZ-WDSC+?J~*bi-bRHnMe zip4)2;yD~9K;Jek<5D%$nnE+VGcw2({@uC{jp)(NBQ8(fDBX1@ZNKiMN@MdBUgzwd z+64)_<*gH3-YjGxfz+J^4R$}r{v(Bz;)^@R4DQz(l%Mmff^Q@EyN`T%ati}Iib6+Y zrkZ!CGh8>~31?(%xx%S?6#(ACpNigb{BMT%zZ~Zvqou{{VSpg zJ|Dt?)f&({sGvZhMx7`Y*CK$J7E+_V0*&gsZ}k5Fl>x@L8d8GTP4BMl55|Kxn+tK= zqX#>2=d*x+)O!B_It!JL>D;|z7ghPtJsKlM3UvOF@u2ZlXv#3wfgrq0$_LWxwZ2pg zXYsgMu(TO4tg%1cyZl81m{_X>35N_}u+&+ia!=FnCW9uvEzEfLG&vp#KXJ$8u@hnw zc1xR@GyQGSU)s!qSi}#KyTpKjdv)A;(WXB$mh*odu6td#XvcWkX z>!I8(59>-ZF>{L3%Lk2dP42UiE=w(eZFQ(a8FE3vL^FB0zkkonq+?+())kwfwzjI$ z8_hQp{{ZGQ{{Xq<9H9|mRLSoQ4^2EhsZw- z0M+A=#_Dvl>#@I;0{AXq=KOyv!nqi~{FfaYBgp+*b=Lm?{h-lB;JI(b<;meZxGR$w zkYLK==xEPswE#RD7lqEv&x!PnT(6+-rDeFH#r&_r<7Eh@S!I?+v(07NplPkIM!${j zJ~js_0%c&%t+;QnH#?0G4;F4PJMU|h-PYiApcyk~c>T5kwA*WIY6>nx!8rb1!cC4N zVs(ACqGIcb;`qKl9O3c1Bao?5>224ydKk@dUNxP^^EpYF#zC0n;*(tfzpV(;AVr!q zM$;bq&%}13jv1MpZu^+p{{S;U`=dKDuk`3m5{@W}FxquL<3%Ts0B}Ja0|4JTEM^!T zr1zpKxdZ*84esEJ`c!IG2bgW_{eQQ0A1aEc!N6>py|BSew_N-yLiac#V|!Nml`N!o zqE+KORPSn~=1S=u|JEH>x_O7Zr)bW2PPg2;f1Yuldy&Iiij)BIGdx*_-PF z;%TD{^JCA+Pk=A`xNoJjpe_vx}sNunEca;7rFab-I=5eJ&^rDbNG1ln*Ihgj{#^phR7_Bh=V@C=Y%9bQRqGC+_hJ z+Va8+Mt|Mff)5Qm(g^oUe2j3(Fg*(%zvDq_5fPPz#m%j$yBY&=b9w=DVg3{fq}Xm2 zC$$0FYBjo^r?3Ngb{le zC$CBgym;a`{{SABCyiIh#6~7ibq4({dX!QgYt6YxQZF6jMh+;tOFWBhPkWk#LiV_S z7X|X2xsNKA#S-O}WN-4LLm0UnbTfGmFry>LvXDoE#;&^ybYi`&RZ~1(bm6jpWsrUT zZ<7J%5u5vW*P!WY3B2Y2Oq0pHhueiLu@Cj0^;x6K$zC7BX3J+W%`9d`BdyO*W1?Lyg zZ6enGCa9x^>d_F=}o*VSt_W^n$ zUj`3@{y>)#Ep~An(JZaiM67hzN{DxnfxIWrSC)=4c#d7ImnTpUuk)ahC+z5OMjwAD7R`;N~U?RcyhH zu8pA})>@8t&3-3N7m%84XuoldBan`D(EVDES}Kkc&2ZU1=Mj?8m2zGQ4aNNh=r`U6 z=A5TBl>B~lJ0^`U?h&CQt%U{8mvimH@sG%+KfJ+$>>uMnh0gOoFyz9v1Oo;e2Hx7< z{{T7#@r-^;jpbv@Y=+2b57!=~SpC!;AC{WfVq{ z1L^5N{w##f><7^hAh)YfNs3lar2ha`zZxYNGZbusVHu`0@jq&)?M7-_`fXL(`%En=c%tK0UHj+Qv zL6rQL$KbaGAdSS6tb^Lw{aWkN^(f~YSCjL8KZ%iziD=g-gCSvlg0iX=d2+MWoZd;K z8=KUzqBS08;}dZ4<2w?jk)`#Z_JV=J+E`E}j)ZHd2jxI)q*}wTQ}Uo7KAHtylnZ-MBFKnt{{U_EA36$7 zUjg{Mw$H{J;UD|41FhehaU^jA&yWtlXPSB4&u}j6(W&6 z!T$icYfwnWTx;HI00z>A{{U6!8Oe(oZh|#E(Q%jA*g4;Qhj-gwCSifz<1IXbkRO#b;7^95J~4EqKMn{vy;6WlzU% z0w}|cfju^z*ZwpX-@A_wijqzv8CD_oV-fVR9-@>|h5UifC+(zE;&|CF?mF0i>r5HH zcZl(BMov40M(2E!Cg`~`jmsOJw!IqA$}>0zY~EFlXO?Dz7CZ~}Di-*5wdiVh6~i70 z44G4Vo==>+iC?Iqw_ST_MQHK=0Ba^U?AB3`(E;u;7rCP>SLN*Hi-n64DFPV}*BI-z z))di!@vci77mLZm%*1IR$+G*P8x1UKBWw6qoQf>z_*7!>$PB4_{Vcyau`5aCQ$LJ( zeh-wwrJ$I5Jb)bt=qPG$osf%l-&&ZDc;0>|mw5c#4=%fTJOS-ar|t)$ zy;a4p1A~#o@uQ5%oJ$5Vv@$WUvF)uxDhHJEJkJ#jIrw<~{s)){mGd1-D}#Qb!K9QQU;_29~vUZjs^0s*;ilJN-B%V zbCmKU9AEo?NnXU0)`F^h>}XYkxC}q1y+NDJ$a$Er+>N*q=m@YhX$Ba-Zy6x&ds#{B zDk+skd$6&w{cA$Ml=j{k4x!X-+wh`Q5AEI9AyNbPwHaDq@Uul1?>B=O1#RC`L1)H; z1c+i%T3@C6*k7$>T~EV!uXSU{kqI_CiM4?S!%C7PYzU4!9RRJg#Cg}`?mK|Tk_=W^ zxfUPW15gPc3JwR(bG(N$l-YT*86|CXIuDSkQmb%tb9t0tfAAk@+HbQxSH z!hV+%B3*@^V)S4?KPZw<}IkKA{WMf6J^-E}mS#2BNPt3xuE3O~2FYJW;v znB<)4ka7~EToq%X)4i)AHyjAe$CPSTMbq`EgRS}Q4E&PusgD_$v|tcAbnjFLjeatM zCmB5F*go7v{{S*8S~kgzfsP8{+Afv<2J979~uJsir>8et?#V^=@=sZv;@e?pGK|d zXcqqf`p|mh3I$f_r2(1%uyNAse!Z?JJtRu;vM{=^BGeh=XX5h)G>lp>H=`bfiRpR^ zu`0<4Wa>xKXgwP>wH8y*_B0alq9<`teJ*X-4)hYe_$9^=x@{4jx3M%zfMaDnK9kes zR9t)YyXmWUtK;mXYrou>8TlcA2gS(x*-bdo^X2-$=uiSPHv@vROWH>?bVFN(h2-3&lLlwy%GnFGYdpUCvk)Ut4xM_6 zLD~Fc#(3OrD;pmp2_;aU-cq`fel=NOJfnw?$2`hDPmGA;!6SlWUYmQ-E1+dTkL3It zV=}JXl-#Y?YYv8?4fuDz7Xdena;P1m4x5got8^4)AHneQJYs3MUP4cQjb?YoRu|F! z(PLVIEI2%$gn2)?$Dx~p=3E(UFYU4G@}SlK0L1aIa+7k{k(lP0Z!4yo^zJED7Ff8i zD&q1mqs%b6wX`;1{lA?}X-mNQCn@9djtj}h{wkJSY?3aot%b>;mz&~(37eGhc39S1 zGr*_mVo%mb#)4CMmK!O-@(d%03g~8Cz<@npZZhwS@ZTkq7Gg3FCkzU1xnrLBMFYK~pxoSsyi_C)CR z2ui1=x?Z4*`{OJu(y;eHu=wdwQGaj)7JXfT0@k9y%4TF)Ev2oeaqC4y$6_a7KGS-- z9LbD5n%a)PDr!_wj%1Z%TLQ+~^s370TwwUYFSS2+~NhNCFT^(@G#DX@db_u=OIX zWDOw~Hn;<~U*S<=s!m^-!)F;-Ei`F%_ZL!Ej&`@M28^eEhA^!k(%}i|_{-Tb0e+sJ;_|7jT9FO@fN)*6UC)_}>mLOP)-Z8>5Vkq;?=)kgn~GI;wrQ3~%Yut1SNj zn(>ilaT3X4z9YF$dwNw$&v+LwG1KBjE%U6bYdI_#6D@fdScbZW+td-D zHAK9)pyg5-fLO930B&Bl_|=T0`)}>p&hVB7`t=nNQ<2T*@%*${lVa}Uru%XL`)|}5 zm0_&kAC4){FTxc}gw{w??32Eu)}uwi<~VO7#5`v)hGU&oj`v*^uho4kLJ0BBf5-CM zD~Fx4xY+K)0h-OPTTm$TFB{~;WAj*lu*jC^-uU`#J8tbk>~9b8enL4V;&Yo8OMo5; z30oezP+CjGXQvdH82N|ah;9ATTo$$dstRWCpC`y*xqd-p9SYv}{xlCahxk_?6}fI) z=S_XaH+u`vKz?HuX%0V*ah}_;+wZ8*4ws;pgMkMbm~wdxzk4;lkfP+MJ!mbnsucZ_i%dN*t zb)a~lKTY-Y?^P-MXg3u*bW>~Fy(1|ZQtXXmUaj|46L@0cXrxH>Z5)on)}YpZ$q1nz zUf0@8hy9|Usxu1>uPCe+cf@vj8I&tKlS4-1iQ`WbvD%2r|N4f`9~2S$7F4{Ww;I83Rth7TS~&j?i&HJvVbv{i(Tz)0HT-3BjTdU zNU#UkzuYIgcA`2j;r{>%mT@;LoSR@l{kY^B9`r{|$#p5UG*QqFmaRb+=G8+ibu}d-bS>9gk`OTZ*B}n^l2{0lgL)wW~E<6=P5Zk8OT+(UYY>P!!)y zPsV{zg^j<}*8cz+2O=fc>K6Y1Is;ub9ViIM3)pm^@ziNRn+Dya-*D7=P&pVNU2FpD zufl`NyM6C%#_d4pZ>vtU9$|fG1RZTanHccoW3&>j!wnB`eMJQ+UQdN*IHTRfeMR-! zM^kRp7WkahS+NF;f^GH2*4zmBd}tCxg{B_O;vIF@S`Qy`z(}?|$mvy_BBXo$!Y$ai z>M9Z{A3vYRM+X?o3X)bq^*avN7c|C*oc3?DT3idsYuG9N9Vh}XCCqZD+|gS_*TGfr!s0 zL0K|X-K0AGUklM=3+Fy@l8*S?bma}UxF5{cRgKA$_xJ+jG@*NGV{K?8cn_9M<6N?d zm;V47VmIHh>3@|(we(C`&`%5*80;m3AwvdUfP8A|8|1PvhaHgfn?kBtuh~Go%{3oA za-MUQkDDj_SI~jCZW^;$^w3eL?Ee6bJn|1C;+-cW{z@`S#z+zsU-Yhqh}6S{`vj6y zZP2+gF=6)q0JQvS>op&l!+uwaFUL++WwW6}p(FZDsuCb@UQn64_+DN=Ymtq%p>brB zZ>PqKhWQ*0Z-LBA%w5@73~!SX^;_xF;ZqfLi9Z?0wa8VO2A|TzA75J1BuFKUGe>s| zeMfpLI=rWg=E(5#SW2#@+ns;S47h_K;wl(!5w}_j!SA%PWllCC=&_xCRKY)t zgrolEWA#WkYuJq`i09sK_coXR07;_2EV4c|*zX{fVc+LPJ;$=__QzXjXeCB~Nw|x(w%_4ZD$fii#@(VV z&Yf>YP^XZ{R>JLXs?-{O3y+v_=P~}$zJ|e8f~%g-SL0YCe{sH_r%eq)S1ZfHE09^3 zHObHqX=CRM znD$D@Is>J0tzu*xl(<-p94u9y;M?O;4PWD5$M4PMuP4aea3MRGH(3vBR+C+#Llki& zIOrmRFan^{r>!tVk;@BKfB-<&fu;mp&^F0oddLk00F$P)1957By`uC8!riD4-B;=r zbTZBfwKZT!6&2B<1wdu8-6s@>E!pC`y z?bG$3obo{|rPt|3*2`i*`3fSU4nxT?#H8Z}KuwqnlAW}oiyxHa7!Ywvu-4W!0L963 zPc5W`D&JGLxACeSI zA-JZOH|b3ne4PGQnVd%mv17PKK0cuGCHC0*4)rlBu6SNtoL3aMvq)u)&5)b@AZTfWPSWFL@zP_+ zH)BktWN-F&rVc&HcqTs`&j3#BNo3y;Ub`>cilbA=cr=_APCt?OyR>f@-!Oe{t<+F- zt|yr}{BZr)0azE^6s(4wXe* zSsq2k$84C6ZX|ab;??^1pilXK7v;~jnF&cqurdv-KT%aGUm3x0aAf}P0pID+{by|w z7nJe!&zy(O{jgEhPhS4D8duHY{1iw|M<*na~9PaUXhfvl`ru42mxHru&u1x3VA z%OVA}nKb=sG;1<1Y9LzQQPO~*lnBcaWnff%z@Ta!$!1YOKNz#f0M~YvQmli2K7CN(B6Z_PSgh;<{?JDlmt|fK(@Nr>OCkSsc95GfBNO=%l;gbtcof<%60 zlOtQ%QhiVUm5|JyS(2ormbLF~C@Wvdxl@wKF5b64Bhs1=kmOY%kjBDXfIgKMI+|#x z51KKww$E<6%}&k;}-=@{G3kR36;wLyP3iLQL(H|=*D1@zET1&_t~SkKS~#rhKG`hQvnaX4;SHt$ol zkLk9q0Oz<|-(hl?U1JuveYfOt{mJ%F-JbbN>Ku+d)r($mPMzKN`ukUQ$Uv z54MWd;PT$iX~fB8_bq>{kEl0l1mwI@7RXHA5>tsTuOt^c9r}yVSmNi&E;B*OK&_Ia zY{4yKrk!ev=kfSNi#HoFgvO_AI@@{q&@l%yb^J!1t)pB;Y)9Hb41h zML%KP@c30!tZYsu8>|qA(^7T+02&6f;pFobf+&j}O|N@h4wMX@GY5)|$`9P-SV3dtH2QJLZ%%3_*vcC5haD7cs z*S5%>L19zS)B~jyj}a7CN{kPuON(n=83L(7KB7Rr zp0ovPxNks3zH~wFRsJ9!nW_!1LE?lce#DXdC>2G;KtjM#O00KKJ*BRC3IIm|zOSeY z=x7S$+UE2gAPNB?7=XR{-`0XmNwj>Z6_noe4Tif~fUiJ|p|m34^%NZAn#C~lc_CB< zztwK_EfCs7$B53t>_5QPbPC2~2P#cbMc8}~N@}W_Y@Ci=kyra+LEJm9Y7I6w7X~bG zAG9od^ewG9g*F#Y;&9fbyWn2?%+r}4^ARvu&38OQ zr0&ut{6JIs;3XAN#`8Lo8DZ5n-7sOUZ?X9rok(cihPsWv(Jx4@^*=_Avd4ABBAtJO`jQe)s-I_r(~^KsW76X* zdyjDl(a+-IhVpxWe@*1cAZp=aH9h!$09`K}$tBteb?-I@?SW}u!KJx*EpW>!^Q-h) zp7}2NsR~t3_cq62J?73E+M9_!_w~I?8*R&RctzNHw}#PeZg2O7`kvq2+iJFnk|Qf` zly$sD@3&Iqesfa7rO~Rj14%cY7vtr_S(Xci;6>MKcC_m1gs8i2B|dqHl`}OGp}B#y z4=Ua;Wmo~xpaGQGW9hB$lUBkUwr67j)v~7-^0|dPuIkL1lt!iz3}O_X%lhS!t^Dv^ zXYa3hVI>z&I3$hdoOC5B)r6>%yo@;5Zv{9#?9uD9<D7O=r3UxoZ~axE zQOJo)jcFsFI>_=oHPe?Xql3C~$*5o7Pd=ODaubi!%hYfG2-V;ddN6MllmLDxQ=WE6 zETW2{b8fm1HlLy*F^%>#fp`(z51Vi|y*&}Yp;`BkP@G2mEUJ$L*CsO%7>cX8J!L(h z6BJyEhfl8x?Dg&1X*b`(-gII#dt(s=Nj7v{xGN?2@-6EJ=jh$8fiJ1{vrCB1vXiL{ z8b<&0eGg(f8)b#rFJR9lL`2mN4xBnCsE}2W?cx1Ec6ul(;*_Att(mH+4u4#dbq;49 zX5k)+*lD6Jxzt{^0s7KeWr`Jo`60&O$MH4$jT6S3-_1l+W~GmxSGU)okSWLG7+YY$ z2_Dc9fGUyEX(mkb4r3`_dkHIW0})B3_t&EpOG*ZrgySsYrlXvG` zk1@;u;w*vTogSq{?xSOE@!?NQu-`efMvLTmmdwAe4TRr^8KKL&o4RV0j^~^bPQ-?P z{s`TFyeI0B(_FP-qn^HWZ`qovQc&P8pR&wdK=)aAivO}U7<-1=y^;a+9+8lQli8X?ARl#NDwhUPYj9XH3CQpobwUaC% zMv28N`b4UnMLTbu_(R{W{Zn>MM3zk?zz!U{2>$e=jkR4WcG#f zT{_Lq_;6m81Lxb#rAscGX6&i;Td_^5RY2IH}mbh%srWu4TR7~PR-d=`HE zZOhVEcq*+yTy8zVnr_IpX1g}Ls`?{G0VNn}MsJ!-X-5HJS0P(-22sE}e4e_Kxe5_S zjikx0%%N9n@2I~;N!;u2CC0gir;KLvu@~8a6gfoBX*aW|Hx}c>l)h;3iF;*cvES%d zB$r9J(s$#<6%#)`@XXW9Ysgq?X|kyY7ut;+FGS9B#}^nj9WdT{|D*NZZRtVyHK-Y$ zHR!E(dBp{KZGTn&>%R!!?=in@y%yW;!&e^#^0O^_t~`Lc)WO3`o&d&)9 z8PMxp+O+O|QzWu?9b<3@ec}Z|GsnwzQHhpvcql;~9js;pT9B0RE(3Z&0e-ro6Vbp2 zG-sALpCnv!I(A~8L)gJCvJr}x~}=Lb2PLn6xfd?y;jgmayH^Kvfa9g-laP|wAOM_70# zwiqXh{d^s2kRdJYdv7^-5;C&NaeHLVL4mBV;(C{H9q@I}|J}4F;+77p)tB@ijvf6D zQft&S3edR%q+Z|icMUdy8u2=6zK;)WrZ)PxQl&amIMpcmdn`Nn)^c}j(2UukrF2y4 zYjVE$!9C7NyP`YLEgNmlUD!%*89-eRu+@7=YafbvaiGoh@n;)1x1j#N;4Ih)UF5(} zVoB)P>q>MO{qX(zO7vst79yzfA5Qu|!0N%HXRp45GVXbJ2WZJ{d01sM`rX+Jxv}=4 zTB@!7-Zq7uj&!M(NZ9+b=PMaZaq-LXWQ*F>9*t{%;`#@XfpHc7;XzhwiPg z7=q0KPp~;hkA&*%lt8vM-0p2fIqS6V0ylz#@cfYDJK0Fl<9l<>uGmU6L5UiSQuOKK zUb;(vRYtRP5pP>MQvC0}u30!nclBUDQFP_g3CqJEb-0zcvXiN4KhNUuH)6X?tGqmg zU>$e4KF8@!0Nfi>ez_RlJUTX@4B8rRJl9#bgofQ_*ORL{w>FtQ zqFImqsyTl4Tb+SQs3&3BE2BO8h^tB6KKZmrCH2o{xt;l4`1NlWHhzv0fd?H#4&V8W zdPN;+EZFKscmvBW=@3JGo>f4vmJ3&2fB<62lQdYc`Fji-#Jy~+Tm%?RHQLj?rTd7W zJbstkb77MdJ>1VmaHsFE*JmcTe~^t)l97P<*v$e7MRfNuf(6+mEg z6+;=E$IC3{5MzCVz9$aR8z%)7!>LqnQkWX4^dDiuI7dz==tQCfd&y3K0VA1fknM0b zdW{L1_mbJT@M^!h9%^TnZ_BnG(PilCE?gkl!vI+2Ursl3fsrN;7Aj# zpo!IIFb{#`dsofY%6F@kAHJmBobd8m#Q|o-7oMOIcU}#5JEo9i2AK5ZRb6<-oiaiyTs$j#XMdm<1&zw6w-}=9PNw5+VpXZ`PW7-91Lq3^QwEExvz!B0cMXt|R!>Kmb^#?%n#Mk%3;v|n;o=DuXK4^ZW@;YUU-}Kin5Om^E!?;bDfJ6b+zoL_x=na?WUV1X5pcY{{iU|C+ zhs3`Rq3s9j%tN)(mz^F~lJ$SO^Hv&uU*%S*ksG{)nP=_#Nkf;&YZBha-9VLY;mFag`tWDQfFdB^n; zy~BDPX-8{wZY&@%NLp9)#X2+#L!fLn+w8S6nA34(7MbH@n_$3*CXS4K-dRvQn>0w@ zmYrElOSBhttp)SG`F#>?`GD-kpvEOi**U|kMtE1f6Hk)c;^1>WOr-4ekU6%naduk`7&k6V`jF^+OkC(;rWh8Z}#Ez$bZ#39Ku^WQ@M$c^#=6 zR=0LMlcNQ~@=UEe+Ui%(koVP{>u9;VHM6r(`BUdXm>52a*bjEXo2~~8m4dpCzUDs- zmrdA0BfDC#Y+`gzPekmVR26+$*t3Aas)s*1fRF%pnGexr(**`URXIKm-Dhv*iV`E) z+6!%5d@alntR5<<)@K+WBDZ7P(!Up}GRw6r|>7 zZOzr}Tzasl!ol^ZPO9|o?vdLRDcahEiLvA?*xcpLAG0I#ng#x?&O%d1y^VOs_vU|0 z?Ik!19nnWe1j5k!q5lAi``@~QVb||J)@N_sisoF>sX&)KKUAQ~GEqWp#p!J|maISO zzP(?j0)B9w+bG)&zrXL3pZVO>(P<(kh}@L#7)3B(SQ6ORiV9 z^`8Ffzy2owLbsJa`#$Jd+v_1d6vud&4<1fKa$qQ`Z6s<47O)VTJH8$euKkqiw{eIy zQ*ys}6w}^uf9rT;Y5x#!g+F&**R}om*_N~L-*m*Bu084Au3T7D-L2@xGpG z2f=Kp;8`=2fU1H8Yi*%mlFBc)GUh&&kXpOH8(YwX!5dtBh~kTuJ-E#rI9 za0E5Sfvm(@&qY4f>Mt^VhWD#6-PP^)OLWYT%A8Q%dUhfAQ@LmaT!n2PI=CV~}UZvqG7@W#Tq zVPt<+`c2?4_J%}V;`lr*P!bD{?5sc*DLseo#Uvwzf*JU{ax)+MLLhr zhomi=w^GiODdq$9IGU!94JG=tQnlOq37CT!Kj;>nKZq%Xx)roW0986s=R7A--7UmG zi}1i=NquqM#JduW5`ghg%c_ODyjp!3`L&8`Hl48g`u-M|0yvRS z{eo_2O#X`og!?gxVX|Oy_}1_N*-vtZb)q79NcfLiXSI{p&>yR7m{_F za2K-@&GKpk_L!!A99C~Y~>J`)2x!2wbs4ks@YR9Ls@5a_Dv3PAkt5nYH-JF=j zhgKP>UWJ`~32aGkapN(0Bgd=hid-{A%6l=E*ho7KldP7@XqSu4)+Mga@@O(Zg=`Ys z#1D8GO`X4~x=g;-q>jiDcpPmG%)5<&j zrrI~n-u0E@+w;0?-End*4 zOwa)0&uSia$-g_WwN!TX-bjpsay=e2Bm=pI>a1)6Iq=d8dCh?)8Oa5fN@8^(epgq) zW|3rFZe*ny4foP`pH-xqY?Wc)u86xGlL}CY^Jh|FeQh1&!;gqL8W<#FcHgFgYNb== z2g7?B#{8u})Ytx^po)xZ?pTikTx%muA%1D288Z%4d+Jki^2z-DO(p6*%;LtM?#DTevV5sh zPwAYEMIo_KL-N93KiQ<-uEM8My+3-%rd_ljsPW6Q5hZf3;h%mLjM&VevpfSoqgJmN z(p52`Q}0jl5wlo_#(NBAQGL&;M6^r_<#zc|I8B|xgD-9uWk_Z_E*urw4v!q-#slsq z?>f=@gifq|upJ!mA_Gky-p(3(`;3fg4F_+)#5nTCY_AS3d&AolZ@d!8iml%A#B8h0 zoHV9|?aOC+PQ-1?V4OajGzf^AthV3Gkl1FmVSFK+MNx# zDx<=#%I|gnyGV#YP5-Lb&own$ZRQ>ij4j}BmaC};pq}m>vb@mm74n0P<&i-cqZf>= z`mqqYwAo;?x!<4Wb>goKzwOz?gF3)F2@gB-7j|bPc2xy^=3C~tsCc~li;cet(8m7& z2ocnF5bVoEaY3Gptl+oCO7kBPuWWg?+5C7Q|4K+xQ;#kz17gN^Gqw#p$oAbA32*se z$y6sa%F;8FJ`-g1$CZk?xy4zqr$UK;NLI6AOvH|zaNF?k{%Iad3hfYJww6A~R?c*z zpA|K_WAhlP2y&no*9i*7gM(qpiogeIaK&TN^N#Cz2ry;}!yz=OWZW;~4+yHYWSR#P z;J5KzOmleF?++Me8U3Px{*3Gt^GA4GorqUVWOc8NdlE-;=5=l^3aNx?Ws#ZKariIq z8`FpC@o^0RlfwDv@PVO*X8@L}vC|87L%ocP-CSj>bR};fZ`~C$Mhyl2voQ9@w z+;hHDjntvjcwiMr_ZDpT#0y$(mc~jIwY!eTcnX89}NJ6aP*ep@!SVW&SG z((uF3Cky_6R&^Bf6_}+iR%9WxHWh(v)+wP zEjA*gv|RY75ytTM^5U9P!iky?=O8lu^G;}Zk($3&i_ zg{x{!O52b8l&If#a+{Tv-X4M4c{k2aWSDuJ|zT9(=-!It+jfq*B$M}p0leNV= zF;N&Qw?q0I2r#*fRF%OT1n2B#--V0L!R3VkL3g=BoD@9@sQN+jU?*VzCMqB?RF4iA zQzlt^YmnoxIg@wBrcl^&`UvAkV5cVR@ypw20j<5NSm6ypu3g>zoAe0QWIQ#JID}%0 zZ;Id@JjCQEQOz<;<4_CR2;S6uvQZYpA?UM=1VW!O_0$$MLy{BjG6p+LItCHwoRAJO zyO8TQs!@SlNM6>2S{GiLQyZRXHNou9C8;tt32<)6bmXA0tNSJR)D zntZ%U%Es**5TJ$nn%N1Hhs1P2T~*GhM7-eruA-$FMbX?(3H`R*j-H}~_9)({gNb}100NgJ z=x8$eraP&pdJi`xES+;jp610*kuisq;Szrq*7@)9PM>mFMy2L*vfH#tH*E@rh5jBv zCC<(Ev7Hg_Yvc?_%+s^n|*@N5d21leXz)HYjdb7#GG(a;Gjw2VBv z@V~`~dKCPm=;0r)xVs!VF$X&V5oCj&n{oW9cNWFi9>r_P^W6dJFC8&-VM=N#>E`Wn zGGWuLgh>FI2k8W{a4tjWtEdqa1``Z1K{H+csM2s&`j?9+5$YzuFEO$!uBL=|(Ko!_ zBjFJVrfo>E9ezVqVHr;z00qmeU1a?#)P(DKGi2TL$twDwF&N}Xfr}p{`eOhyT zFLuOw4lw}{C49BqlRE!SGIBkmC#vb(NY}a(Ul@a3KP&D+(WIsP$(6GXipQ+`hQ5p5;y?B&f`s>A=Q?fmN& z*2cl$u>54#M)Nh(5iPN%8?<)m`gO;Wb3k>Usd#W*kLFf-B2&m=FvWfI5$Vy zvMn8iNh*eBs0KAG?f!sOnq{pol- zEA_S9#m~jrZFq0p_aD}hBM$5%#l7l^W<&gieqGJ+azcQCM&M;z8;ydGAHqI9aB%HM zc&UE!dRCg2GcR%PG(i7Zxmcdw=-6mn7#$N=G~H}n0|y7H;?se6292y#iZuzS+w$T< zcDPmJ{_x$piPH8pa<=&&pvJZ^OH{-o^NXxhrKQ+(lcgXjWx9&|q)`(MhztJ7WBToT zIwS{zxUDFvIDp#;bC+=oI-f9XK}6NFvXSG=w}A+rYY*tDlaTdR<>mhYf-i^t#j`8) zlHFFDbg7PnkaYBhJ6q8AohuePTY3PI2#mf~2dD$EveCqPo#jO|LpF0x^@g*(ZVktk zB8<$$w=;L%!hL%W^QY(mpaPEj;oB<2AUG|^!4I;Z27jreg|9yhu1WFfTFN4+;>bifPesX&LHfxT|8(wRFO zZL=X$jg}~z8(aj_A?E}O7}iDiLPPwag77dZL@LD}sZM%(IK5iQ`yU`-eq49eDn$3ZBE@REXMAA!HrNdCgifa;RmmS{n=Kg4X4f{r{9xB$6X zz)J!cZ9`a`Bu5$?+IH{uD(X`F@6Ftba&QU99CRNxjE-L#gz7O)PNn`;!o3iVa5wxF zWT{W*Qtl9WN{Idvu}L-X-iZ<%eI#_qSTCW3v5_(Z7O6}j`HWj1bLJXzJO+KRVUPrM z;eppc+X#&>T#E(91_Bwh^%yl0!eFDwZW(WwJ)Q1WcRRC`vMZ0VnQQ(@s~ zo?7lg#s`SP=ON>z-4_WK3`ofwLtB6lyf-bV0HJHx>;KUVEU>c{&bJ&uOVxGHn1W()(y^K%s{l5;a$Hp_ zu!rX=c)y0 zcgj@GJE=#if%kF3o*lRY-JyShLHmkKJ<9i!^6yV9q`2xeymE0Kj}6e`i5>a#laXU- z_c_+Qv)(C7|7~)nRLr#6++cvNR_=Uwwg0E=w~yhwTqnfzUzO>vnK>sP%jn50qficU z%6R5JDGI&RUrLf;_QXkV-8;3i0-E#zH;oN~%C%Jb?Xsu^;B@k2^5!+lqTy}+2s;zu zg`oL~+PF2Nu4PN}Yh;%!?u1TWgs{0`EBP|f@wDa)X!=A4-hwNn8)B&9`stbOjjRD0 z)bppRUy7}<-?6|B)m6W8DOQ;H^6$ot*_Zs*mUVPvkFA-VSYp%C7!cxj+%$#tKaFve z;%Q=H5xi@vq+tV-ID=noR9<3QE(u&FujpmW4l!Bxj42)IDJxmlG+J&-d`imMXWQ&j zzG5|S{Mji3`>ZNOaWA8Bkc<8fQ6VdBC5RVve%bx}jwJooy4&hO52-PeX ziuO{d8msk+qDfxs|#0YM*{asvM}r{)rXP(4K|WUfvQLz1B)wcspFb9?{|Dj6R~ zr%{ka!3S#OiHOB1`#p936$m$=s0}03iGe%Ka)U|Hgljoy8jK?5(JxTq=MYZ_Yo0#d zk%&_s@TX7#%qAyV!#4N$?-A3(<314Hrm&g{yPZ@oGnmSuiM>M=bgHyUr)3$LDze3y zuYYx)TI^^0>#vFyENhh) zs7(AhM~UQzhD2G+a*YN*2;kqZmUD|pQKZrs$q!Ry>gMe|!Zq`*KZCwsU(34H8b&)* zQ>8{~owS(`4wRER_+TYUV3z(x8kre9UuoV792*`+w~xvO*b-(NIAEN`PH=$Xj5a1?OsYE>Bm#`E4PpK&UInec{Q2>Y@w77z-v5ZrD-T`aRIwys#mtTn}dqcAgj~BNL2Nx-p81_N6dIK77Lu$wUubHAOD?w>C--&%hw8?yXen zGfR-oY{pXohC*}Wwx#-MEZ1|G;-6K)$KHknm$rvN)Xp|8zCBMy-iQwk5sk{S-dl22 zn3)&z{2d**Odg)yf6#Vgs=aTgWy=~nEd5hk__2kIi}!jf`Up~?#p@SG9m_FxaHmpi z=u7GM_pL@oYP(9$Q;6~=ECOZ1Z^R3XlVV=_ONORbW@@ADtTVm7OGVFFRM<}HhiNF( z8G^=OUH^K?m?S-yH63NMWkR4Ow4`KfSGJjN-CFU8P z1Ujp=YCqWYF9{gYuxH61j5U%sq&w#Ph=!T04kXI^e!6)xk0}*+^?f_u)^_U7o;pj~ zTNe#Insns^O7t5=`kx0GDK|YMc&WeCl{Kz<$1XxYO=)%Q{%I&9>2Tp1cxO2MgMz|w zOh{oT2t%TILYmRTj)H$F#<;-WSIr%M%eX1b@gD{}{MKGDB;c)%xX9(bF_IGR=%NJG zUmvEo-R081hLJ5MAMafaBW`jf~+{igV){x$cD>YwW{;TNjn2JdMXNt*vLbBy%p?=aTPbW{#^~Z}t0(yN#V#aiS*JdJTR00UM zyOANy%U)2DT#}bJ5O%I*3pA7pR&0>yF*lMYWqiMUGPeUu`@9x*J8U8CwJXQrmZN;v zezW>)8j#-vIrl~wYt3;r-qAoWq%3fR2_nwIm2I;6AqA};J~!H98geE`6Rl&=YHNxe zU1qeb&b4l2yp23(PrshdddV}Zlp2JD%^BUKqitpYoXg7LIHUgZ6NN|=9v zkHW)*J|jsWr`q$WUCX+t!UKD|2;|FTp}}KN_3cts1#d&5R#KWWFXg5%a|qOF7R9p# zm3Z$&(GPwPgF+XPT_mbT#C3U5)G0Q6GbDZ}wU8lH2DHG#*v$?1kBvIu&j}TH%V5yQ z@iN7D_-;UB(58; zZ3hpx?BlwViLZE#wACka)KOJ+RGDZ(xZ+3AztCc_CLo9sJaTSDzqAq}mybNSv+CQ) z7kXojVfIkbgx~a)N+%aL^QRs%LC+3rYL2`qZ$Hm(Y1y~oB+q>Jhr*q10NTJwn5xuj z|NC!dC326z;0xA~<5bI)mOn(gC5HYg)88-S-r1sR74x-X4-uLEkBaY1*5++2!FwQw z*(HXbWuEL&MWI_vGbf9Iy~n@JUn4wv>n+-8zZ|rAt z+Tz*VoF=j}9Vg$zF;D1(ew6f=cj~iPhtd0C-*1|laElY)8>hG2S2;SuJ(Yso$k>Lk zDwh$iS3p3>lqN88s|k3|Rf3WOBS)r43;m#n(3p`kJ;oz2bmR440zK}Iwy&=uM#YCu z#AOtw6-*^~=RB!58>~xF;P9_pwy`(id{ZbXT1fBBnOR1)MxK}r#J>Fv<1HG-0&pVv zTY4%2Q$N#Z^rOM9Hw~d|iM;mxK+%<<_ghyrTU)v{|NJpi0pjdETkr|0j!f1C0K4Yz z#rhZED2&kdCX_wI>8TKNFUu2S|MW{-4Fk)b^4T{#Jr1RxZ`3~|U+y{8$l6e_IO1{Z zTKCSjz$V#mu@gx}z9LKq^)&^5971zz8H(eW>2(^b|7gU9=_CWidJb zasQ2^w3iq&#c0gYre-u)$Nw7B$e51cE4mwTG??>@{g5KPH7!{X)tg}2q%n@3E@;Hd zwUAKsGJeOYV7~8Z#y20lQbe)l!G*X-j?b{Ch=|;lSNy;`tiz|$wE(@|l@opzN4Sy5 zNaSw;@i&Wulg#jc`@ija4u3IXLCQSHVi~6&j*PG!m~8gPyvJ;6A)97$(*`SY#SHYF zor!*YNXztBX~S1|TTzyW(4eVF=tORtl+bMbKy`3dn&6 z<>L-9LV6!@JkF%Sx)SCSVhJm)Co4DiH{fpyo557G?UYp!e714*T?aqtug9|7ao6pq zX8p|AXX5q7R`m6iO{!U9+{zSRi>>p8+Gn^b7w#qkf<|4upFSs{Fwm3q2OX=X3;W$o|Zy8m(Q#=@o6 z%0`kuPhkS{7g1oR#5tTM$?cF2b2kWj3#Zv<%m{`$!KJ|yOfZ5r!&%P$fS5Je2Ic!t z6zh+}9jt>^If^TRp}NsG_1(}$)3_fS&oCRi`GnAS*2t}gSQ9F4mEji& zRxkOE^v`K1Dq=`)8L!0WyYrOW*!#1i-8oN7Lkpk6(sK_Jl=|h26B&$Bow?_%D3914 z_&?IRX2qlm%!osENP0ENAq0&;IX^eL#>itL(8FjY_*VE{Ac8Zj@#oNL8^-5QbeL=> zm_Ty|f69j^pA(E*hN*=@ZdhQ~?;TxxZ{H870z)t(n*Fv$i&_h&4Z?MzDF5Z6<@1l> zq#E>z=^zrh_<__htg`kv@bxskWFDZ^dFw*Lf2F%sw7p6?6pBKFToPeB`Ov<82y15n zxEFa6%#I06AiJ#4Z}RG~Pa&t9fK0PULp&%Ed!u?`^Lh{~Eno-wgGi#dnMNS6umq_Q z9=np}c@ca@}_|3qEkZD0R4f3>K8Rp-+S{(=SAn`@Oy@3 z%n$*?;xNSM3-b*O2Hm;DRh3Y^mX0bWO*p%2cV63Zy|D(11kVs+YLHq)`pp7tXMOOX z%cRe`;H1-R1S>^T)c|Y?MN)yk@8?z#u9nouWa6;0Fdkd2I2-<%B}^VldiybZal{!V zF!h1XCgAe}3!wwks}N<=XQd7=`LVJ@>`TvQ{kkfzS^NQ>Ay5&*HN|Vupj~J&Mqd%6 zx5-LN*^kqVL(sHhYy!BhSsIRwZN_6&_|;nDEKa?~+RnZ*W#3#|7yTBHxfHtcn~*Mi&PtYgc*61zTs z!ND^_6Xw8QM*W-LO3w_+MswBbZhdq2c^JP&@LGRv_sXoP}+;0((xt;C{lD{ z*fCH9D;Tyji#$$%Xa@BXBmskPT3`%W&qKanz)qAxsJru)e61iPg2M?s55SoHL>7*N z6JS9IR<%VGNoXbEuK5Ve+CAK38zTEiR-im{&HWSVyXRdMv4#_3V%OJ5LXd9-{Lcb0v-(06F{ zS_)5`ltD~XKkReZ?OPR}^V*_<8~x!^|I$Y>o@%rJY1m2#Y#aC+wi5uYL(*^FI44Nt z!&dznZ>Ym#@E`(n1%xM+5cD5_?acWRIZ^a76G?io?p{hqxM7y$Fdi3q>#k*$`=8!x zDl%R4#EujNCNd4Qprr)q{yENhuHRP5a~KQtF>BHR*tj(m)K4umPQp;`MhbI8!*NzQTfVOWJ*QmM5BVksk8E{?R7wT3m=M7PA=2W}=Q zpF`Ws8=3ESKk6d~IT>a(Rn@{H5yFQJ0$-Wy&tuAHnrW&#nJzKW>9ah1$tW9##nc>W zTe)mZ53cc~v+loyPq0H`(Yxor3q_tV%9e|u87MulUebofviA@u z*h<)e(gq_KrYH#V5_8ss3AXRI16}u`J14Ge-qS2ay)!7Ol+=AXD}J#EslLkFDrzmsR{O5bMtTS`+h8xaM>Q{%`h29HLO_a_$^gghc&+vIf&UO^HsH?Px` zfiTYSn4piKjC4kZUIHf`&`RL~r)+@+ctAbUh^bI1L?U}XE3|2oUcWyO!o$uBQM?jc zyLsUK&4Iq!ELPYs zsbp~uHubKP-=mW(Za;(HP$q8*b`DJA0l&cN>9cG1$8sRH1QyanJ-_@}cH!FJXPND< z_}!U>owxkQPa8+Kr5)jYk&5(O6g~aaSp^^?%pVc>qkO0cx$;epWS|s;N8AMIUl8gD zAcRadpq(LbvSTb^t5|UQ5Xo+C|Nb+dM=g1<9EM+xso6pbW}ThImy%L%dWXxz<=sb< z^@?s13>*8UW~W}q8W-f(&3;EYa}$PI8#l z#jqLaai(WKdo&*XknclUOdf!+L5ZJ#&YJAqf(d40Un+Uqum5G{Oh8gkKA7J1&&ceO zT%UP#{)#_Z!34ELOk^B;to%e^x2IE3EvQ>07LlGDKd=gwI)_uo9Akunh)DB(1jRW) z5OWeuiTOkRxRwzf&HF)(r*@%FT?nk<6bv0yWM;Z1g7%P)jA_6e;rqec3De|AKoJPr z23zY&kKeFAs!nr6Fg_t0AsbI~U<#>p8r^1;oeAODAGM4_Z$DXCCo6lN@AhONvyF9| z>(!I45AP0~PUX_-^x0zk=C-EEqK!JV5j=*Wu8U{+JDKX+M$Cv6cB-^sZ|yD$v}ELyY(SP^otsQn2`5AJ)b=TcB9@Qk3LdE7eltwiDNR z70!@Gg@;K7^Gnb?={d26kqvnYS(rA)vXuI|Ed%o6D3wA>e`-`PY2y{@oqwxbI_$&EklMt5x^Z?nI&UJr^KP^9F~~K zm*DiN1|H^gYC-qU)J>GlR4ygoZ;Wv!a)S&H`>6@f%wsi*sWS?huJdYDOyIPxQy1nZ)jxMn;oJMZ9-tB-rUo zeV3AwPA|ro!V5~C1)~LSLo{Tebwfyo|nm<5y6Ww@RLO=q(m*&OKrGNQf;2Ub1xt;>+(T2RiK5j1_gxbU+J+SF zl3*E@G}lCtz1Xu{Ji%VEExI+>`kl?|ClVHK!U4j#2 z2^u8G<@vq#oqOKz-tXLh@7r^BrnaZ6rmDK8ySlpOe(ru1AbhTwx$Hw1;|zhu@30b^!Xn zxAA+p{!<<(|AYN+R~ai8OKS!#TNe)(gsqD!gPiU&UVa933%H&0KQ#aV&H??`6LZl1 zgBl2y1O5*fIQoa|{Tj}lt%rxJ*wd%Z2wsbS=<>o{oSynvxIX3Q<$DT9Nc*^2z#XhT z7%Z)A?3^W;et&CYVz9H4WHNlF&Zq7wXKibz?B{N+>!+ay_j7#g*tzL`i z$SeLk?!!MxrhjMB+uNJhTaeeq-R3Dj6bgOHC-782fajqF55m{k!@`Hh8NvJ?6y&WD zaCbXb4?7oUhJPqpSh{$6NHRS<`QKP_a{W)T|E=u*OG9<_|L3MoPX7q>FR};^9qa$% z_rE2K(DQY*eyU@QaPf49TR${s{s-ArOwQff!o$T~&&9=2lIj1YkN<_Ct}dqHjPS5< zhFh!1OENvs;I*@}5|iT-lot^d6_l41P~_)V5QU10h|0-{LWTIDf>04z!T-ROcY%94 zSv!0D2d>qB;R^k4asTODC)bB3%UiqKd0AU2y1O_r{7YytyZ?q40R`b_B7&kKP*E9? z2VMkZL=*&|&qQPui?*Eu%Cax z|1?A?hW{!5qk;d?!2f9Ae>Ctv8u%X#{Qpk_|K-YBJ3siZ-Vcr{aE}5+|I3dSl`8G%z{_CIkx`=g~uhIzj*q1O}s_gE27vc?>A<;XZ&)gh9;6FM~;< zZ2@6&BNYfv%Ee-qt?nSx89!nXv~&-_#(7LmK}p5R#?HaXCG<>KL=-9}C$FHWq^$B> zS5M!-(8%}&+{*g3jjf$M!o$bJ=hyDu@yY4g`Jao+tLuN{dRR~V zn^+I$f0OM0Cf9=!KxpXbV06eoa)Hpi{}G%B9fOe{lUPO@V&O)@BoK^6Dw~vB-GR+4 zsB=VS={}D0m_=xf_4psr{w3M}nP4IRpCtQF!Tw9GML-U~L`O%*K*z+uz{JLU_&vhH z#Kd}pi-YqB2M3oB_g{q&7oUKT03Vlxgp`zogoc`$nuh*A%l!ip5Cj6j!^NW@B&47s zBO;^v&zt|J!2R;WvW(+?0l)==9*hZ01V{t#Y)9PMbCUJda+n)l*`vR}!dyE2y|@oE zN-|992|S(ng!};4K8a1Vts>!C^FsAODvKJw5Q)ZcX(Fztf`u3`LCvSgpi!X%{^qP2 z>w>v{Uj$b-4Lokg_I2;0bN1+bak`iJ`%*ODT3TlVEUQ;cQ{iwsp#moGo$=?cZ#50M z=Q(*FnrA&qo7`jUEW?hzG~y7LizRY7izjs>a(UYEO7L<*jM(XC@)cX^i`F;_uj+jH zXE)z&-zk1-|GvU-^!V3&-N&*sai`0hH)b5NG!ina45Vz8-khGBJk_iBz)uO>*bK}U zq1|*4#(-gGl)De+WQFvq#A_sP$GQvJ2jPau*O4d)@m%wUWCw%WZ#4rRrS(dP6OM#F zsaq`a#JCOJCcT6H={@iwvCL`l@~vDPxH1xDrAXabR~ua>J@2@>v5;Y5c|%Y5{TbA%@m`RPENnE_T zq%C-JepbSw7Gfam)S+o{c0C43_X+J-sE89}ol^TErc#TNnlG_tLrf|vNw(M-}%HPS>DnY5KSr|owES%f6oZ0Hwbo5$)w6L|G zNfX^Di=khnhH26)tu3ZH@u$3aTOv_eCGfr;`Zy!Q zDtskzAY=N>&+G%w5|TCtg}%Wp(>~f%2#NHm?0#|BWSjJp;e@B)=Lsim9rQA^letd1*Bkz+ws^*gx#MjenL)W8lbnxx#AK?NAqp#tHjBTGsJN zN+Z;Ca5>pw{OPPKk^Dta?opzNkD+mEV^plhk*sSY<(eW4JT-3KRPZ$H86?TNNePeJ zHm(imiB?(2@$9j!B6E3$N3R&K8YnzE`dv}W`PwX z-O<51VOgj~`g0Xikb-{Eku?`C_~PT#_4sS1xy#;TCDo+#KR$=giOZbL4LazKQ^7*#Tw9G&63ePkCr!oR4fx-l z+NAZwSVU~5)vaw~nbXSvr6cc1kKU@kdNW^9%cLUXzx8+0j8oz4QniQJbb%F@UaGI{ z3<tcj)H2#*RfkGylTAECB9Z45o4>Jh zok*52gpGprMNU}IygwLlL&KV2$_fFTnBqrV>?CvuX?PdWp)axi>?18Y9IlZ!$i zj<>O1y{9==eX;nykX%URJzy66I8RH7{*w@RT;Lukk_^{+tRsNkv3fR)#&H>{AkvI@ zg8WNqttW`vU3CV0w2 zYP}%X@`>ov56_+B1EVY2d<5|yU7zkM+o;&B^8bN9>-+$s=awY2 zksg5?_o?hecU)<;0NIoi<8%`ZLFzP%`bZ4yNX@`|Vo8pK(V|-WZ3SPTRGI<9Da; zxoVi3V1DJhlOr5JaC9aG_F>50j3B*Th=BZK6`b?a=5>glUxoqKwti*!A&WuQuPZ`c z>K&-ppT(O~WnNDzuO=J(EXUux%G!5jTqJ2{1Hq$JIgSsSd?al+t~t*n^DHlJJgRL5 zgPFEwS7ZH|$0+$FyEant@UKZP)Ec_jMysTq@Z44brS$wLzQ%m4B9}^|lRreyS#;Hg zHZTqZAAA;sS00(#^TJ%1=VHu0YQ#@FNLqDXiHhbOa zor1dHXf?@>yn8R)E zN`N~iyGZ&I;2)OHkK5+i?s?5O48Zo3CI3zmskhVkWsZ#z2_*a8aN;WJD7uBh`bG#| zp!Zps;p3q--A~MF_l;f&9mv?h2M$Lyri^$siW`BzwL$;Wg&1Iq?)ye;R$(&Q5D^Wa zA)c*y8l8{)r0wm_ACL6D)(4rhtpzKD^@<6W(0hPR5&ONQ$WteHr|T#}LYHe5sy`x! zLCMicYsj)5QsFxr=^-*M~lCr&jBip$!2KV9> zu5E5@%LMOsB1y97Svy_mN)pe{T+rLJYJkC z=jrF-CBsTLSl6R|(HjoXSNUYT#(w$pOozkDQ6ILk$4uwRFfaTz}GSFq2=&G0_V_W)%J1!Xi?c;+*QL)XVAF84ei_QZDC+l7<3gK-O@vT(sPTiJwX3P z?u8h&xcPq*>G8kYS7=2Hg-_o{#<|025o4#j{e^7)`l;BYPp~K! z_@nJe+<(a=^=P`-f7T_Gr8wbCH-Z7o^qS>pDnbCLDUd@|BB-9DrT3^qKPB=K_X9m#IdL%BiH?}coE~6txck8bum^~#S z?jkh#W6q9kp3>m&w-g4Fv-W%lWSt&+Nj7~v4I_!=6dxZ*$g}~A=H@PKQ>&yL#JPXx z>fZ`yPIha&(-)>umio2Ii@Q%YW; z9y(Y{SXugQxfb!4Y_*NE9PDu{nu}c+p}okApHta8Z)fHZ#&r19Uyj4T5{v}e<2U90 z>mRFs9( zxTZ$GaO#6vtksH7ft8%o*m!(NTlu*IRNbj3K?cvMXVf*m-FqvI0(l_G}Y)Z zFt9T-vwITp+$ml2GA!YA68Pd{Pso-@ES3a!VI$sJQ&}DTmMpP!w|X1bnOcIwp3=mN ziz`DFGJE=wP>lG4d*Hl;KQC+gM)?_;4To1ZO<#D>_f-p5u?^CQL8{g?7n`+Vwz*hS z(^2OOmUBACH46IAnYs)e$0v)b0m%!Bk2z`xH}KNjK^w&kdU(v~X(X1yYw06OvIK6I zI&dZ9qbr5(cToq*;uS*6<4OA@#6f3_>R6j#E6f8J^Kr2?UGP!HdEP8by|mZ*9N6sRRir8Im;km_J!3UP zZ!z;(KIBPdCr$A__10pDB4_Ys^F0t;SNgFq&AK59o7H&pjy?yHk@T5IS~f=6@UQxb zsb=CYOF87+Im?ddS%F*f%QCUJ+4uF6rlOuc(@Wt71_Mb6a+7reFL)Mr7a}%Kab&MH zEF<3d_GtQXV-BCCdsE@{1S9NiGHf5ghN*tz#x$9@0-;GNIimF8tv<&rGaXV6F;Cv- z>rPpKnCZF@DxhAhImKutt{nPy9oxLTBi8x4<7c6p2`d=)Kx^a9LK^bV-;lvZq|dEy zDchzzv8K=%*;d~sKBq7kC(~m+KldY6-);uF{5%9)ARc@_bD)O;C!C@TWCQ|Mx;%5_ zLtPyS29PYB#E4n;3@?gv7~1j7d*aYgl&)LmXjJY*TkYL^Nuz^Dl+MH)vHRBhR;7Sg zI9=wLP%L#enW+M4(ymsw4NZV}A{l9Rp;02ib6~sfrFXaOpvOXLr4yqBUNQD4BRNL( zp)Hk+!+Ir2Pwx8Emh&n_yf zF5MFxo3yF8Fs~!7h0ZlH0f_EUKKKMMC111ye2T}C8vocWbpMm!khEYpSQ2_|S-8dg zto9>D;HGbvmvi>JjbV_xdSXvpgB3qg1&riZ$Ii@ZT5tS(JCY?auZVc6CGG}0a7(^y zFhjmv?DpuVU1T=pr%Zf*MS&PN-y!4n5^kmG^>*2a<6la=!1$`fQa09f5A?=!5S8TI zuuq?t&Ig>{zO$sLZfev~T)sgEm-M!B&MA4)|8Rlq*83k=f7GA)%p~KsjXr0X#jJ+k z5-Iefdy)FRbA@$HV`K7<>uLFSUhmwH14W7bHR%-jT|`xBk8LA;tf=$c#>0gKgmpU6 z-Nkgw71xjtgA>p2)Ey~y64`4gYBoLhlxyV4KGdzNhn4%VUW{&!Kj(_gue=zHNP9;UTptS zt92`_re-k9a!88jpb~UOy&LqImPjIg$5*}0;EH&XRWUyz`HOORxtE*whgz;h(}Vk1 zO40|Z=j6|m5>6Lh`e1bY>21-gwP7qg^khy3oi5%(ct4os{Q8cxOTB2xblyziS#8gw zxH&`JcmkKZI>664d_Bg=15aF=6F)!n@d;7qs+UyjRNrgs!#Zzxz@G-;EWK)1z_Dah z5z;6Wk!1Ol=tUxBp1GzLpWE}*@m!}0+ml2(L%BV8-%Up@ztQAZ<)Y}0q+3QloeeF2|= z5Xn>6fr8yJdl%np=;vrp4J@$3n}ASQnMa05LM@NNkC*gS%X|R2DCHmL)Lyvd;iBH( zW7ELT;E)y(029%x4U28kInPYa&mKv9-)&OWxium-2J&SYty7{aI{FQx6v(SzZ%1aA zt8zX>W?Ls~_r25iw7vqkZtRx?}pOBYFiUMeZWoG{MyKdJH6#Wvb5DjwKk{{-?-Q^oqGHV*@qFuYw-2GQ%H2WemdY zo#K$>1br-i*2PBR$(X?-q(D~6w~(?t)FZ@`@SQMob&Q;=pOj(HCCQzKOovR^viwi&fYGEjE7xE>N>O| zep@j#rae4c3Ot;;sjn1xmTzG>L@8bk2zVb6PM+Tm%H5uasX z^k-{4Q2gvre2gu2|D}6JV>+6Qq~2MMd;?WP`t!(*H%j8w8L2&pR;C&A`Vcwi@HeJ- zbNhNaHu3gU;lo42EamCa?p zUg=I_@2^+s7X_rjr%u!+&OyuWwz5t0KIZB+7~kYI)7V$&!u<3Zl-JKj@$IQ9<2_X` zW3?A|XOBmHSt>|5h#>&DpPT$NHzD6JXkceXvI{R{)dJ_}#`Ch&Fe zUahl7+{l&KHM8b_9o&&9rUjIZBbUTZe00Zj9HFYOCG(v5gA*LJU7z#`-v$*hFmS2T zXo0brGe8d!s^2;i=5)6YmWu&{hbF?nlhGgo986$8R^+T3jN7W@666OIluLy4+U^~Y z-inW527bi|8TL9#KUs3?e9}L5v8#p~s0A*&bnbt9*q-&rtdR#-Z7}Iz2oFGrQTUSl zsMr}=b}7Bg@`5*h5OIp}5(_gv5}bHXbGv6&|8Y&+#J|L8o66-++VqmmwtviIW19pG zym42K%9=i!D(-xxb$sY=?$?HRA&T}>O0e=Jjn$@Jsw;K;Zwuo&Yn1S+9786*p=PrZ z<_GRQk6kNQGj`00jf-IavK9(nf4YsC46>2l4!_KW*L9>#LnM~e;rl1ms~dCE;ln}1 zyLLr;HC#Z|P{6OiwC2z=b33f%N$x}LTKyBir1TzW9CGy%aXT}!3uZFu!*yLOoNQd2 z+qu-k9d?!3bM+AdX)SWcXWKmbsCy)5Am8)-^vLsGU0dA0jEF`?PxSlu9r$>*Un+Bt3nvM zCul_dIojRFN+Z(}lb6ZDQnn+TL0**WUsg4&iF?W^ST$dE;_}%x#>aFl++3?)cQdk$ zxTBY3L8N^M?1BAfxBN~;CZ*0i&iFo?m=ebaO3$%5@aE8IG7_O}d| z4cWjag6EW@@Uiyq5X#&nOZogY$$~e`D``Kv@uYt7+<5DTCvvie;>PPC-B6hmmbPb4 z(a0K)BAP*LGR67PZb+GW1qu8QPt^Hocf#L(C?b4wrsj4gG0hN3T^>13n8#S#LCMe+t?BQ$Jy1-0gTGREdMKqe&j}Pu6_KAeoBqH^x*N zLLYwy{J`4PH-89rodIuIJZS|V+|`@~86gl?G0R3#0RSaa=|GK*VY)<;2uJ3HSoQo? zE6Ks+=8nt*KeEcz%~{0_zC^$%smiP9_>PX*Elpo6FLEBdEI+{{I1Z?7Z$HXP5SnSF z3}WcT6=N&|R)*Fi#eWj~ zDi?YC9V#r`FISV@ z*frCuO^P~L3gU-RmyU=yL%d*O2#6(Nz^R_aiEzkFDR@dvh>*2_9xt=_!%;MMkz#usGmFw`-HwqLW3lQ z-_C5l@i-tCcOoY7&r>Z};-w=vhqTf1@5iUM_sr>mmRx^o*~7La?A`DoFp#ua(2o*4 zIyeV=qAiE=2Yr(unSPh&>6rSPJ-$Bl9BXzUl5Q6GHiZy&*2kl`ux5ft)X#8(hDM$x zI^Bf7+aVeoKioeLvo_udEh$Y>6e3TDZMrgkz72p^_7_CP3^Ewg8GT>Q)#KyrTX6Rk zntLoER~jJiPhYpZvdokkuNaRpmm|6s^Hig?Ht|s9==8^n*1F>UAaAm%Vf%6+Vq$k| zn6dls8>n`V?-$IGQ?(SGKr!-Zub2yrB0XxDr5{ns=!Q;LRF*tuB9Sn~R*)D_u{s@m zKY1pTwz`@$OFvO32s{!g6q`Udmd)|Vfq(PUMN6;!R{O{8BF`FehBvyajD^bJr_t!D zpSvl~qr?$?HIYc!d=`SoXP@#Su;?$(_c{GH*TE+>Pjj()_w_JnEl*ATTNHaWtR227 zGtPL(M~0Jo^@mE}YREuES^fEP6tHwp6cTIjYnJOsg=%@`BzHAj<9>c2y_pmyThe=k8`YFk>0ROoDc1Zy_{GacAd9_ zcO~*48XKbX-vd_b1vyZHOI^fVdbaWOYVG9uw(+zED(~yVSoUXMh4Ir)1lz>9iLzsv zhc6G)M9LkjNDWbXY%&FuCIWFv=x_N6`E#=3g5QvxS(;O_2B|db^cZb=K5Tw5V$0x~ zT?g7jV5)6SSv=qEK3m<=)5+Rh!xwuqSRMTE#>Znzekl(y~Fy#R`%y*O?q)gN^^=NAU zQapnljc=1#*D>7q9orF*M;iuo zx@e+ilD#cVN!wwM;XNF1jy)1SCRPd^ftAAi)IO&*o2!sbPKrSPsi)cSS6PiWAJ)Uj#qsd9G0X8c;<`#x<{n0^flnd5 ztQE_b_LJ*uwF@^S!&WN%9gZ*_#yGEt5h+uxUU*K+WfMZ`Bzyf zzbI~tn2P~Vy4rTcV*Z$r^S5-E-nDtW9Vn@=eSceqO|6R05K>{7d!r--DozZSYj^6D zI4O#kx!!(H>AisNgb~~9wP=D8P)r_fn#=u4#{@B2qEmvo`fI7j{$0l7p7ELKMNwgv za1E9i$zwa11c&~K(ZM`$1`?r=q!na!ZUF1#nVeIQx2Hr3v+?rY%+b>Y#u&=D&GzN? zc5VoJrj}4VyX?`6-2|2ex>AQgI~GMj`UNV+Q$o2~-vxSRXnPL!cZIdN!WhMuX4`_^ z_7l)}R);7nTrB-4!-u`^6lKenqpxXb91VXNjkAQsTQPq4wPB7jPv-(O%Lbir zMvIs1j_rlrl;@&t>aTFD&=f_$lP&y|&9S%z zS)wTJwB2j5Ve-ju7^w%`Wz7UKd82}$E&28#Rtk<+T6MrGz7>xrr||iu(ppgXQ_TgN zDrzeiKGQ{EvJTom$3KO--2E;(o^aB%2D`4sOOF>)nqEU_V*id?EG zb-zygAzb1~hsN=CJDUDw!Eshk7;}9cHcoz^@R}aMxAvW)rfx9zm!|i8e{;?+{^a0} z5^>y(Spt*_?6gvC1%U5PoE08mYJPft4~UlV(5S~}utgYeQN+p2AO3t>p!Ff#lj8M~ z(lZ`xYBTxe1dwTG-@Q)?`jNn9&!HbRp&#vD^O#E>r_GNqD z{N+FkY>x3t&ExZ`z2koG3t5UU9AwcaPrQHGQr=IW<*VV+Ei2f|@BwbIey1=3^L%rH zazOcmjv@zW&oz$*2N<3*x2?eZj7&4-StxgXW8dh3?AerJ3HjniH(6ut0 zPh@iNt%UeoWUD)ALq(GqZc#0nUFH%sG`AJ!4FO+7(N7l{6=%|%xHGgS5Az=XCLeWi z9-{mVKV=$_8641fc`FzV$`t8Ro#T~IcgRx_NwT)E8Zb?%8fH2&p@rrqdo@EMCm#G+ z|E?NG+uH5N8FLgCn_sU45`QimENotb)#SEnnkQxyzTV#MMC^#nXdzWTDt~7gq}l-0 zuOVMqz+#Sf$1&e(Ocu$I4BpnP?w^KaIpEDc4sTkGaJj^0@W@FqJeX+?ELh2v5KwY( zhkt9CM8h_qE|C8I0q6%aki?6QmSeD3J5Xt)m9eN-TnQ+puqCWNa%-kd#=X)x$8rl! z>5xui{BTVk_FX04;c=z4m(@5+z(A5R)#C(o*SF%ncc{mdHYSq+ufEs|veuZ+?b@T! zIL072#q$XUog_TEffq-28O@#rBOlSXt)X6?OS{zBN{(9zW*k6pbfZ>*8jM^cpX7q{ zZ|w+6pLQ#Rq;!ReqLpv+pY|E zZ*xOkeN*zGcuc7sdvC^gdOA(cYOR$(g@01)dJ2``!mV`xp3ZdYJwWso&Zgw1{OvLC zj2$ohtyAac9GfSRtI*x==e@j z(wcI~IlBAAmM3IwZQgI~z&P~b%?mr~%$n@?S<9UKv6s39-TplHK%~p_!Dj~_wLi(B z#r#Bvr}R#QvDuo;o7lFpwoK&X2*o*gtY`oVHdm&gF)&Jwy`;&hfWoSVOJzz(pU+5n zN6IO0gTLvHm(=r+GuJ5n(IY%MVWp?HnX^m|6vh%AcHAHEs^4U=&`27S$q*QI27DHYHy=+T>Yg6yU2r;h{l{GT+1ith@@5XZmLn?mc&z94#k z@rwfuPwa4YIlssATl8bj9eIfujP_!I#-vEV`GeEUhxYPw z2V(R_eElc69joJ+({l$~>wvl{oMV1wN{)d!y2fthL)#xKJjYorbm87U4Gd84bdE74 zy6LOTL=c0==9sFajoi^l+}dB(+UhX!cEOh-0?M7=yf&)7R6Q<8-OSoyxaBm$*{XT4 zYBRY8A!M9}lB8V@Lt>Tl8GqjW?Ad0*qeTAOCpy^7d(c%}_zaS@9{`;*A!&mkMn$Yhy-gSh#%<#YM;jD^{k}zgbwS<8K@y(BbWe9~SC(a0Lp$yPCEe_cC@?8* zviuW?Ec-r~{#B7V;l+`ZB9dwDCbyyjUmq0Qo1M zeVmMRD~BznCoq+hsqA-K&<-K|9fhM5(`li5p!U~AWT(1Z^NQhW*Xqq+{94;{+1Cl( zE~x$Dv-#R0qc!w`?jJPDv71iNyB#wnKVa{Mx@521x!Eu>#ktx%yRlWSnRCP9WPlo+ zGTtMV+BGcTyw%2IzmH#)wEkN)HJfdl(5+cm0u9=MTGDw$FgZi8@{ycarTYU)#6&{KJi@}>QK}PCti!w4TY!We`{1j+m)MQ+>#RmS*RkE5`yjKMFf?wI(z?gBD^P2MgY zF(F4NefAX@w;kE@hOEBfDyP3~X4^^fGd5h2N<;bZYhBsbjScB*Idzf56Rvz>o3UCd zHS6D0gq)~&)83^9eBa`K<*~ zvrbU;B61bTDx!b>F<7JLuXZar4vS43MM;+Kg_5kEcMf)0dB#%eWRA$Y9hRlG-To1N zh4vWLqpI)II@f2ng@=nzpAF)x{r;i&DN;N0j`R?s6@X_>JFj((dH^mw za>N`^Cfl@B>Tf)=m^{x>g1QwCG`rcPSZ~M9XGtJWWxE1x!{lAE(i zY$YJh3x%v|Nvxh1apfdKD*W|owTb!D;mlj~&LCRJnoDg9)0U#n6ly-6c$zabq)qN6 zfxN&g>L$#y&@}k%;m?FStWVNfYkJ&smq;em#t$I0;8NYHxg`-tRoT#cB-}_rP9G?y-#>#h$A= zAc3jdy9FaAl^_Z6Uq`Z_quSY-kaVP3e5BbLnPZsjF-~kL%HCQ*?t8scxU1PS1;HH( zgAXBel5_+3Sn(!KaK>XoC*o?+ntlw@oA#v1tj{Ib>4&^F{!Bc*b51HftBqcdrUErM zh^Bnrba$6Ij$hezvM3dEgINA%*x|06M0D&=K7%Qa$De@)d=}e}_YJnUM=xHa!hCH$ ztAu{4JoKl6xi9FY57Hbf+3yxfP0a`eY^^4VnRaQidb_(lZ+D5xS0x?pIp6T=zK>ihw?;z?ECw+h>Hi z%7~zkz!d&4gXc6&1#O+o^+7Sdiu*m}Gl6+WjPvcv%kP2l4VD_r4NBPJ7jv=j! zJ&y(}wF+eGNob;I?@Y=g!TO_5JM$jcHo*aZld#45h6 zEAW~AteCE6HTrDhEv^cR55+82c=h;3GeI>N<=F9U#+iO_GFSUkcl=lvevXz99qv)> z0`2$f!Y@5Rq*xTcfYENTk$gaTzj<915P-C*2Etc$bH?XW8#L`(;bIXMjf zcGMmyX9G#>JmV%Z9%f*61PucZX2ri7{YdG4;@k+%sE5cj$R~C}w{j=A2THHC*f`=` zl_hMlPj_KuT!r*A$ZD=)x%SmD0i(|^+b@2@2#87o;@?)bpwY7^A@kx_+Ts`)lo!$C zYX`aZ>W2|UOE90GB!KUJY<%i_I_LL`wI@E>z3yDV*yK2~Qd*C!@RUXqd%|P^nozlr z>bKpE4AejeNrVxST7_<9t`OO_u1RgTLaTqu_%tgrlYVxg%#KFo@-OyxqrAyY4owP% zOmEQ=$AH4jdw{~Re4B5$$h5qD{%=dpp+MmB_tVU1Z*tD+yc@%#;i*zP|0hPKo25KI zP+$mTdrL#IxY7Mx-7pT1tk1cmWS8MlY%dpmy8hk%MltgYQV`{o7fq>NYofJ-@`7xG z;@$NEeGAOF=IpLD-x0qUnCZ_9XE~a`*nW?n#2%wXXCD(Diy?XPeFB7nV~mSD*7X(e z)@TG*<#-Wg8$xWiV4JZt4||@9c3;0aOyDyo>krx#(Pi(HvthQS|Cp17GE*m1h>g9E z_ZuM(?V_IAvJ%(#7nc0%QK&s_#%=p?F_V+#tXoJ`Vz$?CU7Ar3m9F@sSOV#my)VZp z*mlGlLsppJjG&Sn-iJif^HfO!Nin*na%EQi5kLGh;Pwk9 zMDsaXM?>2d%qR_zRB}!QF2|AmyVqmr!R{iKNZNO(iNrGHFS3}22sOAaRKg}c^&u-+ zXWa3ea?N&NZSbkN%x-VdGhwuq7|UdF8;Sat%D)hI2$<16BQG;^LIr=1T|VSPfY!;U zLOY{yeGGRocoxB$dK`^+=P16h{Y9c<8`5+|yvJgt)e-{|*VMoCIJY`0!SkqRkH>y^ zBYRl)BtKqYKp)ZmJKjx}XxOK;-QDiGJUrn&ONw67L>K+{lP~YAdl9uBH+HeVBrpO4 zi_C`CP>Qmzk&pzpRmKtiEWe3r9@>JHj1$#ArHHSaxXuTi<6n?N!{3u@4ePf#d)|1` z*i30*nE3D-?AeunbT9Nde@H=s_|D*~z&D@!p}CztL|@~`L8UGwiXz>j9?*@Z>oE+6 z;={1 z3ZG0Gv9Dn6ykppVWjBRN7m$ypl8cwR0>N%D@KR$=Z?ulBTSLhri8_gM!juU{I&Xiz zNUG6nr^h?zF|>)Qi$b?^?M8hzSIf7NvV{cSjri-z)74ox*jd=~I8B9(?07-Fox|T> zPva(4Cu_b*1-)$FkyK}R(u@?{hmkFX!ibO}M9XCMEYG@e`hY7P|D;pipcjeqFSrg~v(VJI zjmk^~B32UTzkUl->#TR%)eG~@?&0X0NVhT=q0DE_!GVrWq@V{JKZJ+|YGhHQ{R67h zq@Gek`9zmg5!$8XaW^k5iEn84{5{C{Ses02GT2sp;#4umJd%7d~HS4O^SicMFo>-+ifB^SnS&?0@qQH^#2V zU8QPklZE4iKXo9vR&vftqqt3CL<@Cj5kDn6$P9?W7Sv|AN5S+@-Con1xhWva1?FFJ z3MhS5pT7yTt-`Z?t&JPbgrZWxtlNUu>wRZBeS+P-riL zw>qReq5YchK6I8=loa&p)i=HR>;SlBBEIGd#m??1DVl`NovK5e^YyhBb4{&zlER1y zcuRj&H*;)2EFt0!=N<@qrzOQWOX3{m!q&&Lm{9v1Q(t6bh*W!p~4=IlR&|_`Z_1kCJ5Bv%;kXI^Z|2QwZFGS_6O?mRBO#0 zoiXO53HGGVac>U#kpq64xQO##c6gs~xu35EnWf2c>|OH|mRcYjTysO^2Ozo;=v3wb zY3u5k`?Ay4lNHK#%%Bd_+a7j1=gxbX8+kdTLu zzN8wCB+!-Yy6PTtIt)jIHMXuuBbKZlLPQ6y@*_`DDwNvRLmW%D8GGpfS`^s2FS*?9=t!Xuz47*SH= z+2-|_;Qh>bSPBDZ_eG+laq!~L7Bm`n2u%i%=8lQIyW}^<-$}3K7Y)1@MfcMJFJG>1 zBQe-r%xuwQx7o9OI_5WWm+}4mgaV%jK$)24_p_6d*AS1NPzMN#us=2YxcbPY+4BYkO`$q@kyHrKFn~>n=_2 z$NiXV`MX2JEChu*Ct$dS#vo`ofY z`BLbVwj1GB{2K&}O^(|sd()N@eeJ7&xI0HxbXl|pM^zu)vn55Y7iieJ&d_{x{1%lQ zD{hTLF|UWsxY=`C!Yls-vJ&)xP}g3$DJ}GGZ{S(k14uew905MDhg3iu-?JRcxQ?bx z{8SL1)BJ&CF&eB0)Vv*{D3G0#^X&f$C<@p00rDp1I6=u{+v`DAIqiGmPlI$g+xtUP zhs^^i1WY86zKXe}bTt#r^*@igMyq{kV-19mSsBnte=CJ_Mmt%AU>-RgNTlRGQ_^e@ zsSCe)_WXN;?@s1+E#E1MG;luY2XO4Y3H+&)Q1LE+*Ykw(#wY40BW=Kp~s)><>Sk0~pP_ zfzN-+ffZeXq>RTqdCei5=C`Q%YBrR^a9Ex`o|J}>=bGNIWpWqTJAFMWN<8~p)$Of= zMQ?_@c3=reE^^f3cxN1$utFp*@fW$0EQ?Sp-EsndYS}G6l2s-2?->& z&?8AKiyS~@-G~5wCyGsvl;S*5@teWEA0{-o{?ob{{l%#aGCMQpYMPseDd!sB?J2BW zM)B#ITvlZL*<_H)!~O$+KU!BEk3I1R#P1sGNYLNv(>1#sn|R2LGwv4$kF6JAa_w-$ zll(`i_4=Aa7CAGJ+=Ia$)T{^8KM1^69=qWie=*DI8g4DGbJdEE-MIe%c`^9arcI9S z^?ch>haqK52JQ*U*BOyHFNKYKtFUl|mUL zU}W^g0wFjkxEKSe&r|I{BD}oiv+{}tFgg{=o`QiHT9xEb+ejV0@FVd5049?uBg{No z;;Tz^VA3y^qiT%x$G7R4OlOvA-Yc4I$!tm~B>A7jbNwn=Oy{JLq+-PGVscNdAq`{l zuBMvmwrV8UTURZ0Xj4qyN$MD1if>y+vw4 zGJVYd02Kmck-Ik0)b^lPCJsR7q3cLwH(HIImAlV%GD8G>ypG#I9^}z-ta(?9zi53j z%$siyTYb7g{{Syje6Ww8A2WK4aZ=dP$@skEt~y zIj=bJ+vz|IEZ(#L)^A#O0XGUjIY|dJ0pI)}@o@0}0E-}7i2`Z5o-J>flY|E*IRN!p z2nX2vQ$T%B^E4_4BzGQz{A$rG^RE?X7ja)nJOKGJAD9fDd-3RMSX{wNE46Y;7KG;` zoQ!nhfVronXfj>hFP5Z7)${4!-@Yl>E_y|jLMY>oHQI_W6wpbb8+@FcQyC1h9&z_` zOb9&2&s>ArfEhEOh^}x>37{E?s%tP>LVj_(_;7zdo|FZTBJt0}q)8sy*}Tbi;|hLN zIUP^*scd?lQ-7-6-ppmVeaw4yskucaY=KAs5uDHgxC8A#2-pWJ(tsS9W5;>`n0?w- z03CUv!eiGyqZC*PBFO4#fn_HuGQ4A@0Bh-fEY_?fMzy?}9i1Z%N@LKe$j7w+dmn&2 zbsvZjS>LL~si@qnI7wHbE9iZw6^^#%9jk%PXcI*VP=SxufGJ6kv<&~!^(f_B(76!0Fljj&*C?Sd@lp7+~RBH0Hus%7h-y6&{AfWK6vrB#I1kD zbFjRWC6cybv_Z90cHNJ>N2#e}ITw`XqzK8)05g+70~}BTPbZoHK9m69O#n*^;N!8O z0rii;PZ*C9cuFL=Vf~?`?X_fyJI4#O^}-y8-p6K z?ec?22g74-iSi&^`V3P6Qr%U94%y@DNMcEVrz0!LG{k0HOjmb1NX~m=kPsWKiM1r|j}Gxy#7q54d=}5;z)vm)3Bk`EoYJ_?F{NfX z&Pn8x+|pn~Pc(oFaf$#yXaVu^aX=0r4m%nERa|lU&;)>vlmPBKb)Z%d3If8jkwEN8 z8#dgJ%7G7#Q=UZtXzBW$#kA=xE>c+7JZujkl;@A|pbv5IM~-d$L2LGFxdLi%E?Gtb zI{L4_sGv;gXM`2!kEH@K=L`uA`ctqU|I_s7&Q~L#?^=*g5d(?@aX%BjBSqnR4Ge{`(K*NtBWEMfLT~<;1r)QW+ zO&745KE8y;MFdz~pg~O-p5RoHvPnD5bizG0WyZ@WO{9j_iASVvb>@W6L#N zdizqDrMZ(i=!1nHhjZ^qurqh(B+>#l>ybbWpaq}rE2L<|;uL{C#P#NB`9H=)l1B>T5$JWO%>Dwq6<0E;S2{ zrckAAp+EA{LOxX=jRhm+ZEIM+)pZH)?_@GhI~HP2a0w(R_8kRH%1F*D(-3 zYX1Nbw0kH+yt|mLOLS3_fs^k=$I5vZi~cXcaP1OF=8%8Yp~(8@?v5!iNb_w|#oDdt zC>+BVqLaJmTdg6OjUzG1IVX}$NDU-V0tPeyW`GI+548X&By^wv6P%g>inz{bCgaeI zP!}b@`cN^zk4|V9;Mh5!29cgYqymC4IOc%?KGc{Bt%_)5$t!Ldi-Wf=~b1yP1u& literal 0 HcmV?d00001 diff --git a/readingbat-core/src/test/kotlin/TestData.kt b/readingbat-core/src/test/kotlin/TestData.kt index d1dee3e46..9617b1501 100644 --- a/readingbat-core/src/test/kotlin/TestData.kt +++ b/readingbat-core/src/test/kotlin/TestData.kt @@ -33,7 +33,7 @@ object TestData { readingBatContent { python { //repo = GitHubRepo(OwnerType.Organization, "readingbat", "readingbat-core") - //branchName = "1.9.0" + //branchName = "1.10.0" repo = FileSystemSource("../") srcPath = "python" diff --git a/readingbat-core/src/test/kotlin/com/github/readingbat/test_content/InfiniteLoop.kt b/readingbat-core/src/test/kotlin/com/github/readingbat/test_content/InfiniteLoop.kt index 0c7e3ddcf..8c84125b2 100644 --- a/readingbat-core/src/test/kotlin/com/github/readingbat/test_content/InfiniteLoop.kt +++ b/readingbat-core/src/test/kotlin/com/github/readingbat/test_content/InfiniteLoop.kt @@ -1,6 +1,6 @@ package com.github.readingbat.com.github.readingbat.test_content -fun deadEnd(i: Int): Boolean { +fun deadEnd(): Boolean { while (true) { println("I am stuck") Thread.sleep(1000) @@ -9,7 +9,7 @@ fun deadEnd(i: Int): Boolean { } fun main() { - println(deadEnd(1)) - println(deadEnd(2)) - println(deadEnd(3)) + println(deadEnd()) + println(deadEnd()) + println(deadEnd()) } \ No newline at end of file diff --git a/readingbat-kotest/src/main/kotlin/com/github/readingbat/kotest/TestSupport.kt b/readingbat-kotest/src/main/kotlin/com/github/readingbat/kotest/TestSupport.kt index 89a1c019b..39bbb874f 100644 --- a/readingbat-kotest/src/main/kotlin/com/github/readingbat/kotest/TestSupport.kt +++ b/readingbat-kotest/src/main/kotlin/com/github/readingbat/kotest/TestSupport.kt @@ -54,11 +54,13 @@ import kotlinx.coroutines.runBlocking class ChallengeAnswer(val funcInfo: FunctionInfo, val index: Int) +@Suppress("unused") class ChallengeResult(val answerStatus: AnswerStatus, val hint: String, val index: Int, val correctAnswer: String) +@Suppress("unused") object TestSupport { inline infix fun ReadingBatContent.forEachLanguage(block: LanguageGroup<*>.() -> Unit) = @@ -134,9 +136,9 @@ object TestSupport { fun ReadingBatContent.kotlinChallenge(groupName: String, challengeName: String, block: FunctionInfo.() -> Unit) = kotlinGroup(groupName).functionInfo(challengeName).apply(block) - fun ReadingBatContent.pythonGroup(name: String) = python.get(name) - fun ReadingBatContent.javaGroup(name: String) = java.get(name) - fun ReadingBatContent.kotlinGroup(name: String) = kotlin.get(name) + fun ReadingBatContent.pythonGroup(name: String) = python[name] + fun ReadingBatContent.javaGroup(name: String) = java[name] + fun ReadingBatContent.kotlinGroup(name: String) = kotlin[name] fun ChallengeGroup.challengeByName(name: String) = challenges.firstOrNull { it.challengeName.value == name } ?: error("Missing challenge $name") @@ -173,6 +175,7 @@ object TestSupport { fun Application.testModule(content: ReadingBatContent) { Property.IS_TESTING.setProperty("true") + Property.assignInitialized() installs(false) @@ -180,7 +183,7 @@ object TestSupport { adminRoutes(ReadingBatServer.metrics) locations(ReadingBatServer.metrics) { content } userRoutes(ReadingBatServer.metrics) { content } - sysAdminRoutes(ReadingBatServer.metrics) { s: String -> } + sysAdminRoutes(ReadingBatServer.metrics) { } wsRoutes(ReadingBatServer.metrics) { content } static(Endpoints.STATIC_ROOT) { resources("static") } } diff --git a/settings.gradle b/settings.gradle index 4215bf6d0..35b99697b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,20 +1,3 @@ -/* - * Copyright © 2020 Paul Ambrose (pambrose@mac.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - pluginManagement { repositories { maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }