[IMP] Deadlines on invitations
authorRichard Mathot (OpenERP) <rim@openerp.com>
Thu, 12 Dec 2013 11:04:35 +0000 (12:04 +0100)
committerRichard Mathot (OpenERP) <rim@openerp.com>
Thu, 12 Dec 2013 11:04:35 +0000 (12:04 +0100)
[REF] Survey auth refactoring

[REF/IMP] Pagination system refactoring + add go back function

[FIX] Small glitches

[REF] Code refactoring

bzr revid: rim@openerp.com-20131212110435-he9h9dkeyb722l1a

addons/survey/controllers/main.py
addons/survey/survey.py
addons/survey/views/survey_templates.xml

index e5d4888..67e388f 100644 (file)
@@ -23,16 +23,56 @@ from openerp.addons.web import http
 from openerp.addons.web.http import request
 from openerp.addons.website.models import website
 from openerp import SUPERUSER_ID
-
+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
+from datetime import datetime
 import werkzeug
 import json
 import logging
 
+
 _logger = logging.getLogger(__name__)
 
 
 class WebsiteSurvey(http.Controller):
 
+    ## HELPER METHODS ##
+
+    def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, context=None):
+        # In case of bad survey, redirect to surveys list
+        if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []:
+            return werkzeug.utils.redirect("/survey/")
+
+        # In case of auth required, block public user
+        if survey.auth_required and uid == request.registry['website'].get_public_user(cr, uid, context):
+            return request.website.render("website.401")
+
+        # In case of non open surveys
+        if survey.state != 'open':
+            return request.website.render("survey.notopen")
+
+        # If enough surveys completed
+        if survey.user_input_limit > 0:
+            completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
+            if completed >= survey.user_input_limit:
+                return request.website.render("survey.notopen")
+
+        # Everything seems to be ok
+        return None
+
+    def _check_deadline(self, cr, uid, user_input, context=None):
+        '''Prevent opening of the survey if the deadline has turned out
+
+        ! This will NOT disallow access to users who have already partially filled the survey !'''
+        if user_input.deadline:
+            dt_deadline = datetime.strptime(user_input.deadline, DTF)
+            dt_now = datetime.now()
+            if dt_now > dt_deadline:  # survey is not open anymore
+                return request.website.render("survey.notopen")
+
+        return None
+
+    ## ROUTES HANDLERS ##
+
     # Survey list
     @website.route(['/survey/',
                     '/survey/list/'],
@@ -56,23 +96,10 @@ class WebsiteSurvey(http.Controller):
         survey_obj = request.registry['survey.survey']
         user_input_obj = request.registry['survey.user_input']
 
-        # In case of bad survey, redirect to surveys list
-        if survey_obj.exists(cr, uid, survey.id, context=context) == []:
-            return werkzeug.utils.redirect("/survey/")
-
-        # In case of auth required, block public user
-        if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context):
-            return request.website.render("website.401")
-
-        # In case of non open surveys
-        if survey.state != 'open':
-            return request.website.render("survey.notopen")
-
-        # If enough surveys completed
-        if survey.user_input_limit > 0:
-            completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
-            if completed >= survey.user_input_limit:
-                return request.website.render("survey.notopen")
+        # Controls if the survey can be displayed
+        errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
+        if errpage:
+            return errpage
 
         # Manual surveying
         if not token:
@@ -89,73 +116,50 @@ class WebsiteSurvey(http.Controller):
             else:
                 user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
 
-        # Prevent opening of the survey if the deadline has turned out
-        # ! This will NOT disallow access to users who have already partially filled the survey !
-        # if user_input.deadline > fields.date.now() and user_input.state == 'new':
-        #     return request.website.render("survey.notopen")
-            # TODO check if this is ok
+        # Do not open expired survey
+        errpage = self._check_deadline(cr, uid, user_input, context=context)
+        if errpage:
+            return errpage
 
         # Select the right page
-
         if user_input.state == 'new':  # Intro page
             data = {'survey': survey, 'page': None, 'token': user_input.token}
             return request.website.render('survey.survey_init', data)
         else:
             return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
 
-
     # Survey displaying
-    @website.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>'],
+    @website.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
+                    '/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
                    type='http', auth='public', multilang=True)
-    def fill_survey(self, survey, token, **post):
+    def fill_survey(self, survey, token, prev=None, **post):
         '''Display and validates a survey'''
         cr, uid, context = request.cr, request.uid, request.context
         survey_obj = request.registry['survey.survey']
         user_input_obj = request.registry['survey.user_input']
 
-        # In case of bad survey, redirect to surveys list
-        if survey_obj.exists(cr, uid, survey.id, context=context) == []:
-            return werkzeug.utils.redirect("/survey/")
-
-        # In case of auth required, block public user
-        if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context):
-            return request.website.render("website.401")
-
-        # In case of non open surveys
-        if survey.state != 'open':
-            return request.website.render("survey.notopen")
-
-        # If enough surveys completed
-        if survey.user_input_limit > 0:
-            completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True)
-            if completed >= survey.user_input_limit:
-                return request.website.render("survey.notopen")
+        # Controls if the survey can be displayed
+        errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
+        if errpage:
+            return errpage
 
-        # Manual surveying
-        if not token:
-            if survey.visible_to_user:
-                user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id})
-                user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
-            else:  # An user cannot open hidden surveys without token
-                return request.website.render("website.403")
+        # Load the user_input
+        try:
+            user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
+        except IndexError:  # Invalid token
+            return request.website.render("website.403")
         else:
-            try:
-                user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0]
-            except IndexError:  # Invalid token
-                return request.website.render("website.403")
-            else:
-                user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
+            user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
 
-        # Prevent opening of the survey if the deadline has turned out
-        # ! This will NOT disallow access to users who have already partially filled the survey !
-        # if user_input.deadline > fields.date.now() and user_input.state == 'new':
-        #     return request.website.render("survey.notopen")
-            # TODO check if this is ok
+        # Do not display expired survey (even if some pages have already been
+        # displayed -- There's a time for everything!)
+        errpage = self._check_deadline(cr, uid, user_input, context=context)
+        if errpage:
+            return errpage
 
         # Select the right page
-
         if user_input.state == 'new':  # First page
-            page, page_nr, last = self.find_next_page(survey, user_input)
+            page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context)
             data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
             if last:
                 data.update({'last': True})
@@ -163,39 +167,33 @@ class WebsiteSurvey(http.Controller):
         elif user_input.state == 'done':  # Display success message
             return request.website.render('survey.finished', {'survey': survey})
         elif user_input.state == 'skip':
-            page, page_nr, last = self.find_next_page(survey, user_input)
+            flag = (True if prev and prev == 'prev' else False)
+            page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context)
             data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
             if last:
                 data.update({'last': True})
+            return request.website.render('survey.survey', data)
         else:
             return request.website.render("website.403")
 
-    # @website.route(['/survey/prefill/<model("survey.survey"):survey>'], type='json', auth='public', multilang=True):
+    # AJAX prefilling of a survey
+    # @website.route(['/survey/prefill/<model("survey.survey"):survey>'],
+    #                type='json', auth='public', multilang=True):
+    # def prefill(self, survey, **post):
 
+    # AJAX validation of some questions
     # @website.route(['/survey/validate/<model("survey.survey"):survey>'],
     #                type='http', auth='public', multilang=True)
     # def validate(self, survey, **post):
-    #     _logger.debug('Incoming data: %s', post)
-    #     page_id = int(post['page_id'])
-    #     cr, uid, context = request.cr, request.uid, request.context
-    #     questions_obj = request.registry['survey.question']
-    #     questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
-    #     questions = questions_obj.browse(cr, uid, questions_ids, context=context)
-    #     errors = {}
-    #     for question in questions:
-    #         answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
-    #         errors.update(self.validate_question(question, post, answer_tag))
-    #     ret = {}
-    #     if (len(errors) != 0):
-    #         ret['errors'] = errors
-    #     return json.dumps(ret)
 
+    # AJAX submission of a page
     @website.route(['/survey/submit/<model("survey.survey"):survey>'],
                    type='http', auth='public', multilang=True)
     def submit(self, survey, **post):
         _logger.debug('Incoming data: %s', post)
         page_id = int(post['page_id'])
         cr, uid, context = request.cr, request.uid, request.context
+        survey_obj = request.registry['survey.survey']
         questions_obj = request.registry['survey.question']
         questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
         questions = questions_obj.browse(cr, uid, questions_ids, context=context)
@@ -213,6 +211,7 @@ class WebsiteSurvey(http.Controller):
         else:
             # Store answers into database
             user_input_obj = request.registry['survey.user_input']
+
             user_input_line_obj = request.registry['survey.user_input_line']
             try:
                 user_input_id = user_input_obj.search(cr, uid, [('token', '=', post['token'])], context=context)[0]
@@ -222,12 +221,18 @@ class WebsiteSurvey(http.Controller):
                 answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
                 user_input_line_obj.save_lines(cr, uid, user_input_id, question, post, answer_tag, context=context)
 
-                # page, _, _ = self.find_next_page(survey, user_input_obj.browse(cr, uid, [user_input_id], context=context))
-                # if page:
-                #     user_input_obj.write(cr, uid, user_input_id, {'state': 'skip'})
-                # else:
-                user_input_obj.write(cr, uid, user_input_id, {'state': 'done'})
+            user_input = user_input_obj.browse(cr, uid, user_input_id, context=context)
+            go_back = post['button_submit'] == 'previous'
+            next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context)
+            vals = {'last_displayed_page_id': page_id}
+            if next_page is None and not go_back:
+                vals.update({'state': 'done'})
+            else:
+                vals.update({'state': 'skip'})
+            user_input_obj.write(cr, uid, user_input_id, vals, context=context)
             ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
+            if go_back:
+                ret['redirect'] += '/prev'
         return json.dumps(ret)
 
     # Printing routes
@@ -256,7 +261,7 @@ class WebsiteSurvey(http.Controller):
                     page_nr = page_nr + 1
                 else:
                     nextpage = page
-                if page_nr == len(survey.page_ids):
+                if page_nr == len(survey.page_ids) - 1:
                     last = True
             return nextpage, page_nr, last
 
index b4a0523..bcdfb13 100644 (file)
@@ -150,6 +150,45 @@ class survey_survey(osv.osv):
         return super(survey_survey, self).copy(cr, uid, ids, vals,
             context=context)
 
+    def next_page(self, cr, uid, user_input, page_id, go_back=False, context=None):
+        '''The next page to display to the user, knowing that page_id is the id
+        of the last displayed page.
+
+        If page_id == 0, it will always return the first page of the survey.
+
+        If all the pages have been displayed and go_back == False, it will
+        return None
+
+        If go_back == True, it will return the *previous* page instead of the
+        next page.
+
+        .. note::
+            It is assumed here that a careful user will not try to set go_back
+            to True if she knows that the page to display is the first one!
+            (doing this will probably cause a giant worm to eat her house)'''
+        survey = user_input.survey_id
+        pages = list(enumerate(survey.page_ids))
+
+        # First page
+        if page_id == 0:
+            return (pages[0][1], 0, len(pages) == 1)
+
+        current_page_index = pages.index((filter(lambda p: p[1].id == page_id, pages))[0])
+
+        # All the pages have been displayed
+        if current_page_index == len(pages) - 1 and not go_back:
+            return (None, -1, False)
+        # Let's get back, baby!
+        elif go_back and survey.users_can_go_back:
+            return (pages[current_page_index - 1][1], current_page_index - 1, False)
+        else:
+            # This will show the last page
+            if current_page_index == len(pages) - 2:
+                return (pages[current_page_index + 1][1], current_page_index + 1, True)
+            # This will show a regular page
+            else:
+                return (pages[current_page_index + 1][1], current_page_index + 1, False)
+
     def action_print_survey_questions(self, cr, uid, ids, context=None):
         ''' Generates a printable view of an empty survey '''
         pass
@@ -287,6 +326,9 @@ class survey_page(osv.osv):
             help="An introductory text to your page", translate=True,
             oldname="note"),
     }
+    _defaults = {
+        'sequence': 10
+    }
 
     # Public methods #
 
@@ -408,6 +450,7 @@ class survey_question(osv.osv):
     }
     _defaults = {
         'page_id': lambda s, cr, uid, c: c.get('page_id'),
+        'sequence': 10,
         'type': 'free_text',
         'matrix_subtype': 'simple',
         'column_nb': '12',
@@ -661,7 +704,7 @@ class survey_question(osv.osv):
         if question.constr_mandatory:
             lines_number = len(question.labels_ids_2)
             answer_candidates = dict_keys_startswith(post, answer_tag)
-            comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None)
+            #comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None)
             # Number of lines that have been answered
             if question.matrix_subtype == 'simple':
                 answer_number = len(answer_candidates)
@@ -701,6 +744,9 @@ class survey_label(osv.osv):
         'value': fields.char("Suggested value", translate=True,
             required=True)
     }
+    defaults = {
+        'sequence': 10
+    }
 
 
 class survey_user_input(osv.osv):
@@ -714,8 +760,9 @@ class survey_user_input(osv.osv):
                                      readonly=1, ondelete='restrict'),
         'date_create': fields.datetime('Creation Date', required=True,
                                        readonly=1),
-        'deadline': fields.date("Deadline",
-                                help="Date by which the person can take part to the survey",
+        'deadline': fields.datetime("Deadline",
+                                help="Date by which the person can open the survey and submit answers.\
+                                Warning: ",
                                 oldname="date_deadline"),
         'type': fields.selection([('manually', 'Manually'), ('link', 'Link')],
                                  'Answer Type', required=1, readonly=1,
@@ -732,6 +779,9 @@ class survey_user_input(osv.osv):
         'partner_id': fields.many2one('res.partner', 'Partner', readonly=1),
         'email': fields.char("E-mail", readonly=1),
 
+        # Displaying data
+        'last_displayed_page_id': fields.many2one('survey.page',
+                                              'Last displayed page'),
         # The answers !
         'user_input_line_ids': fields.one2many('survey.user_input_line',
                                                'user_input_id', 'Answers'),
@@ -872,7 +922,7 @@ class survey_user_input_line(osv.osv):
         'survey_id': fields.related('user_input_id', 'survey_id',
                                     type="many2one", relation="survey.survey",
                                     string='Survey'),
-        'date_create': fields.datetime('Create Date', required=1),  # drop
+        'date_create': fields.datetime('Create Date', required=1),
         'skipped': fields.boolean('Skipped'),
         'answer_type': fields.selection([('text', 'Text'),
                                          ('number', 'Number'),
@@ -888,7 +938,7 @@ class survey_user_input_line(osv.osv):
     }
     _defaults = {
         'skipped': False,
-        'date_create': fields.datetime.now
+        'date_create': fields.datetime.now()
     }
 
     def save_lines(self, cr, uid, user_input_id, question, post, answer_tag,
@@ -912,7 +962,7 @@ class survey_user_input_line(osv.osv):
             'page_id': question.page_id.id,
             'survey_id': question.survey_id.id,
         }
-        if answer_tag in post:
+        if answer_tag in post and post[answer_tag].strip() != '':
             vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
         else:
             vals.update({'skipped': True})
@@ -933,7 +983,7 @@ class survey_user_input_line(osv.osv):
             'page_id': question.page_id.id,
             'survey_id': question.survey_id.id,
         }
-        if answer_tag in post:
+        if answer_tag in post and post[answer_tag].strip() != '':
             vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
         else:
             vals.update({'skipped': True})
@@ -954,10 +1004,10 @@ class survey_user_input_line(osv.osv):
             'page_id': question.page_id.id,
             'survey_id': question.survey_id.id,
         }
-        if answer_tag in post:
+        if answer_tag in post and post[answer_tag].strip() != '':
             vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
         else:
-            vals.update({'skipped': True})
+            vals.update({'skipped': True, 'answer_type': None})
         old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
                                         ('survey_id', '=', question.survey_id.id),
                                         ('question_id', '=', question.id)],
@@ -989,26 +1039,26 @@ class survey_user_input_line(osv.osv):
             self.create(cr, uid, vals, context=context)
         return True
 
-    def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
-        vals = {
-            'user_input_id': user_input_id,
-            'question_id': question.id,
-            'page_id': question.page_id.id,
-            'survey_id': question.survey_id.id,
-        }
-        if answer_tag in post:
-            vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
-        else:
-            vals.update({'skipped': True})
-        old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
-                                        ('survey_id', '=', question.survey_id.id),
-                                        ('question_id', '=', question.id)],
-                              context=context)
-        if old_uil:
-            self.write(cr, uid, old_uil[0], vals, context=context)
-        else:
-            self.create(cr, uid, vals, context=context)
-        return True
+    # def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
+    #     vals = {
+    #         'user_input_id': user_input_id,
+    #         'question_id': question.id,
+    #         'page_id': question.page_id.id,
+    #         'survey_id': question.survey_id.id,
+    #     }
+    #     if answer_tag in post:
+    #         vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
+    #     else:
+    #         vals.update({'skipped': True})
+    #     old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
+    #                                     ('survey_id', '=', question.survey_id.id),
+    #                                     ('question_id', '=', question.id)],
+    #                           context=context)
+    #     if old_uil:
+    #         self.write(cr, uid, old_uil[0], vals, context=context)
+    #     else:
+    #         self.create(cr, uid, vals, context=context)
+    #     return True
 
 
 def dict_keys_startswith(dictionary, string):
index fa57efa..a3055f7 100644 (file)
                 </div>
             </t>
             <div class="text-center mt16 mb16">
-                <button t-if="survey.users_can_go_back and page_nr > 0" type="submit" class="btn btn-default">Previous page</button>
-                <button t-if="not last" type="submit" class="btn btn-primary">Next page</button>
-                <button type="submit" t-if="last" class="btn btn-primary">Submit survey</button>
+                <button t-if="survey.users_can_go_back and page_nr > 0" type="submit" class="btn btn-default" name="button_submit" value="previous">Previous page</button>
+                <button t-if="not last" type="submit" class="btn btn-primary" name="button_submit" value="next">Next page</button>
+                <button t-if="last" type="submit" class="btn btn-primary" name="button_submit" value="finish">Submit survey</button>
             </div>
         </form>