Commits

Marc-Alexandre Chan committed 8926ebf

refactored CheckMessageEvent; cleaned up Add/UpdatePromptCommand w abstract base;
started adding exception handling;

Comments (0)

Files changed (4)

         self.r_post_id = None
 
     def set_posted(self, post_id):
-        """ Set the prompt entry as posted and store the post id. """
+        """ Set the prompt entry as posted and store the post id. Note that
+        post_time is not updated—consider calling Prompt.queue() or setting
+        post_time if the value needs to be set first. """
         self.status = self.STATUS_POSTED
         self.r_post_id = post_id
 
     """ Raised when a command message does not contain valid parameters. """
     pass
 
+class BadCommandError(ValueError, CommandError):
+    """ Raised when a command message is invalid and no parsing can be done. """
+    pass
+
 class MissingParameterError(KeyError, CommandError):
     """ Raised when a required command parameter is not passed in a command
     message. """
 
     def run(self):
         for msg in self.reddit.user.get_unread(limit=self.msg_chunk):
-            msg_command = None
-            # if a PM (no subreddit) and directly to the bot (not modmail)
-            # and from approved user, then attempt parsing message
-            if (msg.subreddit is None and msg.dest == reddit.user.name and
-                    msg.author.name in self.approved):
-                try:
-                    msg_data = self._parse_message(msg)
-                    msg_command = self._make_command(msg, msg_data)
-                except (CommandParseError, CommandNameError) as e:
-                    self.log.warning("%s: %s", classname(self), e.args[0])
-                    self.log.info("%s: Ignoring bad PM from %s (%d)",
-                        classname(self), msg.author, msg.id)
-                    msg.mark_as_read()
-                self.owner.queue_event(msg_command)
+            try:
+                self._process_message(msg)
+            except (CommandParseError, CommandNameError,
+                    CommandParameterError, MissingParameterError) as e:
+                self.log.info("%s: %s", classname(self), e.args[0])
+                self._send_error_reply(msg, e)
+                msg.mark_as_read()
             else: # bad message, ignore it
-                # if bad PM, log; don't care about replies/comments though
+                # if bad PM, log; but don't care about replies/comments
                 if msg.subreddit is None:
-                    self.log.info("%s: Ignoring bad PM from %s (%d)",
+                    self.log.info(
+                        "%s: Ignoring bad private message from %s (%d)",
                         classname(self), msg.author, msg.id)
                 msg.mark_as_read()
 
     def end(self):
         pass
 
+    def _process_message(self, msg):
+        """ Process a message. May throw CommandParseError, CommandNameError,
+        CommandParameterError, BadCommandError. """
+        if not self._is_valid_message(msg):
+            raise BadCommandError('CheckMessageEvent: bad command message', msg)
+
+        # raises CommandParseError
+        msg_data = self._parse_message(msg)
+        # build event obj. raises CommandNameError/CommandParameterError
+        msg_command = self._make_command(msg, msg_data)
+        # queue the event in the scheduler
+        self.owner.queue_event(msg_command)
+
+    def _is_valid_message(self, msg):
+        """ Check whether message source is appropriate for a command message
+        (is a PM sent to the bot (not modmail) from an approved user). """
+        return (msg.subreddit is None and
+                msg.dest == self.reddit.user.name and
+                msg.author.name in self.approved)
+
+    def _send_error_reply(self, msg, e):
+        """ Build an error message and queue a SendReplyMessage event for it.
+        """
+        self.log.info("%s: Replying to invalid command PM %s (%d)",
+            classname(self), msg.author, msg.id)
+        re_subject = "Invalid Command"
+        re_body = ("Sorry, there was a problem with the command you "
+                   "sent me. I wasn't able to understand what you wanted me to "
+                   "do. The error that occurred is:\n\n{error}").\
+                   format(error=e.args[0])
+        reply_event = SendReplyCommand(msg, re_subject, re_body)
+        self.owner.queue_event(reply_event)
+
     # command message parsing variables
     PARSE_FIELD = 0
     PARSE_TEXT = 1
                 new_cmd = PostSuggestionThreadCommand(id_, t_post)
                 self.owner.queue_event(new_cmd)
 
+    def handle_exception(self, e):
+        """ Handle an exception thrown by the command. Return True if succesful,
+        False otherwise (to propagate the exception upward in the system). """
+        return False # expected recoverable exceptions are handled in the code
+
 
 class SuggestionThreadQueueMaintainer(DateParseMixin):
     """ Skeleton event for the Minibot form of the bot. This event checks
     def end(self):
         pass
 
+    def handle_exception(self, e):
+        """ Handle an exception thrown by the command. Return True if succesful,
+        False otherwise (to propagate the exception upward in the system). """
+        return False # expected recoverable exceptions are handled in the code
 
-class AddPromptCommand(CommandBase, DataFormatMixin, DateParseMixin):
-    """ Command to add a prompt to the queue.
+
+class PromptCommandBase(CommandBase, DataFormatMixin, DateParseMixin):
+    """ Command to process the insertion or modification of a prompt in queue.
 
     Defaults:
         * ``start_time`` = 0 (immediately)
         * ``interval`` = 0
 
     """
-    # TODO: Clean up repetition of UpdatePromptCommand & very long methods
-    required_res = ['reddit', 'dbsession', 'config.minibot', 'config.reddit',
-                    'logger']
+    required_res = ['reddit', 'dbsession', 'config.minibot', 'logger']
     start_time = 0 # execute ASAP
 
+    PARAMS = ['title', 'text', 'user', 'submission', 'date', 'time']
+
     def __init__(self, msg, **kwargs):
         """ Constructor. Keyword arguments are strings defined as per the
         `CheckCommandSpec` class documentation; the text block is expected as
         on missing or invalid parameters (respectively). """
 
         self.msg = msg
+        self.raw_params = kwargs
+        self.params = dict()
+        self._check_param_fields()
 
-        # required params
-        try:
-            self.text  = kwargs.pop('text')
-        except KeyError:
-            raise MissingParameterError(
-                "The required 'text block' parameter is missing.")
+        self.log = self.res['logger']
 
-        # optional params
-        self.user = kwargs.pop('user', None)
-        self.submission = kwargs.pop('submission', None)
-        self.title = kwargs.pop('title', '')
+    def _check_param_fields(self):
+        """ Check that the parameter fields passed are valid and that all
+        required parameters are included. Child classes may override this method
+        and call PromptCommandBase's method if they need to include additional
+        criteria. Returns True if params are valid, or raises a
+        CommandParameterError on errors. """
+        input_params = list(self.raw_params.keys())
+        unknown_params = list()
+        for param in input_params:
+            if param not in self.PARAMS:
+                # remember param, in single quote marks
+                unknown_params.append(''.join(["'", param, "'"]))
 
-        str_date = kwargs.pop('date', None)
-        if hasattr(str_date, 'strip'): str_date = str_date.strip()
-        str_time = kwargs.pop('time', None)
-        if has_attr(str_time, 'strip'): str_time = str_time.strip()
-        self.datetime = _parse_datetime(str_date, str_time)
-        self.date_default = (str_date is None)
-        self.time_default = (str_time is None)
+        if unknown_params:
+            if len(unknown_params) == 1:
+                text = "Unknown parameter: {params}"
+            else:
+                text = "Unknown parameters: {params}"
+            raise CommandParameterError(
+                text.format(params=', '.join(unknown_params))
 
-        # if parameters left over, they're not valid
-        if len(kwargs):
-            if len(kwargs) == 1:
-                raise CommandParameterError(
-                    "Unknown parameter: '" + kwargs.keys()[0])
-            else:
-                raise CommandParameterError(
-                    "Unknown parameters: '" + ', '.join(kwargs.keys()))
+        return True
 
-    def start(self):
+    def _parse_params(self):
+        """ Parse params. Stores a dict ``params`` containing parsed parameters
+        with defaults set. Parameters: title, text, user, r_user_id,
+        r_source_url, r_approver_id, post_time
+        """
+        # lazy shorthands 'p' and 'raw'
+        raw = self.raw_params
+        self.params = p = dict()
+
         # resources
         reddit = self.res['reddit']
-        db     = self.res['dbsession']
-        default_time_str = self.res['config.minibot'].default_time
-        target_reddit = self.res['config.reddit'].target
-        hash_opts = {'globsalt' : self.res['config.minibot'].hash_salt,
-                     'rounds' : self.res['config.minibot'].hash_rounds}
-        self.log = self.res['logger']
 
-        ### process default and inferred Prompt attributes ###
-        # post_time
-        if self.date_default and self.time_default:
-            default_date = self._get_autodate()
-            default_time = self._parse_datetime(default_time_str)
-            self.datetime = datetime.combine(default_date, default_time)
-        elif self.date_default:
-            # in this case, self.datetime is a dt_time object instead
-            default_date = self._get_autodate()
-            self.datetime = datetime.combine(default_date, self.datetime)
-        elif self.time_default:
-            default_time = self._parse_datetime(default_time_str)
-            self.datetime = datetime.combine(self.datetime.date(), default_time)
+        # store params
+        p['title'] = raw.pop('title', None)
+        p['text'] = raw.pop('text', None)
 
-        post_time = self._utctime(self.datetime)
+        # post time
+        p['post_time'] = self.__parse_param_post_time(
+                            raw.pop('date', None), raw.pop('time', None))
 
-        # user
-        if self.user:
+        # submitter (user)
+        tmp_user = raw.pop('user', None)
+        if tmp_user:
             try:
-                r_user_id = reddit.get_redditor(self.user).id
-                user = self.user
-            except HTTPError:
+                p['r_user_id'] = reddit.get_redditor(tmp_user).id
+                p['user'] = tmp_user
+            except HTTPError, AttributeError:
                 raise CommandParameterError(
-                    ''.join(["Unknown Reddit user: '", self.user, "'."]))
+                    ''.join(["Unknown Reddit user: '", tmp_user, "'."]))
         else:
-            user = None
+            p['user'] = None
+            p['r_user_id'] = None
 
-        # source url
-        if self.submission:
-            source_url_parsed = urlparse(self.submission)
+        # source url - check for reddit domain
+        tmp_submission = raw.pop('submission', None)
+        if tmp_submission:
+            source_url_parsed = urlparse(tmp_submission)
             domain = source_url_parsed.netloc
             if domain == reddit.config.domain or\
                domain == reddit.config.domain.replace('www.', '') or\
                domain == reddit.config._short_domain.replace('http://', '').\
                             replace('https://', ''):
-                source_url = self.submission
+                p['r_source_url'] = tmp_submission
             else:
-                raise CommandParameterError(
-                    ''.join(["Invalid domain on source URL: '", self.submission,
-                             "'."]))
+                raise CommandParameterError(''.join(
+                    ["Invalid domain on source URL: '", self.submission, "'."]))
         else:
-            source_url = ''
-
+            p['r_source_url'] = None
 
         # approver
-        r_approver_id = self.msg.author.id
-        approver_name = self.msg.author.name
+        p['r_approver_id'] = self.msg.author.id
 
-        # check approver in User table
+    def __parse_param_post_time(self, datestr, timestr):
+        """ Parse date/time params. Returns post time UTC (with default values
+        set if params not specified). """
+        # parse the input parameters
+        if hasattr(datestr, 'strip'): datestr = datestr.strip()
+        if hasattr(timestr, 'strip'): timestr = timestr.strip()
+        param_datetime = _parse_datetime(datestr, timestr)
+
+        # determine which defaults to use
+        use_default_date = (datestr is None)
+        use_default_time = (timestr is None)
+
+        # determine final datetime object
+        if use_default_date and use_default_time:
+            localtime = datetime.combine(
+                            self._get_autodate(), self._get_autotime())
+        elif self.date_default:
+            # in this case, param_datetime is a dt_time object instead
+            localtime = datetime.combine(
+                            self._get_autodate(), param_datetime)
+        elif self.time_default:
+            localtime = datetime.combine(
+                            param_datetime.date(), self._get_autotime())
+
+        return self._utctime(localtime)
+
+    def _make_approver_user(self, r_approver_id, approver_name):
+        """ If the approver isn't in the User database, make an entry for the
+        approver. The action is NOT committed in the DB session. """
+        db = self.res['dbsession']
+        hash_opts = {'globsalt' : self.res['config.minibot'].hash_salt,
+                     'rounds' : self.res['config.minibot'].hash_rounds}
+
         if not db.query(User).filter(User.r_id==r_approver_id).count():
             self.log.info(
                 "%s: User %s not found. Adding to database `user` table.",
                 "%s: User %s (%s) found in database `user` table.",
                 classname(self), r_approver_id, approver_name)
 
-        new_prompt = Prompt(
-                            self.title, self.text, r_approver_id,
-                            user=user, user_id=r_user_id, source_url=source_url)
-        new_prompt.queue(post_time)
-        db.add(new_prompt)
-
-        self.log.info("%s: Added prompt '%s' (%d) to database queue.",
-            classname(self), self.title, new_prompt.id)
-        self.log.debug("%s: %s", classname(self), repr(new_prompt))
-
-        db.commit()
-        self.msg.mark_as_read()
-
-        self.log.info("%s: Marked message %s as read.",
-            classname(self), self.msg.name)
-
-        reply_topic = ''.join([target_reddit, ": Prompt added"])
-        reply_text  = ''.join([
-                        u"Your prompt has been added.\n\n___\n\n",
-                        self._format_prompt(new_prompt)])
-        self.owner.queue_command(
-            SendReplyCommand(self.msg, reply_topic, reply_text))
+    def start(self):
+        raise NotImplementedError("PromptCommandBase.start()")
 
     def run(self):
-        pass
+        """ To be implemented by derived class. Should probably call
+        self._parse_params() and self._make_approver_user() at some point. """
+        raise NotImplementedError("PromptCommandBase.run()")
 
     def end(self):
         # close off the session
         del self.res['dbsession']
 
     def _get_autodate(self):
+        """ Determine the default date for a new prompt. This is the nearest
+        future date that does not already have a prompt queued. """
         db = self.res['dbsession']
         a_day = timedelta(days=1)
         # get today in UTC timezone
             classname(self), current_date.stftime('%Y-%m-%d'))
         return current_date
 
+    def _get_autotime(self):
+        """ Determine the default time for a new prompt. This is set in the
+        config.minibot resource. """
+        default_time_str = self.res['config.minibot'].default_time
+        return self._parse_datetime(default_time_str)
+
+
+class AddPromptCommand(PromptCommandBase):
+    """ Command to add a prompt to the queue.
+
+    Defaults:
+        * ``start_time`` = 0 (immediately)
+        * ``duration`` = 0
+        * ``interval`` = 0
+
+    """
+    def __init__(self, msg, **kwargs):
+        """ Constructor. Keyword arguments are strings defined as per the
+        `CheckCommandSpec` class documentation; the text block is expected as
+        a 'text' argument. Throws MissingParameterError or CommandParameterError
+        on missing or invalid parameters (respectively). """
+        PromptCommandBase.__init__(msg, **kwargs)
+
+    def _check_param_fields(self):
+        """ Overriden from PromptCommandBase. """
+        try:
+            self.raw_params.get('text')
+        except KeyError:
+            raise MissingParameterError(
+                "The required text block parameter is missing.")
+        return PromptCommandBase._check_param_fields()
+
+    def start(self):
+        self._parse_params()
+        self.run()
+
+    def run(self):
+        # shorthand
+        p = self.params
+
+        # first check User table for approver
+        self._make_approver_user(p['r_approver_id'], self.msg.author.name)
+
+        # now make new prompt entry in queue
+        new_prompt = Prompt(
+                            p['title'], p['text'], p['r_approver_id'],
+                            datetime.utcfromtimestamp(self.msg.created_utc),
+                            user=p['user'], user_id=p['r_user_id'],
+                            source_url=p['source_url'])
+        new_prompt.queue(p['post_time'])
+        db.add(new_prompt)
+
+        db.commit()
+        self.log.info("%s: Added prompt '%s' (%d) to database queue.",
+            classname(self), self.title, new_prompt.id)
+        self.log.debug("%s: %s", classname(self), repr(new_prompt))
+
+        self.msg.mark_as_read()
+        self.log.info("%s: Marked message %s as read.",
+            classname(self), self.msg.name)
+
+        reply_topic = "Prompt added"
+        reply_text  = ''.join([
+                        u"Your prompt has been added.\n\n___\n\n",
+                        self._format_prompt(new_prompt)])
+        self.owner.queue_command(
+            SendReplyCommand(self.msg, reply_topic, reply_text))
+
 
 class RemovePromptCommand(CommandBase, DataFormatMixin):
     """ Command to remove a prompt from the queue.
         del self.res['dbsession']
 
 
-class UpdatePromptCommand(CommandBase, DateParseMixin):
+class UpdatePromptCommand(PromptCommandBase):
     """ Command to remove a prompt from the queue.
 
     Defaults:
         * ``interval`` = 0
 
     """
-    # TODO: Clean up repetition of AddPromptCommand & very long methods
-    required_res = ['reddit', 'dbsession', 'config.minibot', 'config.reddit',
-                    'logger']
-    start_time = 0 # execute ASAP
-
     def __init__(self, msg, **kwargs):
         """ Constructor. Keyword arguments are strings defined as per the
         `CheckCommandSpec` class documentation; the text block is expected as
         a 'text' argument. Throws MissingParameterError or CommandParameterError
         on missing or invalid parameters (respectively). """
+        PromptCommandBase.__init__(msg, **kwargs)
 
-        self.msg = msg
-
-        # required params
+    def _check_param_fields(self):
+        """ Overriden from PromptCommandBase. """
+        # required id param
         try:
-            id_ = kwargs.pop('id')
-            self.id  = int(id_, 10)
+            str_id = kwargs.pop('id')
+            self.id  = int(str_id, 10)
         except KeyError:
             raise MissingParameterError(
                 "The required 'id' parameter is missing.")
         except ValueError, TypeError:
             raise CommandParameterError(
-                ''.join(["The 'id' parameter is not valid: '", id_, "'."]))
-
-
-        # optional params
-        self.user = kwargs.pop('user', None)
-        self.submission = kwargs.pop('submission', None)
-        self.title = kwargs.pop('title', None)
-        self.text  = kwargs.pop('text', None)
-
-        str_date = kwargs.pop('date', None)
-        str_time = kwargs.pop('time', None)
-        if hasattr(str_date, 'strip'): str_date = str_date.strip()
-        if has_attr(str_time, 'strip'): str_time = str_time.strip()
-        self.datetime = _parse_datetime(str_date, str_time)
-        # for an update, empty value indicates default
-        self.date_default = (str_date == '')
-        self.time_default = (str_time == '')
-
-        # if parameters left over, they're not valid
-        if len(kwargs):
-            if len(kwargs) == 1:
-                raise CommandParameterError(
-                    "Unknown parameter: '" + kwargs.keys()[0])
-            else:
-                raise CommandParameterError(
-                    "Unknown parameters: '" + ', '.join(kwargs.keys()))
+                "The 'id' parameter is not valid: '{}'.".format(str_id))
+        return PromptCommandBase._check_param_fields()
 
     def start(self):
-        # resources
-        reddit = self.res['reddit']
-        db     = self.res['dbsession']
-        default_time_str = self.res['config.minibot'].default_time
-        target_reddit = self.res['config.reddit'].target
-        hash_opts = {'globsalt' : self.res['config.minibot'].hash_salt,
-                     'rounds' : self.res['config.minibot'].hash_rounds}
-        self.log = self.res['logger']
+        db = self.res['dbsession']
 
-        upd_prompt = db.query(Prompt).filter(Prompt.id==self.id).first()
-        if upd_prompt is None:
+        # retrieve prompt to update and check it
+        self.prompt = db.query(Prompt).filter(Prompt.id==self.id).first()
+        if self.prompt is None:
             raise CommandParameterError(
-                ''.join(["Prompt ", self.id, " not found."]))
+                "Prompt {:d} not found.".format(self.id))
+        if self.prompt.status == Prompt.STATUS_POSTED:
+            raise CommandParameterError(
+                "Prompt {:d} is already posted.".format(self.id))
 
-        if upd_prompt.status == Prompt.STATUS_POSTED:
-            raise CommandParameterError(
-                ''.join(["Prompt ", self.id, " is already posted."]))
+        # pre-process raw params - behaviour differs for update action
+        # date and time. Original bhav: absent = default value; '' = invalid.
+        if self.raw_params.get('date', None) is None: # absent = no change
+            self.raw_params['date'] = self.prompt.post_time.date()
+        elif not self.raw_params.get('date', None): # '' = default value
+            self.raw_params['date'] = None
 
-        ### process default and inferred Prompt attributes ###
-        # title/text
-        if self.title is not None:
-            upd_prompt.title = self.title
-        if self.text:
-            upd_prompt.text = self.text
+        if self.raw_params.get('time', None) is None: # absent = no change
+            self.raw_params['time'] = self.prompt.post_time.time()
+        elif not self.raw_params.get('time', None): # '' = default value
+            self.raw_params['time'] = None
 
-        # post_time
-        if self.date_default and self.time_default:
-            default_date = self._get_autodate()
-            default_time = self._parse_datetime(default_time_str)
-            self.datetime = datetime.combine(default_date, default_time)
-        elif self.date_default:
-            # in this case, self.datetime is a dt_time object instead
-            default_date = self._get_autodate()
-            self.datetime = datetime.combine(default_date, self.datetime)
-        elif self.time_default:
-            default_time = self._parse_datetime(default_time_str)
-            self.datetime = datetime.combine(self.datetime.date(), default_time)
+        self._parse_params()
+        self.run()
 
-        upd_prompt.queue(self._local_to_utc(self.datetime))
+    def run(self):
+        # shorthand
+        db = self.res['dbsession']
+        p = self.params
+        prompt = self.prompt
+
+        # first check User table for approver
+        self._make_approver_user(p['r_approver_id'], self.msg.author.name)
+
+        # now update prompt in database (retrieved in start())
+        if p['title'] is not None:
+            prompt.title = p['title']
+        if p['text'] is not None:
+            prompt.text = p['text']
+
+        prompt.queue(p['post_time'])
+
+        if p['user'] is not None:
+            prompt.user = p['user']
+            prompt.r_user_id = p['r_user_id']
+
+        if p['r_source_url'] is not None:
+            prompt.r_source_url = p['r_source_url']
+
+        # update approver - replace as if old queued prompt is replaced
+        prompt.r_approver_id = self.msg.author.id
 
         # update the submit time - treat as if old queued prompt is replaced
-        upd_prompt.submit_time = datetime.utcnow()
+        prompt.submit_time = datetime.utcfromtimestamp(self.msg.created_utc)
 
-        # user
-        if self.user is not None:
-            try:
-                upd_prompt.r_user_id = reddit.get_redditor(user).id
-                upd_prompt.user = self.user
-            except HTTPError:
-                raise CommandParameterError(
-                    ''.join(["Unknown Reddit user: '", self.user, "'."]))
-
-        # Source submission URL
-        if self.submission is not None:
-            if self.submission: # if not blank
-                source_url_parsed = urlparse(self.submission)
-                domain = source_url_parsed.netloc
-                if domain == reddit.config.domain or\
-                   domain == reddit.config.domain.replace('www.', '') or\
-                   domain == reddit.config._short_domain.replace('http://', '').\
-                                replace('https://', ''):
-                    upd_prompt.r_source_url = self.submission
-                else:
-                    raise CommandParameterError(
-                        ''.join(["Invalid domain on source URL: '",
-                                 self.submission, "'."]))
-            else:
-                upd_prompt.r_source_url = ''
-
-
-        # approver - replace, as if old queued prompt is deleted and replaced
-        upd_prompt.r_approver_id = self.msg.author.id
-        approver_name = self.msg.author.name
-
-        if not db.query(User).filter(User.r_id==upd_prompt.r_approver_id).count():
-            new_user = User(upd_prompt.r_approver_id, approver_name, **hash_opts)
-            db.add(new_user) # set as unregistered by default
-
+        # finalise
+        db.commit()
         self.log.info(
             "%s: Updated prompt '%s' (%d) in database queue.",
             classname(self), self.title, upd_prompt.id)
         self.log.debug("%s: %s", classname(self), repr(upd_prompt))
 
-        db.commit()
+        # and remove the prompt from the scheduler queue, if applicable
+        self._check_event_queue()
+
+        # take care of things reddit-side
         self.msg.mark_as_read()
-
         self.log.info("%s: Marked message %s as read.",
                     classname(self), self.msg.name)
 
-        reply_title = ''.join([target_reddit, ": Prompt updated"])
+        reply_title = "Prompt updated"
         reply_text = u"Prompt {id_:d} has been updated.\n\n___\n\n{prompt}".\
             format(id_=upd_prompt.id, prompt=self._format_prompt(upd_prompt))
         self.owner.queue_command(
             SendReplyCommand(self.msg, reply_title, reply_text))
 
-    def run(self):
-        pass
-
-    def end(self):
-        self.res['dbsession'].close()
-        del self.res['dbsession']
-
-    def _get_autodate(self):
-        db = self.res['dbsession']
-        a_day = timedelta(days=1)
-        # get today in UTC timezone
-        current_date = datetime.utcnow().date()
-        # iterate queued prompts for the future, except prompt being modified
-        for q_time in db.query(Prompt.post_time).\
-                      filter(Prompt.status == Prompt.STATUS_QUEUED).\
-                      filter(Prompt.post_time >= datetime.utcnow()).\
-                      filter(Prompt.id != self.id).\
-                      order_by(Prompt.post_time):
-            q_date = q_time.date()
-            if current_date == q_date:
-                current_date += a_day
-            elif current_date < q_date:
-                break
-            else:
-                continue
-        self.log.debug("%s: Nearest unqueued date found: %s.",
-            classname(self), current_date.stftime('%Y-%m-%d'))
-        return current_date
+    def _check_event_queue(self):
+        """ Check whether a prompt add event for this prompt is in the
+        scheduler. If so, remove it. """
+        for ev in self.owner.get_events(PostPromptCommand):
+            if ev.post_id == self.id:
+                self.owner.remove_event(ev)
 
 
 class SendMessageCommand(CommandBase):

minibot/eventscheduler.py

                                   classname(self), repr(event))
 
     def queue_event(self, event):
-        """ Queues a new event object for execution. The event object should
+        """ Queue a new event object for execution. The event object should
         have been newly constructed; an event object that has already been
         queued or executed may not have a consistent internal state for queueing
         and starting. """
-        if hasattr(event, 'owner'):
+        if hasattr(event, 'owner') and owner is not None:
             raise AttachedEventError(("Cannot queue Event '{}': "
                 "Event is already attached to an EventScheduler. Please only "
                 "queue new events to the scheduler.").format(classname(event)))
             time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(event.start_time)),
             r_priority, self.STATUS_STRING[r_status], repr(event))
 
+    def remove_event(self, event):
+        """ Remove a particular event object from the queue. """
+        del_index = None
+        for q_i, q_evtuple in enumerate(self._queue):
+            if event is q_evtuple[3]:
+                del_index = q_i
+                break
+        if del_index is not None:
+            del self._queue[del_index]
+
     def _insert_event(self, time, priority, status, event):
         """ Insert an event into the appropriate location in a queue. """
         evtuple = (time, priority, status, event)