source: trunk/grails-app/domain/dbnp/studycapturing/Study.groovy @ 996

Last change on this file since 996 was 996, checked in by robert@…, 10 years ago

Implemented clone feature in template editor (ticket #6), added a rest call 'getUser' and added webflow to the application.properties again.

  • Property svn:keywords set to Author Date Rev
File size: 13.1 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.authentication.SecUser
4
5/**
6 * Domain class describing the basic entity in the study capture part: the Study class.
7 *
8 * Revision information:
9 * $Rev: 996 $
10 * $Author: robert@isdat.nl $
11 * $Date: 2010-10-26 13:19:22 +0000 (di, 26 okt 2010) $
12 */
13class Study extends TemplateEntity {
14        static searchable = {
15            [only: ['title', 'Description']] // the description field will be searched only if defined in a study template
16        }
17
18        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
19        String title        // The title of the study
20        String code             // currently used as the external study ID, e.g. to reference a study in a SAM module
21        Date dateCreated
22        Date lastUpdated
23        Date startDate
24        List subjects
25        List events
26        List samplingEvents
27        List eventGroups
28        List samples
29        List assays
30        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
31    boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
32       
33        static hasMany = [             
34                subjects: Subject,
35                samplingEvents: SamplingEvent,
36                events: Event,
37                eventGroups: EventGroup,
38                samples: Sample,
39                assays: Assay,
40                persons: StudyPerson,
41                publications: Publication,
42                readers: SecUser,
43                writers: SecUser
44        ]
45
46        static constraints = {
47                owner(nullable: true, blank: true)
48                code(nullable:false, blank:true,unique:true)
49
50                // TODO: add custom validator for 'published' to assess whether the study meets all quality criteria for publication
51                // tested by SampleTests.testStudyPublish
52        }
53
54        static mapping = {
55                autoTimestamp true
56
57                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
58                templateTextFields type: 'text'
59        }
60
61        // The external identifier (studyToken) is currently the code of the study.
62        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
63        def getToken() { code }
64
65        /**
66         * return the domain fields for this domain class
67         * @return List
68         */
69        static List<TemplateField> giveDomainFields() { return Study.domainFields }
70
71        static final List<TemplateField> domainFields = [
72                new TemplateField(
73                        name: 'title',
74                        type: TemplateFieldType.STRING,
75                        required: true),
76                new TemplateField(
77                        name: 'code',
78                        type: TemplateFieldType.STRING,
79                        preferredIdentifier:true,
80                        comment: 'Fill out the code by which many people will recognize your study',
81                        required: true),
82                new TemplateField(
83                        name: 'startDate',
84                        type: TemplateFieldType.DATE,
85                        comment: 'Fill out the official start date or date of first action',
86                        required: true),
87                new TemplateField(
88                        name: 'published',
89                        type: TemplateFieldType.BOOLEAN,
90                        comment: 'Determines whether this study is published (accessible for the study readers and, if the study is public, for anonymous users). A study can only be published if it meets certain quality criteria, which will be checked upon save.',
91                        required: false)
92        ]
93
94        /**
95         * return the title of this study
96         */
97        def String toString() {
98                return title
99        }
100
101        /**
102         * returns all events and sampling events that do not belong to a group
103         */
104        def Set<Event> getOrphanEvents() {
105                def orphans =   events.findAll { event -> !event.belongsToGroup(eventGroups) } +
106                                                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
107
108                return orphans
109        }
110
111        /**
112         * Return the unique Subject templates that are used in this study
113         */
114        def Set<Template> giveSubjectTemplates() {
115                TemplateEntity.giveTemplates(subjects)
116        }
117
118        /**
119         * Return all subjects for a specific template
120         * @param Template
121         * @return ArrayList
122         */
123        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
124                subjects.findAll { it.template.equals(template) }
125        }
126
127        /**
128         * Return all unique assay templates
129         * @return Set
130         */
131        Set<Template> giveAllAssayTemplates() {
132                TemplateEntity.giveTemplates(( (assays) ? assays : [] ))
133        }
134
135        /**
136         * Return all assays for a particular template
137         * @return ArrayList
138         */
139        def ArrayList giveAssaysForTemplate(Template template) {
140                assays.findAll { it.template.equals(template) }
141        }
142
143        /**
144         * Return the unique Event and SamplingEvent templates that are used in this study
145         */
146        Set<Template> giveAllEventTemplates() {
147                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
148                // gives trouble when asking .size() to the result
149                // So we also use giveTemplates here
150                TemplateEntity.giveTemplates( ((events) ? events : []) + ((samplingEvents) ? samplingEvents : []) )
151        }
152
153
154        /**
155         * Return all events and samplingEvenets for a specific template
156         * @param Template
157         * @return ArrayList
158         */
159        def ArrayList giveEventsForTemplate(Template template) {
160                def events = events.findAll { it.template.equals(template) }
161                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
162
163                return (events) ? events : samplingEvents
164        }
165
166        /**
167         * Return the unique Event templates that are used in this study
168         */
169        Set<Template> giveEventTemplates() {
170                TemplateEntity.giveTemplates(events)
171        }
172
173        /**
174         * Return the unique SamplingEvent templates that are used in this study
175         */
176        Set<Template> giveSamplingEventTemplates() {
177                TemplateEntity.giveTemplates(samplingEvents)
178        }
179
180        /**
181         * Returns the unique Sample templates that are used in the study
182         */
183        Set<Template> giveSampleTemplates() {
184                TemplateEntity.giveTemplates(samples)
185        }
186
187        /**
188         * Return all samples for a specific template
189         * @param Template
190         * @return ArrayList
191         */
192        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
193                samples.findAll { it.template.equals(template) }
194        }
195
196        /**
197         * Returns the template of the study
198         */
199        Template giveStudyTemplate() {
200                return this.template
201        }
202
203
204        /**
205         * Delete a specific subject from this study, including all its relations
206         * @param subject The subject to be deleted
207         * @return A String which contains a (user-readable) message describing the changes to the database
208         */
209        String deleteSubject(Subject subject) {
210                String msg = "Subject ${subject.name} was deleted"
211
212                // Delete the subject from the event groups it was referenced in
213                this.eventGroups.each {
214                        if (it.subjects.contains(subject)) {
215                                it.removeFromSubjects(subject)
216                                msg += ", deleted from event group '${it.name}'"
217                        }
218                }
219
220                // Delete the samples that have this subject as parent
221                this.samples.findAll { it.parentSubject.equals(subject) }.each {
222                        // This should remove the sample itself too, because of the cascading belongsTo relation
223                        this.removeFromSamples(it)
224                        // But apparently it needs an explicit delete() too
225                        it.delete()
226                        msg += ", sample '${it.name}' was deleted"
227                }
228
229                // This should remove the subject itself too, because of the cascading belongsTo relation
230                this.removeFromSubjects(subject)
231                // But apparently it needs an explicit delete() too
232                subject.delete()
233
234                return msg
235        }
236
237        /**
238         * Delete an assay from the study
239         * @param Assay
240         * @void
241         */
242        def deleteAssay(Assay assay) {
243                if (assay && assay instanceof Assay) {
244                        // iterate through linked samples
245                        assay.samples.findAll { true }.each() { sample ->
246                                assay.removeFromSamples(sample)
247                        }
248
249                        // remove this assay from the study
250                        this.removeFromAssays(assay)
251
252                        // and delete it explicitly
253                        assay.delete()
254                }
255        }
256
257        /**
258         * Delete an event from the study, including all its relations
259         * @param Event
260         * @return String
261         */
262        String deleteEvent(Event event) {
263                String msg = "Event ${event} was deleted"
264
265                // remove event from the study
266                this.removeFromEvents(event)
267
268                // remove event from eventGroups
269                this.eventGroups.each() { eventGroup ->
270                        eventGroup.removeFromEvents(event)
271                }
272
273                return msg
274        }
275
276        /**
277         * Delete a samplingEvent from the study, including all its relations
278         * @param SamplingEvent
279         * @return String
280         */
281        String deleteSamplingEvent(SamplingEvent samplingEvent) {
282                String msg = "SamplingEvent ${samplingEvent} was deleted"
283
284                // remove event from eventGroups
285                this.eventGroups.each() { eventGroup ->
286                        eventGroup.removeFromSamplingEvents(samplingEvent)
287                }
288
289                // Delete the samples that have this sampling event as parent
290                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
291                        // This should remove the sample itself too, because of the cascading belongsTo relation
292                        this.removeFromSamples(it)
293                        // But apparently it needs an explicit delete() too
294                        it.delete()
295                        msg += ", sample '${it.name}' was deleted"
296                }
297
298                // Remove event from the study
299                // This should remove the event group itself too, because of the cascading belongsTo relation
300                this.removeFromSamplingEvents(samplingEvent)
301
302                // But apparently it needs an explicit delete() too
303                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
304                samplingEvent.delete()
305
306                return msg
307        }
308       
309        /**
310         * Delete an eventGroup from the study, including all its relations
311         * @param EventGroup
312         * @return String
313         */
314        String deleteEventGroup(EventGroup eventGroup) {
315                String msg = "EventGroup ${eventGroup} was deleted"
316
317                // If the event group contains sampling events
318                if (eventGroup.samplingEvents) {
319                        // remove all samples that originate from this eventGroup
320                        if (eventGroup.samplingEvents.size()) {
321                                // find all samples related to this eventGroup
322                                // - subject comparison is relatively straightforward and
323                                //   behaves as expected
324                                // - event comparison behaves strange, so now we compare
325                                //              1. database id's or,
326                                //              2. object identifiers or,
327                                //              3. objects itself
328                                //   this seems now to work as expected
329                                this.samples.findAll { sample ->
330                                        (
331                                                (eventGroup.subjects.findAll {
332                                                        it.equals(sample.parentSubject)
333                                                })
334                                                &&
335                                                (eventGroup.samplingEvents.findAll {
336                                                        (
337                                                                (it.id && sample.parentEvent.id && it.id==sample.parentEvent.id)
338                                                                ||
339                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
340                                                                ||
341                                                                it.equals(sample.parentEvent)
342                                                        )
343                                                })
344                                        )
345                                }.each() { sample ->
346                                        // remove sample from study
347
348                                        // -------
349                                        // NOTE, the right samples are found, but the don't
350                                        // get deleted from the database!
351                                        // -------
352
353                                        println ".removing sample '${sample.name}' from study '${this.title}'"
354                                        msg += ", sample '${sample.name}' was deleted"
355                                        this.removeFromSamples( sample )
356
357                                        // remove the sample from any sampling events it belongs to
358                                        this.samplingEvents.findAll { it.samples.any { it == sample }} .each {
359                                                println ".removed sample ${sample.name} from sampling event ${it} at ${it.getStartTimeString()}"
360                                                it.removeFromSamples(sample)
361                                        }
362
363                                        // remove the sample from any assays it belongs to
364                                        this.assays.findAll { it.samples.any { it == sample }} .each {
365                                                println ".removed sample ${sample.name} from assay ${it.name}"
366                                                it.removeFromSamples(sample)
367                                        }
368
369                                        // Also here, contrary to documentation, an extra delete() is needed
370                                        // otherwise date is not properly deleted!
371                                        sample.delete()
372                                }
373                        }
374
375                        // remove all samplingEvents from this eventGroup
376                        eventGroup.samplingEvents.findAll{}.each() {
377                                eventGroup.removeFromSamplingEvents(it)
378                                println ".removed samplingEvent '${it.name}' from eventGroup '${eventGroup.name}'"
379                                msg += ", samplingEvent '${it.name}' was removed from eventGroup '${eventGroup.name}'"
380                        }
381                }
382
383                // If the event group contains subjects
384                if (eventGroup.subjects) {
385                        // remove all subject from this eventGroup
386                        eventGroup.subjects.findAll{}.each() {
387                                eventGroup.removeFromSubjects(it)
388                                println ".removed subject '${it.name}' from eventGroup '${eventGroup.name}'"
389                                msg += ", subject '${it.name}' was removed from eventGroup '${eventGroup.name}'"
390                        }
391                }
392
393                // remove the eventGroup from the study
394                println ".remove eventGroup '${eventGroup.name}' from study '${this.title}'"
395                this.removeFromEventGroups(eventGroup)
396
397                // Also here, contrary to documentation, an extra delete() is needed
398                // otherwise cascaded deletes are not properly performed
399                eventGroup.delete()
400
401                return msg
402        }
403
404    /**
405     * Returns true if the given user is allowed to read this study
406     */
407    public boolean canRead(SecUser loggedInUser) {
408        // Anonymous readers are only given access when published and public
409        if( loggedInUser == null ) {
410            return this.publicstudy && this.published;
411        }
412
413        // Owners and writers are allowed to read this study
414        if( this.owner == loggedInUser || this.writers.contains(loggedInUser) ) {
415            return true
416        }
417           
418        // Readers are allowed to read this study when it is published
419        if( this.readers.contains(loggedInUser) && this.published ) {
420            return true
421        }
422       
423        return false
424    }
425
426    /**
427     * Returns true if the given user is allowed to write this study
428     */
429    public boolean canWrite(SecUser loggedInUser) {
430        if( loggedInUser == null ) {
431            return false;
432        }
433        return this.owner == loggedInUser || this.writers.contains(loggedInUser)
434    }
435
436    /**
437     * Returns true if the given user is the owner of this study
438     */
439    public boolean isOwner(SecUser loggedInUser) {
440        if( loggedInUser == null ) {
441            return false;
442        }
443        return this.owner == loggedInUser
444    }
445
446}
Note: See TracBrowser for help on using the repository browser.