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

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

Implemented module notification when a study changes (#259)

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