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

Last change on this file since 1456 was 1456, checked in by business@…, 9 years ago

moved gdt package to org.dbnp, moved tests for RelTime?, TemplateEntity?, Template etc. to gdt plugin

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