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

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