001/*
002 *  Copyright 2021 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.forms.repository;
017
018import java.time.LocalDate;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.stream.Collectors;
026
027import javax.jcr.Node;
028
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.cms.data.ametysobject.ModifiableModelAwareDataAwareAmetysObject;
032import org.ametys.cms.data.holder.ModifiableIndexableDataHolder;
033import org.ametys.cms.data.holder.impl.DefaultModifiableModelAwareDataHolder;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.plugins.forms.FormAndDirectoryCommonMethods;
036import org.ametys.plugins.forms.dao.FormEntryDAO;
037import org.ametys.plugins.forms.question.FormQuestionType;
038import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
039import org.ametys.plugins.forms.repository.type.Rule;
040import org.ametys.plugins.repository.AmetysObject;
041import org.ametys.plugins.repository.AmetysRepositoryException;
042import org.ametys.plugins.repository.CopiableAmetysObject;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.repository.MovableAmetysObject;
045import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
046import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
047import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
048import org.ametys.web.repository.SiteAwareAmetysObject;
049import org.ametys.web.repository.site.Site;
050
051/**
052 * Class representing a form, backed by a JCR node.<br>
053 */
054public class Form extends DefaultTraversableAmetysObject<FormFactory> implements ModifiableModelAwareDataAwareAmetysObject, MovableAmetysObject, SiteAwareAmetysObject, CopiableAmetysObject
055{
056    /** Property name for form displayed name */
057    public static final String TITLE = "title";
058    /** Property name for form description */
059    public static final String DESCRIPTION = "description";
060    /** Property name for form author */
061    public static final String AUTHOR = "author";
062    /** Property name for form last contributor */
063    public static final String CONTRIBUTOR = "contributor";
064    /** Property name for form creation date */
065    public static final String CREATIONDATE = "creationDate";
066    /** Property name for form last modification date */
067    public static final String LASTMODIFICATIONDATE = "lastModificationDate";
068    /** Property name for form workflow name */
069    public static final String WORKFLOWNAME = "workflowName";
070    /** Property name for form to limit one entry by user */
071    public static final String LIMIT_TO_ONE_ENTRY_BY_USER = "limit-to-one-entry-by-user";
072    /** Property name for form to limit the entries */
073    public static final String LIMIT_ENTRIES_ENABLED = "limit-entries-enabled";
074    /** Property name for form to enable queue */
075    public static final String QUEUE_ENABLED = "queue-enabled";
076    /** Property name for form's queue size */
077    public static final String QUEUE_SIZE = "queue-size";
078    /** Property name for form displayed message when closed because of limited number of entries and a queue is enabled */
079    public static final String QUEUE_CLOSED_MSG = "queue-closed-message";
080    /** Property name for form's authorized max number of entries */
081    public static final String MAX_ENTRIES = "max-entries";
082    /** Property name for form displayed message when entries are limited in number  */
083    public static final String REMAINING_MSG = "remaining-message";
084    /** Property name for form displayed message when closed because of limited number of entries */
085    public static final String CLOSED_MSG = "closed-message";
086    /** Property name for form entries admin emails */
087    public static final String ADMIN_EMAILS = "admin-emails";
088    /** Property name for form acknowledgement of receipt's sender */
089    public static final String RECEIPT_SENDER = "receipt-sender";
090    /** Property name for form acknowledgement of receipt's receiver*/
091    public static final String RECEIPT_RECEIVER = "receipt-receiver";
092    /** Property name for form acknowledgement of receipt's subject */
093    public static final String RECEIPT_SUBJECT = "receipt-subject";
094    /** Property name for form acknowledgement of receipt's body */
095    public static final String RECEIPT_BODY = "receipt-body";
096    /** Property name for form acknowledgement of queue's sender */
097    public static final String QUEUE_SENDER = "queue-sender";
098    /** Property name for form acknowledgement of queue's receiver*/
099    public static final String QUEUE_RECEIVER = "queue-receiver";
100    /** Property name for form acknowledgement of queue's subject */
101    public static final String QUEUE_SUBJECT = "queue-subject";
102    /** Property name for form acknowledgement of queue's body */
103    public static final String QUEUE_BODY = "queue-body";
104    /** Property name for form start date */
105    public static final String START_DATE = "startDate";
106    /** Property name for form end date */
107    public static final String END_DATE = "endDate";
108    
109    /**
110     * Creates an {@link Form}.
111     * @param node the node backing this {@link AmetysObject}
112     * @param parentPath the parentPath in the Ametys hierarchy
113     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
114     */
115    public Form(Node node, String parentPath, FormFactory factory)
116    {
117        super(node, parentPath, factory);
118    }
119    
120    /**
121     * Set the title of this form.
122     * @param title the short title
123     */
124    public void setTitle (String title)
125    {
126        this.setValue(TITLE, title);
127    }
128    
129    /**
130     * Set the description of this form.
131     * @param description the description
132     */
133    public void setDescription (String description)
134    {
135        this.setValue(DESCRIPTION, description);
136    }
137    
138    /**
139     * Set the author of this form.
140     * @param author the author
141     */
142    public void setAuthor(UserIdentity author)
143    {
144        this.setValue(AUTHOR, author);
145    }
146
147    /**
148     * Set the last contributor of this form.
149     * @param contributor the last contributor
150     */
151    public void setContributor(UserIdentity contributor)
152    {
153        this.setValue(CONTRIBUTOR, contributor);
154    }
155    
156    /**
157     * Set the date of the creation.
158     * @param creationDate the last modification date to set.
159     */
160    public void setCreationDate(ZonedDateTime creationDate)
161    {
162        this.setValue(CREATIONDATE, creationDate);
163    }
164    
165    /**
166     * Set the date of the last modification.
167     * @param lastModificationDate the last modification date to set.
168     */
169    public void setLastModificationDate(ZonedDateTime lastModificationDate)
170    {
171        this.setValue(LASTMODIFICATIONDATE, lastModificationDate);
172    }
173    
174    /**
175     * Get the title of the form
176     * @return The title
177     */
178    public String getTitle()
179    {        
180        return this.getValue(TITLE);
181    }
182    
183    /**
184     * Get the description of the form
185     * @return The description
186     */
187    public String getDescription()
188    {
189        return this.getValue(DESCRIPTION);
190    }
191    
192    /**
193     * Get the author of the form
194     * @return The author
195     */
196    public UserIdentity getAuthor ()
197    {
198        return this.getValue(AUTHOR);
199    }
200    
201    /**
202     * Get the last contributor of the form
203     * @return The contributor
204     */
205    public UserIdentity getContributor()
206    {
207        return this.getValue(CONTRIBUTOR);
208    }
209    
210    /**
211     * Get the date of the last modification of the form
212     * @return The date
213     */
214    public ZonedDateTime getCreationDate()
215    {
216        return this.getValue(CREATIONDATE);
217    }
218    
219    /**
220     * Get the date of the last modification of the form
221     * @return The date
222     */
223    public ZonedDateTime getLastModificationDate()
224    {
225        return this.getValue(LASTMODIFICATIONDATE);
226    }
227    
228    /**
229     * Get the name of the workflow applied to the form
230     * @return The workflow name
231     */
232    public String getWorkflowName()
233    {
234        return this.getValue(WORKFLOWNAME);
235    }
236    
237    /**
238     * Set the name of the workflow.
239     * @param workflowName the name of the workflow to set.
240     */
241    public void setWorkflowName(String workflowName)
242    {
243        this.setValue(WORKFLOWNAME, workflowName);
244    }
245    
246    /**
247     * Get the receipt's sender of this form.
248     * @return The receipt's sender
249     */
250    public Optional<String> getReceiptSender()
251    {
252        return Optional.ofNullable(getValue(RECEIPT_SENDER));
253    }
254    
255    /**
256     * Set the receipt's sender of this form.
257     * @param sender The receipt's sender
258     */
259    public void setReceiptSender(String sender)
260    {
261        this.setValue(RECEIPT_SENDER, sender);
262    }
263    /**
264     * Get the receipt's receiver of this form.
265     * @return The receipt's receiver
266     */
267    public Optional<String> getReceiptReceiver()
268    {
269        return Optional.ofNullable(getValue(RECEIPT_RECEIVER));
270    }
271    
272    /**
273     * Set the receipt's receiver of this form.
274     * @param receiver The receipt's receiver
275     */
276    public void setReceiptReceiver(String receiver)
277    {
278        this.setValue(RECEIPT_RECEIVER, receiver);
279    }
280    /**
281     * Get the receipt's subject of this form.
282     * @return The receipt's subject
283     */
284    public Optional<String> getReceiptSubject()
285    {
286        return Optional.ofNullable(getValue(RECEIPT_SUBJECT));
287    }
288    
289    /**
290     * Set the receipt's subject of this form.
291     * @param subject The receipt's subject
292     */
293    public void setReceiptSubject(String subject)
294    {
295        this.setValue(RECEIPT_SUBJECT, subject);
296    }
297    /**
298     * Get the receipt's body of this form.
299     * @return The receipt's body
300     */
301    public Optional<String> getReceiptBody()
302    {
303        return Optional.ofNullable(getValue(RECEIPT_BODY));
304    }
305    
306    /**
307     * Set the receipt's body of this form.
308     * @param body The receipt's body
309     */
310    public void setReceiptBody(String body)
311    {
312        this.setValue(RECEIPT_BODY, body);
313    }
314    
315    /**
316     * Indicate is this form can be considered as a mini survey
317     * @return true if the form as only one question of type list and is limited to one answer by user
318     */
319    public boolean isMiniSurvey()
320    {
321        // do the cheap test before any expensive computation
322        // Mini survey is restricted to only one entry by user
323        if (!this.isLimitedToOneEntryByUser())
324        {
325            return false;
326        }
327        else
328        {
329            // There must be exactly one question that is not display only. And this question must be a choice list
330            boolean hasAQuestion = false;
331            for (FormQuestion question : this.getQuestions())
332            {
333                FormQuestionType type = question.getType();
334                if (!type.onlyForDisplay(question))
335                {
336                    // first ChoicesListQuestion
337                    if (type instanceof ChoicesListQuestionType && !hasAQuestion)
338                    {
339                        hasAQuestion = true;
340                    }
341                    // not a ChoicesListQuestion or second one
342                    else
343                    {
344                        return false;
345                    }
346                }
347            }
348            return hasAQuestion;
349        }
350    }
351    
352    /**
353     * Get if this form is limited to one entry by user
354     * @return <code>true</code> if the form is limited to one entry by user
355     */
356    public boolean isLimitedToOneEntryByUser()
357    {
358        return this.getValue(LIMIT_TO_ONE_ENTRY_BY_USER, false, false);
359    }
360    
361    /**
362     * Limit or not to one entry by user
363     * @param limit <code>true</code> to limit to one entry by user
364     */
365    public void limitToOneEntryByUser(boolean limit)
366    {
367        this.setValue(LIMIT_TO_ONE_ENTRY_BY_USER, limit);
368    }
369    
370    /**
371     * Get if the form entries are limited
372     * @return <code>true</code> if the form entries are limited
373     */
374    public boolean isEntriesLimited()
375    {
376        return this.getValue(LIMIT_ENTRIES_ENABLED, false, false); 
377    }
378    
379    /**
380     * Limit or not the entries of the form
381     * @param limitEntries <code>true</code> to limit the entries
382     */
383    public void limitEntries(boolean limitEntries)
384    {
385        this.setValue(LIMIT_ENTRIES_ENABLED, limitEntries); 
386    }
387    
388    /**
389     * Get if a queue is enabled
390     * @return <code>true</code> if a queue is enabled
391     */
392    public boolean isQueueEnabled()
393    {
394        return isEntriesLimited() && this.getValue(QUEUE_ENABLED, false, false); 
395    }
396    
397    /**
398     * Enable of not the queue
399     * @param enabled <code>true</code> to enable the queue
400     */
401    public void enableQueue(boolean enabled)
402    {
403        this.setValue(QUEUE_ENABLED, enabled); 
404    }
405
406    /**
407     * Get the max number limit for entries in this form
408     * @return the max number
409     */
410    public Optional<Long> getMaxEntries()
411    {
412        return Optional.ofNullable(getValue(MAX_ENTRIES));
413    }
414    
415    /**
416     * Set the max number limit for entries in this form
417     * @param max The max number
418     */
419    public void setMaxEntries(Long max)
420    {
421        this.setValue(MAX_ENTRIES, max);
422    }
423    
424    /**
425     * Get the message that will be displayed to users when filling the form if a max entries limit is set
426     * @return a message for users
427     */
428    public Optional<String> getRemainingMessage()
429    {
430        return Optional.ofNullable(getValue(REMAINING_MSG));
431    }
432    
433    /**
434     * Set the message that will be displayed to users when filling the form if a max entries limit is set
435     * @param msg The message for users
436     */
437    public void setRemainingMessage(String msg)
438    {
439        this.setValue(REMAINING_MSG, msg);
440    }
441    
442    /**
443     * Get the message that will be displayed to users when the entries limit is reached
444     * @return a message for users
445     */
446    public Optional<String> getClosedMessage()
447    {
448        return Optional.ofNullable(getValue(CLOSED_MSG));
449    }
450    
451    /**
452     * Set the message that will be displayed to users when the entries limit is reached
453     * @param msg The message for users
454     */
455    public void setClosedMessage(String msg)
456    {
457        this.setValue(CLOSED_MSG, msg);
458    }
459    
460    /**
461     * Get the queue max size in this form
462     * @return the max number
463     */
464    public Optional<Long> getQueueSize()
465    {
466        return Optional.ofNullable(getValue(QUEUE_SIZE));
467    }
468    
469    /**
470     * Set the queue max size in this form
471     * @param max The max number
472     */
473    public void setQueueSize(Long max)
474    {
475        this.setValue(QUEUE_SIZE, max);
476    }
477    
478    /**
479     * Get the message that will be displayed to users when the entries limit is reached
480     * and a queue is enabled
481     * @return a message for users
482     */
483    public Optional<String> getClosedQueueMessage()
484    {
485        return Optional.ofNullable(getValue(QUEUE_CLOSED_MSG));
486    }
487    
488    /**
489     * Set the message that will be displayed to users when the entries limit is reached
490     * and a queue is enabled
491     * @param msg The message for users
492     */
493    public void setClosedQueueMessage(String msg)
494    {
495        this.setValue(QUEUE_CLOSED_MSG, msg);
496    }
497    
498    /**
499     * Get the email's sender for notifying exit of queue.
500     * @return The email's sender
501     */
502    public Optional<String> getQueueMailSender()
503    {
504        return Optional.ofNullable(getValue(QUEUE_SENDER));
505    }
506    
507    /**
508     * Set the email's sender for notifying exit of queue.
509     * @param sender The email's sender
510     */
511    public void setQueueMailSender(String sender)
512    {
513        this.setValue(QUEUE_SENDER, sender);
514    }
515    /**
516     * Get the email's receiver for notifying exit of queue.
517     * @return The email's receiver
518     */
519    public Optional<String> getQueueMailReceiver()
520    {
521        return Optional.ofNullable(getValue(QUEUE_RECEIVER));
522    }
523    
524    /**
525     * Set the email's receiver for notifying exit of queue.
526     * @param receiver The email's receiver
527     */
528    public void setQueueMailtReceiver(String receiver)
529    {
530        this.setValue(QUEUE_RECEIVER, receiver);
531    }
532    /**
533     * Get the email's subject for notifying exit of queue.
534     * @return The email's subject
535     */
536    public Optional<String> getQueueMailSubject()
537    {
538        return Optional.ofNullable(getValue(QUEUE_SUBJECT));
539    }
540    
541    /**
542     * Set the email's subject for notifying exit of queue.
543     * @param subject The email's subject
544     */
545    public void setQueueMailSubject(String subject)
546    {
547        this.setValue(QUEUE_SUBJECT, subject);
548    }
549    /**
550     * Get the email's body for notifying exit of queue.
551     * @return The email's body
552     */
553    public Optional<String> getQueueMailBody()
554    {
555        return Optional.ofNullable(getValue(QUEUE_BODY));
556    }
557    
558    /**
559     * Set the email's body for notifying exit of queue.
560     * @param body The email's body
561     */
562    public void setQueueMailBody(String body)
563    {
564        this.setValue(QUEUE_BODY, body);
565    }
566
567    /**
568     * Get the admin emails
569     * @return the admin emails
570     */
571    public Optional<String[]> getAdminEmails()
572    {
573        return Optional.ofNullable(getValue(ADMIN_EMAILS));
574    }
575    
576    /**
577     * Set the admin emails
578     * @param emails the admin emails
579     */
580    public void setAdminEmails(String[] emails)
581    {
582        this.setValue(ADMIN_EMAILS, emails);
583    }
584    
585    /**
586     * Set the start date
587     * @param date The start date
588     */
589    public void setStartDate(LocalDate date) 
590    {
591        setValue(START_DATE, date);
592    }
593    
594    /**
595     * Get the start date
596     * @return The the start date
597     */
598    public LocalDate getStartDate()
599    {
600        return this.getValue(START_DATE);
601    }
602    
603    /**
604     * Set the end date
605     * @param date The end date
606     */
607    public void setEndDate(LocalDate date) 
608    {
609        setValue(END_DATE, date);
610    }
611    
612    /**
613     * Get the end date
614     * @return The the end date
615     */
616    public LocalDate getEndDate()
617    {
618        return this.getValue(END_DATE);
619    }
620    
621    public ModifiableIndexableDataHolder getDataHolder()
622    {
623        JCRRepositoryData repositoryData = new JCRRepositoryData(getNode());
624        return new DefaultModifiableModelAwareDataHolder(repositoryData, this._getFactory().getModel());
625    }
626
627    public String getSiteName() throws AmetysRepositoryException
628    {
629        return getSite().getName();
630    }
631
632    public Site getSite() throws AmetysRepositoryException
633    {
634        AmetysObject parent = getParent();
635        while (parent != null && !(parent instanceof Site)) 
636        {
637            parent = parent.getParent();
638        }
639        if (parent == null)
640        {
641            throw new AmetysRepositoryException("An error occurred with form with id '" + getId() + "'. Forms must always be linked to a site");
642        }
643        return (Site) parent;
644    }
645    
646    @Override
647    public Form copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
648    {
649        Form form = parent.createChild(name, FormFactory.FORM_NODETYPE);
650        form.setTitle(getTitle());
651        
652        for (FormPage page : getPages())
653        {
654            page.copyTo(form, page.getName());
655        }
656        
657        return form;
658    }
659
660    @Override
661    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
662    {
663        return copyTo(parent, name);
664    }
665    
666    /**
667     * Get the form pages.
668     * @return the form pages.
669     * @throws AmetysRepositoryException if an error occurs when retrieving the pages of the form
670     */
671    public List<FormPage> getPages() throws AmetysRepositoryException
672    {
673        return getChildren().stream()
674                .filter(child -> child instanceof FormPage)
675                .map(child -> (FormPage) child)
676                .collect(Collectors.toList());
677    }
678    
679    /**
680     * <code>true</code> if the form has entries
681     * @return <code>true</code> if the form has entries
682     */
683    public boolean hasEntries()
684    {
685        return !getEntries().isEmpty();
686    }
687    /**
688     * Get the form entries.
689     * @return the form entries.
690     * @throws AmetysRepositoryException if an error occurs when retrieving the entries of the form
691     */
692    public List<FormEntry> getEntries() throws AmetysRepositoryException
693    {
694        List<FormEntry> entries = new ArrayList<>();
695        if (hasChild(FormEntryDAO.ENTRIES_ROOT))
696        {
697            ModifiableTraversableAmetysObject entryRoot = getChild(FormEntryDAO.ENTRIES_ROOT);
698            
699            entries.addAll(entryRoot.getChildren()
700                    .stream()
701                    .map(child -> (FormEntry) child)
702                    .collect(Collectors.toList()));
703        }
704        return entries;
705    }
706    
707    /**
708     * Get the active form entries.
709     * @return a list of the active form entries.
710     * @throws AmetysRepositoryException if an error occurs when retrieving the entries of the form
711     */
712    public List<FormEntry> getActiveEntries() throws AmetysRepositoryException
713    {
714        return getEntries().stream()
715            .filter(e -> e.isActive())
716            .collect(Collectors.toList());
717    }
718    
719    /**
720    *  Get a question by its name.
721    * @param name the question name.
722    * @return the question.
723    * @throws AmetysRepositoryException if an error occurs when retrieving a question of a form
724    */
725    public FormQuestion getQuestion(String name) throws AmetysRepositoryException
726    {
727        return getQuestions().stream()
728            .filter(q -> q.getNameForForm().equals(name))
729            .findFirst()
730            .orElse(null);
731    }
732    
733    /**
734     * Get the form questions.
735     * @return the form questions.
736     * @throws AmetysRepositoryException if an error occurs when retrieving all the questions of a form
737     */
738    public List<FormQuestion> getQuestions() throws AmetysRepositoryException
739    {
740        return getPages().stream()
741                .map(FormPage::getQuestions)
742                .flatMap(List::stream)
743                .collect(Collectors.toList());
744    }
745    
746    /**
747     * Returns a unique question name in the form
748     * @param originalName The original name
749     * @return a unique question name
750     */
751    public String findUniqueQuestionName (String originalName)
752    {
753        String name = originalName;
754        int index = 1;
755        List<String> questionNames = getQuestionsNames();
756        while (questionNames.contains(name))
757        {
758            name = originalName + "-" + (index++);
759        }
760        return name;
761    }
762    
763    /**
764     * Verify that no question has the same JCR id as the current question
765     * @param uniqueName jcr name for the current question
766     * @return false if id already exist, true if not
767     */
768    public boolean isQuestionNameUnique(String uniqueName)
769    {
770        return !getQuestionsNames().contains(uniqueName);
771    }
772    
773    /**
774     * Get all the nameForForms of the questions in this form
775     * @return a list of the names for form
776     */
777    public List<String> getQuestionsNames()
778    {
779        return getQuestions().stream()
780                .map(FormQuestion::getNameForForm)
781                .toList();
782    }
783    
784    /**
785     * Returns a unique question title in the form
786     * @param originalTitle The original title
787     * @return a unique question title
788     */
789    public String findUniqueQuestionTitle (String originalTitle)
790    {
791        String title = originalTitle;
792        int index = 1;
793        List<String> questionTitles = getQuestions().stream()
794            .map(FormQuestion::getTitle)
795            .toList();
796        while (questionTitles.contains(title))
797        {
798            title = originalTitle + " " + (index++);
799        }
800        return title;
801    }
802    
803    /**
804     * Get the rules having sourceQuestionId as sourceId
805     * @param sourceQuestionId The source question  
806     * @return a map of question target's name for form and associated rule
807     */
808    public Map<FormQuestion, Rule> getQuestionsRule(String sourceQuestionId)
809    {
810        Map<FormQuestion, Rule> questionsRule = new HashMap<>();
811        for (FormQuestion question : getQuestions())
812        {
813            Optional<Rule> questionRule = question.getFirstQuestionRule();
814            if (questionRule.map(q -> q.getSourceId().equals(sourceQuestionId)).orElse(false))
815            {
816                questionsRule.put(question, questionRule.get());
817            }
818        }
819        return questionsRule;
820    }
821    
822    /**
823     * Delete the rules having sourceQuestionId as sourceId
824     * @param sourceQuestionId The source question  
825     */
826    public void deleteQuestionsRule(String sourceQuestionId)
827    {
828        for (FormQuestion question : getQuestions())
829        {
830            Optional<Rule> questionRule = question.getFirstQuestionRule();
831            
832            if (questionRule.map(q -> q.getSourceId().equals(sourceQuestionId)).orElse(false))
833            {
834                question.getRepeater(FormQuestion.ATTRIBUTE_RULES).removeEntry(0);
835            }
836        }
837        saveChanges();
838    }
839    
840    /**
841     * <code>true</code> if the form has a workflow
842     * @return <code>true</code> if the form has a workflow
843     */
844    public boolean hasWorkflow()
845    {
846        return StringUtils.isNotBlank(getWorkflowName());
847    }
848    
849    @Override
850    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
851    {
852        return FormAndDirectoryCommonMethods.canMoveTo(getSiteName(), newParent, this, _getFactory().getFormDirectoriesDAO());
853    }
854    
855    @Override
856    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
857    {
858        FormAndDirectoryCommonMethods.moveTo(newParent, renameIfExist, this);
859    }
860    
861    @Override
862    public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException
863    {
864        FormAndDirectoryCommonMethods.orderBefore(siblingNode, this);
865    }
866    
867    /**
868     * Rights profiles
869     */
870    public enum FormProfile
871    {
872        /** Read access */
873        READ_ACCESS,
874        /** Write access */
875        WRITE_ACCESS,
876        /** Right access */
877        RIGHT_ACCESS;
878        
879        @Override
880        public String toString()
881        {
882            return name().toLowerCase();
883        }
884    }
885
886}