Skip to content

Commit

Permalink
Release 1.0.0-alpha.14
Browse files Browse the repository at this point in the history
Merge pull request #114 from cph-cachet/develop
  • Loading branch information
Whathecode authored Mar 26, 2020
2 parents 3bca375 + d9238cc commit cca4126
Show file tree
Hide file tree
Showing 23 changed files with 598 additions and 78 deletions.
14 changes: 7 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ buildscript {
// Version used for all submodule artifacts.
// Snapshot publishing changes (or adds) the suffix after '-' with 'SNAPSHOT' prior to publishing.
// The 'publishSigned' task publishes to SonaType's staging repo and 'publishSnapshot' instantly uploads to the snapshots repo.
globalVersion = '1.0.0-alpha.13'
globalVersion = '1.0.0-alpha.14'

versions = [
// Kotlin multiplatform versions.
kotlin:'1.3.70',
kotlin:'1.3.71',
serialization:'0.20.0',
coroutines:'1.3.4',
coroutines:'1.3.5',

// JVM versions.
jvmTarget:'1.6',
jUnit5:'5.6.0',
jUnit5:'5.6.1',
dokkaPlugin:'0.9.18',

// JS versions.
nodePlugin:'2.2.3',
node:'13.10.1',
mocha:'7.1.0',
node:'13.11.0',
mocha:'7.1.1',

// DevOps versions.
detektPlugin:'1.7.0-beta1',
detektPlugin:'1.7.0',
nexusReleasePlugin:'0.21.2'
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ data class StudyInvitation(
/**
* A descriptive name for the study to be shown to participants.
*/
val name: String
val name: String,
/**
* A description of the study clarifying to participants what it is about.
*/
val description: String
)
{
companion object
{
/**
* Initializes a [StudyInvitation] with blank values for all fields.
*/
fun empty(): StudyInvitation = StudyInvitation( "" )
fun empty(): StudyInvitation = StudyInvitation( "", "" )
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ abstract class DeploymentServiceTest
val invitation = StudyInvitation.empty()
deploymentService.addParticipation( studyDeploymentId, setOf( deviceRoleName ), emailIdentity, invitation )

val differentInvitation = StudyInvitation( "Different" )
val differentInvitation = StudyInvitation( "Different", "New description" )
assertFailsWith<IllegalStateException>
{
deploymentService.addParticipation( studyDeploymentId, setOf( deviceRoleName ), emailIdentity, differentInvitation )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class StudyInvitationTest
@Test
fun can_serialize_and_deserialize_study_description_using_JSON()
{
val invitation = StudyInvitation( "Test" )
val invitation = StudyInvitation( "Test", "Description" )

val serialized = invitation.toJson()
val parsed = StudyInvitation.fromJson( serialized )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dk.cachet.carp.common.EmailAddress
import dk.cachet.carp.common.UUID
import dk.cachet.carp.deployment.domain.users.StudyInvitation
import dk.cachet.carp.protocols.domain.StudyProtocolSnapshot
import dk.cachet.carp.studies.domain.StudyDetails
import dk.cachet.carp.studies.domain.users.StudyOwner
import dk.cachet.carp.studies.domain.StudyStatus
import dk.cachet.carp.studies.domain.users.AssignParticipantDevices
Expand All @@ -13,17 +14,46 @@ import dk.cachet.carp.studies.domain.users.Participant
/**
* Application service which allows creating and managing studies.
*/
@Suppress( "TooManyFunctions" ) // TODO: Perhaps split up participation management from main interface.
interface StudyService
{
/**
* Create a new study for the specified [owner].
*/
suspend fun createStudy(
owner: StudyOwner,
/**
* A descriptive name for the study, assigned by, and only visible to, the [owner].
*/
name: String,
/**
* An optional description of the study, assigned by, and only visible to, the [owner].
*/
description: String = "",
/**
* An optional description of the study, shared with participants once they are invited.
* In case no description is specified, [name] is used as the name in [invitation].
*/
invitation: StudyInvitation? = null
): StudyStatus

/**
* Set study details which are visible only to the [StudyOwner].
*
* @param studyId The id of the study to update the study details for.
* @param name A descriptive name for the study.
* @param description A description of the study.
*
* @param name A descriptive name for the study, assigned by, and only visible to, the [owner].
* @param invitation
* An optional description of the study, shared with participants once they are invited.
* In case no description is specified, [name] is used as the name in [invitation].
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
suspend fun createStudy( owner: StudyOwner, name: String, invitation: StudyInvitation? = null ): StudyStatus
suspend fun setInternalDescription( studyId: UUID, name: String, description: String ): StudyStatus

/**
* Gets detailed information about the study with the specified [studyId], including which study protocol is set.
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
suspend fun getStudyDetails( studyId: UUID ): StudyDetails

/**
* Get the status for a study with the given [studyId].
Expand Down Expand Up @@ -54,12 +84,20 @@ interface StudyService
*/
suspend fun getParticipants( studyId: UUID ): List<Participant>

/**
* Specify an [invitation], shared with participants once they are invited to the study with the specified [studyId].
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
suspend fun setInvitation( studyId: UUID, invitation: StudyInvitation ): StudyStatus

/**
* Specify the study [protocol] to use for the study with the specified [studyId].
*
* @throws IllegalArgumentException when a study with [studyId] does not exist,
* when the provided [protocol] snapshot is invalid,
* or when the protocol contains errors preventing it from being used in deployments.
* @throws IllegalStateException when the study protocol can no longer be set since the study went 'live'.
*/
suspend fun setProtocol( studyId: UUID, protocol: StudyProtocolSnapshot ): StudyStatus

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dk.cachet.carp.deployment.domain.users.StudyInvitation
import dk.cachet.carp.protocols.domain.InvalidConfigurationError
import dk.cachet.carp.protocols.domain.StudyProtocolSnapshot
import dk.cachet.carp.studies.domain.Study
import dk.cachet.carp.studies.domain.StudyDetails
import dk.cachet.carp.studies.domain.StudyRepository
import dk.cachet.carp.studies.domain.StudyStatus
import dk.cachet.carp.studies.domain.users.AssignParticipantDevices
Expand All @@ -22,29 +23,82 @@ import dk.cachet.carp.studies.domain.users.StudyOwner
/**
* Implementation of [StudyService] which allows creating and managing studies.
*/
@Suppress( "TooManyFunctions" ) // TODO: Perhaps split up participation management from main interface.
class StudyServiceHost(
private val repository: StudyRepository,
private val deploymentService: DeploymentService
) : StudyService
{
/**
* Create a new study for the specified [owner].
*
* @param name A descriptive name for the study, assigned by, and only visible to, the [owner].
* @param invitation
* An optional description of the study, shared with participants once they are invited.
* In case no description is specified, [name] is used as the name in [invitation].
*/
override suspend fun createStudy( owner: StudyOwner, name: String, invitation: StudyInvitation? ): StudyStatus
override suspend fun createStudy(
owner: StudyOwner,
/**
* A descriptive name for the study, assigned by, and only visible to, the [owner].
*/
name: String,
/**
* An optional description of the study, assigned by, and only visible to, the [owner].
*/
description: String,
/**
* An optional description of the study, shared with participants once they are invited.
* In case no description is specified, [name] is used as the name in [invitation].
*/
invitation: StudyInvitation?
): StudyStatus
{
val ensuredInvitation = invitation ?: StudyInvitation( name )
val study = Study( owner, name, ensuredInvitation )
val ensuredInvitation = invitation ?: StudyInvitation( name, "" )
val study = Study( owner, name, description, ensuredInvitation )

repository.add( study )

return study.getStatus()
}

/**
* Set study details which are visible only to the [StudyOwner].
*
* @param studyId The id of the study to update the study details for.
* @param name A descriptive name for the study.
* @param description A description of the study.
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
override suspend fun setInternalDescription( studyId: UUID, name: String, description: String ): StudyStatus
{
val study = repository.getById( studyId )
require( study != null )

study.name = name
study.description = description
repository.update( study )

return study.getStatus()
}

/**
* Gets detailed information about the study with the specified [studyId], including which study protocol is set.
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
override suspend fun getStudyDetails( studyId: UUID ): StudyDetails
{
val study: Study? = repository.getById( studyId )
require( study != null )

return StudyDetails(
study.id,
study.owner,
study.name,
study.creationDate,
study.description,
study.invitation,
study.protocolSnapshot
)
}

/**
* Get the status for a study with the given [studyId].
*
Expand Down Expand Up @@ -96,12 +150,29 @@ class StudyServiceHost(
override suspend fun getParticipants( studyId: UUID ): List<Participant> =
repository.getParticipants( studyId )

/**
* Specify an [invitation], shared with participants once they are invited to the study with the specified [studyId].
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
override suspend fun setInvitation( studyId: UUID, invitation: StudyInvitation ): StudyStatus
{
val study: Study? = repository.getById( studyId )
require( study != null )

study.invitation = invitation
repository.update( study )

return study.getStatus()
}

/**
* Specify the study [protocol] to use for the study with the specified [studyId].
*
* @throws IllegalArgumentException when a study with [studyId] does not exist,
* when the provided [protocol] snapshot is invalid,
* or when the protocol contains errors preventing it from being used in deployments.
* @throws IllegalStateException when the study protocol can no longer be set since the study went 'live'.
*/
override suspend fun setProtocol( studyId: UUID, protocol: StudyProtocolSnapshot ): StudyStatus
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ class Study(
* A descriptive name for the study, assigned by, and only visible to, the [StudyOwner].
*/
name: String,
/**
* A description for the study, assigned by, and only visible to, the [StudyOwner].
*/
description: String = "",
/**
* A description of the study, shared with participants once they are invited to the study.
*/
val invitation: StudyInvitation = StudyInvitation.empty(),
invitation: StudyInvitation = StudyInvitation.empty(),
val id: UUID = UUID.randomUUID()
) : AggregateRoot<Study, StudySnapshot, Study.Event>()
{
sealed class Event : Immutable()
{
data class NameChanged( val name: String ) : Event()
data class InternalDescriptionChanged( val name: String, val description: String ) : Event()
data class InvitationChanged( val invitation: StudyInvitation ) : Event()
data class ProtocolSnapshotChanged( val protocolSnapshot: StudyProtocolSnapshot? ) : Event()
data class StateChanged( val isLive: Boolean ) : Event()
data class ParticipationAdded( val participation: DeanonymizedParticipation ) : Event()
Expand All @@ -46,7 +51,7 @@ class Study(
{
fun fromSnapshot( snapshot: StudySnapshot ): Study
{
val study = Study( StudyOwner( snapshot.ownerId ), snapshot.name, snapshot.invitation, snapshot.studyId )
val study = Study( StudyOwner( snapshot.ownerId ), snapshot.name, snapshot.description, snapshot.invitation, snapshot.studyId )
study.creationDate = snapshot.creationDate
study.protocolSnapshot = snapshot.protocolSnapshot
study.isLive = snapshot.isLive
Expand All @@ -57,11 +62,43 @@ class Study(
}


/**
* A descriptive name for the study, assigned by, and only visible to, the [StudyOwner].
*/
var name: String = name
set( value )
{
field = value
event( Event.NameChanged( name ) )
event( Event.InternalDescriptionChanged( name, description ) )
}

/**
* A description for the study, assigned by, and only visible to, the [StudyOwner].
*/
var description: String = description
set( value )
{
field = value
event( Event.InternalDescriptionChanged( name, description ) )
}

val canSetInvitation: Boolean get() = !isLive

/**
* A description of the study, shared with participants once they are invited to the study.
*/
var invitation: StudyInvitation = invitation
/**
* Set a new description of the study, to be shared with participants once they are invited to the study.
*
* @throws IllegalStateException when the invitation can no longer be changed since the study went 'live'.
*/
set( value )
{
check( !isLive ) { "Can't change invitation since this study already went live." }

field = value
event( Event.InvitationChanged( invitation ) )
}

/**
Expand All @@ -73,7 +110,11 @@ class Study(
/**
* Get the status (serializable) of this [Study].
*/
fun getStatus(): StudyStatus = StudyStatus( id, name, creationDate, canDeployToParticipants, isLive )
fun getStatus(): StudyStatus =
if ( isLive ) StudyStatus.Live( id, name, creationDate, canSetInvitation, canSetStudyProtocol, canDeployToParticipants )
else StudyStatus.Configuring( id, name, creationDate, canSetInvitation, canSetStudyProtocol, canDeployToParticipants, canGoLive )

val canSetStudyProtocol: Boolean get() = !isLive

/**
* A snapshot of the protocol to use in this study, or null when not yet defined.
Expand All @@ -89,7 +130,7 @@ class Study(
*/
set( value )
{
check( !isLive )
check( !isLive ) { "Can't set protocol since this study already went live." }
if ( value != null )
{
val protocol = StudyProtocol.fromSnapshot( value )
Expand All @@ -107,6 +148,8 @@ class Study(
var isLive: Boolean = false
private set

private val canGoLive: Boolean get() = protocolSnapshot != null

/**
* Lock in the current study protocol so that the study may be deployed to participants.
*
Expand Down
Loading

0 comments on commit cca4126

Please sign in to comment.