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

Last change on this file since 1922 was 1922, checked in by work@…, 9 years ago
  • removed all default instance configuration in favor of just one default.properties file. Instance configuration should be on the server side, not within the code. The setup wizard will help you generate a configuration file...
  • removed obsolete references to searchable
  • removed obsolete references to grails melody
  • Property svn:keywords set to Rev Author Date
File size: 18.4 KB
Line 
1package dbnp.studycapturing
2import org.dbnp.gdt.*
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: 1922 $
10 * $Author: work@osx.eu $
11 * $Date: 2011-06-09 12:10:44 +0000 (do, 09 jun 2011) $
12 */
13class Study extends TemplateEntity {
14        def moduleNotificationService
15
16        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
17        String title            // The title of the study
18        String description      // A brief synopsis of what the study is about
19        String code                     // currently used as the external study ID, e.g. to reference a study in a SAM module
20        Date dateCreated
21        Date lastUpdated
22        Date startDate
23        List subjects
24        List events
25        List samplingEvents
26        List eventGroups
27        List samples
28        List assays
29        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
30        boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
31
32        /**
33         * UUID of this study
34         */
35        String studyUUID
36
37
38        static hasMany = [
39                subjects: Subject,
40                samplingEvents: SamplingEvent,
41                events: Event,
42                eventGroups: EventGroup,
43                samples: Sample,
44                assays: Assay,
45                persons: StudyPerson,
46                publications: Publication,
47                readers: SecUser,
48                writers: SecUser
49        ]
50
51        static constraints = {
52                title(nullable:false, blank: false, unique:true, maxSize: 255)
53                owner(nullable: true, blank: true)
54                code(nullable: true, blank: true, unique: true, maxSize: 255)
55                studyUUID(nullable:true, unique:true, maxSize: 255)
56                persons(size:1..1000)
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        // see org.dbnp.gdt.FuzzyStringMatchController and Service
62        static fuzzyStringMatchable = [
63            "title",
64                "code"
65        ]
66
67        static mapping = {
68                autoTimestamp true
69                sort "title"
70
71                // Make sure the TEXT field description is persisted with a TEXT field in the database
72                description type: 'text'
73                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
74                templateTextFields type: 'text'
75
76        }
77
78        // The external identifier (studyToken) is currently the code of the study.
79        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
80
81        def getToken() { return giveUUID() }
82
83        /**
84         * return the domain fields for this domain class
85         * @return List
86         */
87        static List<TemplateField> giveDomainFields() { return Study.domainFields }
88
89        static final List<TemplateField> domainFields = [
90                new TemplateField(
91                name: 'title',
92                type: TemplateFieldType.STRING,
93                required: true),
94                new TemplateField(
95                name: 'description',
96                type: TemplateFieldType.TEXT,
97                comment:'Give a brief synopsis of what your study is about',
98                required: true),
99                new TemplateField(
100                name: 'code',
101                type: TemplateFieldType.STRING,
102                preferredIdentifier: true,
103                comment: 'Fill out the code by which many people will recognize your study',
104                required: false),
105                new TemplateField(
106                name: 'startDate',
107                type: TemplateFieldType.DATE,
108                comment: 'Fill out the official start date or date of first action',
109                required: true),
110                new TemplateField(
111                name: 'published',
112                type: TemplateFieldType.BOOLEAN,
113                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.',
114                required: false)
115        ]
116
117        /**
118         * return the title of this study
119         */
120        def String toString() {
121                return ( (code) ? code : "[no code]") + " - "+ title
122        }
123
124        /**
125         * returns all events and sampling events that do not belong to a group
126         */
127        def List<Event> getOrphanEvents() {
128                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
129                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
130
131                return orphans
132        }
133
134        /**
135         * Return the unique Subject templates that are used in this study
136         */
137        def List<Template> giveSubjectTemplates() {
138                TemplateEntity.giveTemplates(subjects)
139        }
140
141        /**
142         * Return all subjects for a specific template
143         * @param Template
144         * @return ArrayList
145         */
146        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
147                subjects.findAll { it.template.equals(template) }
148        }
149
150        /**
151         * Return all unique assay templates
152         * @return Set
153         */
154        List<Template> giveAllAssayTemplates() {
155                TemplateEntity.giveTemplates(((assays) ? assays : []))
156        }
157
158        /**
159         * Return all assays for a particular template
160         * @return ArrayList
161         */
162        def ArrayList giveAssaysForTemplate(Template template) {
163                assays.findAll { it && it.template.equals(template) }
164        }
165
166        /**
167         * Return the unique Event and SamplingEvent templates that are used in this study
168         */
169        List<Template> giveAllEventTemplates() {
170                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
171                // gives trouble when asking .size() to the result
172                // So we also use giveTemplates here
173                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
174        }
175
176        /**
177         * Return all events and samplingEvenets for a specific template
178         * @param Template
179         * @return ArrayList
180         */
181        def ArrayList giveEventsForTemplate(Template template) {
182                def events = events.findAll { it.template.equals(template) }
183                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
184
185                return (events) ? events : samplingEvents
186        }
187
188        /**
189         * Return the unique Event templates that are used in this study
190         */
191        List<Template> giveEventTemplates() {
192                TemplateEntity.giveTemplates(events)
193        }
194
195        /**
196         * Return the unique SamplingEvent templates that are used in this study
197         */
198        List<Template> giveSamplingEventTemplates() {
199                TemplateEntity.giveTemplates(samplingEvents)
200        }
201
202        /**
203         * Returns the unique Sample templates that are used in the study
204         */
205        List<Template> giveSampleTemplates() {
206                TemplateEntity.giveTemplates(samples)
207        }
208
209        /**
210         * Return all samples for a specific template, sorted by subject name
211         * @param Template
212         * @return ArrayList
213         */
214        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
215                // sort in a concatenated string as sorting on 3 seperate elements
216                // in a map does not seem to work properly
217                samples.findAll { it.template.equals(template) }.sort {
218                        "${it.parentEvent?.template}|${it.parentEvent?.startTime}|${it.parentSubject?.name}".toLowerCase()
219                }
220        }
221
222        /**
223         * Returns the template of the study
224         */
225        Template giveStudyTemplate() {
226                return this.template
227        }
228
229        /**
230         * Delete a specific subject from this study, including all its relations
231         * @param subject The subject to be deleted
232         * @void
233         */
234        void deleteSubject(Subject subject) {
235                // Delete the subject from the event groups it was referenced in
236                this.eventGroups.each {
237                        if (it.subjects?.contains(subject)) {
238                                it.removeFromSubjects(subject)
239                        }
240                }
241
242                // Delete the samples that have this subject as parent
243                this.samples.findAll { it.parentSubject.equals(subject) }.each {
244                        this.deleteSample(it)
245                }
246
247                // This should remove the subject itself too, because of the cascading belongsTo relation
248                this.removeFromSubjects(subject)
249
250                // But apparently it needs an explicit delete() too
251                subject.delete()
252        }
253
254        /**
255         * Delete an assay from the study
256         * @param Assay
257         * @void
258         */
259        def deleteAssay(Assay assay) {
260                if (assay && assay instanceof Assay) {
261                        // iterate through linked samples
262                        assay.samples.findAll { true }.each() { sample ->
263                                assay.removeFromSamples(sample)
264                        }
265
266                        // remove this assay from the study
267                        this.removeFromAssays(assay)
268
269                        // and delete it explicitly
270                        assay.delete()
271                }
272        }
273
274        /**
275         * Delete an event from the study, including all its relations
276         * @param Event
277         * @void
278         */
279        void deleteEvent(Event event) {
280                // remove event from eventGroups
281                this.eventGroups.each() { eventGroup ->
282                        eventGroup.removeFromEvents(event)
283                }
284
285                // remove event from the study
286                this.removeFromEvents(event)
287
288                // and perform a hard delete
289                event.delete()
290        }
291
292        /**
293         * Delete a sample from the study, including all its relations
294         * @param Event
295         * @void
296         */
297        void deleteSample(Sample sample) {
298                // remove the sample from the study
299                this.removeFromSamples(sample)
300
301                // remove the sample from any sampling events it belongs to
302                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
303                        it.removeFromSamples(sample)
304                }
305
306                // remove the sample from any assays it belongs to
307                this.assays.findAll { it.samples.any { it == sample }}.each {
308                        it.removeFromSamples(sample)
309                }
310
311                // Also here, contrary to documentation, an extra delete() is needed
312                // otherwise date is not properly deleted!
313                sample.delete()
314        }
315
316        /**
317         * Delete a samplingEvent from the study, including all its relations
318         * @param SamplingEvent
319         * @void
320         */
321        void deleteSamplingEvent(SamplingEvent samplingEvent) {
322                // remove event from eventGroups
323                this.eventGroups.each() { eventGroup ->
324                        eventGroup.removeFromSamplingEvents(samplingEvent)
325                }
326
327                // Delete the samples that have this sampling event as parent
328                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
329                        // This should remove the sample itself too, because of the cascading belongsTo relation
330                        this.deleteSample(it)
331                }
332
333                // Remove event from the study
334                // This should remove the event group itself too, because of the cascading belongsTo relation
335                this.removeFromSamplingEvents(samplingEvent)
336
337                // But apparently it needs an explicit delete() too
338                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
339                samplingEvent.delete()
340        }
341
342        /**
343         * Delete an eventGroup from the study, including all its relations
344         * @param EventGroup
345         * @void
346         */
347        void deleteEventGroup(EventGroup eventGroup) {
348                // If the event group contains sampling events
349                if (eventGroup.samplingEvents) {
350                        // remove all samples that originate from this eventGroup
351                        if (eventGroup.samplingEvents.size()) {
352                                // find all samples that
353                                //      - are part of this study
354                                this.samples.findAll { sample ->
355                                        (
356                                                // - belong to this eventGroup
357                                                (
358                                                        (sample.parentEventGroup.id && eventGroup.id && sample.parentEventGroup.id == eventGroup.id)
359                                                        ||
360                                                        (sample.parentEventGroup.getIdentifier() == eventGroup.getIdentifier())
361                                                        ||
362                                                        sample.parentEventGroup.equals(eventGroup)
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, Integer max = null) {
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.listDistinct {
460                                if (max != null) maxResults(max)
461                                order("title", "asc")
462                               
463                        }
464                }
465
466                return c.listDistinct {
467                        if (max != null) maxResults(max)
468                        order("title", "asc")
469                        or {
470                                eq("owner", user)
471                                writers {
472                                        eq("id", user.id)
473                                }
474                        }
475                }
476        }
477
478        /**
479         * Returns a list of studies that are readable by the given user
480         */
481        public static giveReadableStudies(SecUser user, Integer max = null, int offset = 0) {
482                def c = Study.createCriteria()
483
484                // Administrators are allowed to read everything
485                if (user == null) {
486                        return c.listDistinct {
487                                if (max != null) maxResults(max)
488                                firstResult(offset)
489                                order("title", "asc")
490                                and {
491                                        eq("published", true)
492                                        eq("publicstudy", true)
493                                }
494                        }
495                } else if (user.hasAdminRights()) {
496                        return c.listDistinct {
497                                if (max != null) maxResults(max)
498                                firstResult(offset)
499                                order("title", "asc")
500                        }
501                } else {
502                        return c.listDistinct {
503                                if (max != null) maxResults(max)
504                                firstResult(offset)
505                                order("title", "asc")
506                                or {
507                                        eq("owner", user)
508                                        writers {
509                                                eq("id", user.id)
510                                        }
511                                        and {
512                                                readers {
513                                                        eq("id", user.id)
514                                                }
515                                                eq("published", true)
516                                        }
517                                }
518                        }
519                }
520        }
521
522        /**
523         * perform a text search on studies
524         * @param query
525         * @return
526         */
527        public static textSearchReadableStudies(SecUser user, String query) {
528                def c = Study.createCriteria()
529
530                if (user == null) {
531                        // regular user
532                        return c.listDistinct {
533                                or {
534                                        ilike("title", "%${query}%")
535                                        ilike("description", "%${query}%")
536                                }
537                                and {
538                                        eq("published", true)
539                                        eq("publicstudy", true)
540                                }
541                        }
542                } else if (user.hasAdminRights()) {
543                        // admin can search everything
544                        return c.listDistinct {
545                                or {
546                                        ilike("title", "%${query}%")
547                                        ilike("description", "%${query}%")
548                                }
549                        }
550                } else {
551                        return c.listDistinct {
552                                or {
553                                        ilike("title", "%${query}%")
554                                        ilike("description", "%${query}%")
555                                }
556                                and {
557                                        or {
558                                                eq("owner", user)
559                                                writers {
560                                                        eq("id", user.id)
561                                                }
562                                                and {
563                                                        readers {
564                                                                eq("id", user.id)
565                                                        }
566                                                        eq("published", true)
567                                                }
568                                        }
569                                }
570                        }
571
572                }
573        }
574
575        /**
576         * Returns the number of public studies
577         * @return int
578         */
579        public static countPublicStudies() { return countPublicStudies(true) }
580        public static countPublicStudies(boolean published) {
581                def c = Study.createCriteria()
582                return (c.listDistinct {
583                        and {
584                                eq("published", published)
585                                eq("publicstudy", true)
586                        }
587                }).size()
588        }
589
590        /**
591         * Returns the number of private studies
592         * @return int
593         */
594        public static countPrivateStudies() { return countPrivateStudies(false) }
595        public static countPrivateStudies(boolean published) {
596                def c = Study.createCriteria()
597                return (c.listDistinct {
598                        and {
599                                eq("publicstudy", false)
600                        }
601                        or {
602                                eq("published", published)
603                                eq("publicstudy", true)
604                        }
605                }).size()
606        }
607
608        /**
609         * Returns the number of studies that are readable by the given user
610         */
611        public static countReadableStudies(SecUser user) {
612                def c = Study.createCriteria()
613
614                // got a user?
615                if (user == null) {
616                        return c.count {
617                                and {
618                                        eq("published", true)
619                                        eq("publicstudy", true)
620                                }
621                        }
622                } else if (user.hasAdminRights()) {
623                        // Administrators are allowed to read everything
624                        return Study.count()
625                } else {
626                        return (c.listDistinct {
627                                or {
628                                        eq("owner", user)
629                                        writers {
630                                                eq("id", user.id)
631                                        }
632                                        and {
633                                                readers {
634                                                        eq("id", user.id)
635                                                }
636                                                eq("published", true)
637                                        }
638                                }
639                        }).size()
640                }
641        }
642
643        /**
644         * Returns the number of studies that are readable & writable by the given user
645         */
646        public static countReadableAndWritableStudies(SecUser user) {
647                def c = Study.createCriteria()
648
649                // got a user?
650                if (user == null) {
651                        return 0
652                } else if (user.hasAdminRights()) {
653                        return Study.count()
654                } else {
655                        return (c.listDistinct {
656                                or {
657                                        eq("owner", user)
658                                        writers {
659                                                eq("id", user.id)
660                                        }
661                                }
662                        }).size()
663                }
664        }
665
666        /**
667         * Returns the UUID of this study and generates one if needed
668         */
669        public String giveUUID() {
670                if( !this.studyUUID ) {
671                        this.studyUUID = UUID.randomUUID().toString();
672                        if( !this.save(flush:true) ) {
673                                log.error "Couldn't save study UUID: " + this.getErrors();
674                        }
675                }
676
677                return this.studyUUID;
678        }
679
680        /**
681         * Basic equals method to check whether objects are equals, by comparing the ids
682         * @param o             Object to compare with
683         * @return              True iff the id of the given Study is equal to the id of this Study
684         */
685        public boolean equals( Object o ) {
686                if( o == null )
687                        return false;
688
689                if( !( o instanceof Study ) )
690                        return false
691
692                Study s = (Study) o;
693
694                return this.id == s.id
695        }
696
697    // This closure is used in the before{Insert,Update,Delete} closures below.
698    // It is necessary to prevent flushing in the same session as a top level
699    // database action such as 'save' or 'addTo...'. This confuses hibernate and
700    // produces hard to trace errors.
701    // The same holds for flushing during validation (but that's not the case
702    // here).
703    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
704    static manualFlush(closure) {
705        withSession {session ->
706            def save
707            try {
708                save = session.flushMode
709                session.flushMode = org.hibernate.FlushMode.MANUAL
710                closure()
711            } finally {
712                if (save) {
713                    session.flushMode = save
714                }
715         }
716        }
717    }
718
719        // Send messages to modules about changes in this study
720        def beforeInsert = {
721        manualFlush{
722            moduleNotificationService.invalidateStudy( this )
723        }
724        }
725        def beforeUpdate = {
726        manualFlush{
727            moduleNotificationService.invalidateStudy( this )
728        }
729        }
730        def beforeDelete = {
731                manualFlush{
732            moduleNotificationService.invalidateStudy( this )
733        }
734        }
735    }
Note: See TracBrowser for help on using the repository browser.