root/trunk/grails-app/domain/dbnp/studycapturing/Study.groovy @ 1452

Revision 1452, 13.7 KB (checked in by work@…, 3 years ago)

- changed gdt imports
- added default searchable config
- did some debuggin on #227 but issue is still there... also if searchable is enabled the project becomes really slow... perhaps we need to get rid of searchable and implement search fnctionality in another way?

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