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

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