class Interpreter(object): def __init__(self, treasurer): self.tr = treasurer self.sender = None self.res = None self.log = None # region Services @staticmethod def _eval_daterange(dr): """ :param dr: daterance tuple of 2 elements, each representing a number of days in the past, with 0 being today :return: a new tuple, with datetime instances as values (or None) NOTE: the first element is set at the BEGINNING of its day, the second at the END (when they're not None) """ ret = [] for i, d in enumerate(dr): if d is not None: time = utc_now() - datetime.timedelta(days=d) if i == 0: time.replace(hour=0, minute=0, second=0) elif i == 1: time.replace(hour=23, minute=59, second=59) ret.append(time) else: ret.append(d) return ret # endregion # region balance def _cmd_balance_balance(self, cmd): cur = self.tr.dm.cur_mng.get_by_id(cmd['currency']) if cmd['currency'] is not None and cur is None: return self.res.void_not_found_error() return self.res.write(user=self.sender.clone(), cur=DataEl.clone_el(cur)) def _cmd_balance_debts(self, cmd): logs = self.tr.lm.retrieve( _type_='PaymentLog', user=self.sender, daterange=self._eval_daterange(cmd['daterange']), operation='debit', limit=IMP.OUT['n_logs'], ) return self.res.write(user=self.sender.clone(), logs=list(logs)) def _cmd_balance_credits(self, cmd): logs = self.tr.lm.retrieve( _type_='PaymentLog', user=self.sender, daterange=self._eval_daterange(cmd['daterange']), operation='credit', limit=IMP.OUT['n_logs'], ) return self.res.write(user=self.sender.clone(), logs=list(logs)) def _cmd_balance_debtors(self, cmd): cur = self.tr.dm.cur_mng.get_by_id(cmd['currency']) if cmd['currency'] is not None and cur is None: return self.res.void_not_found_error() if cur is None: curs = self.tr.dm.cur_mng.currencies.values() else: curs = [cur] data_per_cur = {cur.id: self.tr.dm.user_mng.get_all_debtors(cur) for cur in curs} return self.res.write(data_per_cur=data_per_cur) def _cmd_balance_creditors(self, cmd): cur = self.tr.dm.cur_mng.get_by_id(cmd['currency']) if cmd['currency'] is not None and cur is None: return self.res.void_not_found_error() if cur is None: curs = self.tr.dm.cur_mng.currencies.values() else: curs = [cur] data_per_cur = {cur.id: self.tr.dm.user_mng.get_all_creditors(cur) for cur in curs} return self.res.write(data_per_cur=data_per_cur) # endregion # region log def _cmd_log_log(self, cmd): logs = self.tr.lm.retrieve( daterange=cmd['daterange'], tag=cmd['tag'], limit=IMP.OUT['n_logs'] if cmd['n'] is None else cmd['n'] ) return self.res.write(logs=list(logs)) def _cmd_log___clear___log(self, cmd): daterange = self._eval_daterange(cmd['daterange']) n_deleted = self.tr.lm.clear_logs(daterange[1]) return self.res.commit() # endregion # region currency def _cmd_currency_currency(self, cmd): curs = {} missing_cids = [] for cur_id in cmd['currencies']: cur = self.tr.dm.cur_mng.get_by_id(cur_id) if cur is None: missing_cids.append(cur_id) else: curs[cur_id] = cur if missing_cids: if curs: self.res.not_found_warning(cur=missing_cids) else: return self.res.void_not_found_error(cur=missing_cids) return self.res.write( user=self.sender.clone(), currencies=curs, ) def _cmd_currency_add_currency(self, cmd): if self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0]) is None: self.tr.dm.cur_mng.add( Currency(id_=cmd['currencies'][0], rate=cmd['rate'], manager=self.tr.dm.cur_mng) ) return self.res.commit() return self.res.void() def _cmd_currency_set_currency(self, cmd): cur = self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0]) # cannot modify a Currency if is __base__ or doesn't exist if cur is None: return self.res.void_not_found_error() changed = False if cmd['rate'] > 0 and cmd['rate'] != cur.rate: cur.rate = cmd['rate'] changed = True elif cmd['val'] is not None: if cmd['val'] == 'auto' and not cur.auto_convert: cur.auto_convert = True changed = True elif cmd['val'] == 'default': if cmd['global'] is True: if cur != self.tr.dm.cur_mng.default: self.tr.dm.cur_mng.set_default(cur) changed = True else: self.sender.set_def_cur(cur) changed = True if not changed: return self.res.void() return self.res.commit() def _cmd_currency_clear_currency_rate(self, cmd): cur = self.tr.dm.cur_mng.get_by_id(cmd['currencies'][0]) if cur.clear_rate() is None: return self.res.void_not_found_error() return self.res.commit() def _cmd_currency_clear_default_currency(self, cmd): if cmd['global']: if self.tr.dm.cur_mng.clear_default(): return self.res.commit() else: if self.sender.set_def_cur(None): return self.res.commit() return self.res.void() def _cmd_currency_merge_currency(self, cmd): curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']] # all currencies must exist for call to be valid if None in curs: return self.res.void_not_found_error() if self.tr.dm.user_mng.merge_cur(curs) == 0: return self.res.void() return self.res.commit() def _cmd_currency_remove_currency(self, cmd): """ unlike other cases, only committed if ALL requested curs can be removed """ curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']] if None in curs: return self.res.not_found_error() for cur in curs: # remove might be None or False if not self.tr.dm.cur_mng.remove(cur, user_mng=self.tr.dm.user_mng): return self.res.invalid_operation_error() return self.res.commit() def _cmd_currency___reset___currency(self, cmd): """ unlike other cases, only committed if ALL requested curs can be reset """ curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']] if None in curs: return self.res.not_found_error() # resetting a currency is obtained by removing it from all # users but not from the CurrencyManager if self.tr.dm.user_mng.remove_cur(curs) == 0: return self.res.void() return self.res.commit() def _cmd_currency___ground_zero___currency(self, cmd): """ unlike other cases, only committed if ALL requested curs can be destroyed """ curs = [self.tr.dm.cur_mng.get_by_id(cid) for cid in cmd['currencies']] # keep only valid currencies if None in curs: return self.res.not_found_error() for cur in curs: # remove might be None or False if not self.tr.dm.cur_mng.remove( cur, ground_zero=True, user_mng=self.tr.dm.user_mng): return self.res.invalid_operation_error() return self.res.commit() # endregion # region undo def _cmd_undo_undo(self, cmd, f='do_undo'): res = None if cmd['log_id'] is None: assert self.sender.lm == self.tr.lm res = getattr(self.sender, f)() else: log = self.tr.lm.get_by_id(cmd['log_id']) if log: res = getattr(log, f)() if not res: return self.res.invalid_operation_error() if res is not True: # res might be a dict of users/currency which had been # deleter and hat do be restored, in order to undo return self.res.not_found_warning(**res) return self.res.commit() def _cmd_undo_redo(self, cmd): return self._cmd_undo_undo(cmd, f='do_redo') # endregion # region groups def _cmd_groups_groups(self, cmd): """ read requested groups(s) or all existing groups sets groups dict in self.result """ groups = {} missing_gids = [] if cmd['groups']: for gid in cmd['groups']: g = self.tr.dm.group_mng.get_by_id(gid) if g is None: missing_gids.append(gid) else: groups[gid] = g else: groups = None if missing_gids: self.res.not_found_warning(groups=missing_gids) # user_spec: if True, user asked for specific group(s) (show details) return self.res.write(groups=groups) def _cmd_groups_my_groups(self, cmd): return self.res.write(groups=self.sender.get_groups()) def _cmd_groups_add_group(self, cmd): if self.tr.dm.group_mng.get_by_id(cmd['groups'][0]) is None: g = Group(id_=cmd['groups'][0], manager=self.tr.dm.group_mng) created_uids = [] for uid in cmd['users']: if uid is None: uid = self.sender.id user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True) g.add_user(user) if created: created_uids.append(uid) if created_uids: self.res.not_found_warning(new_users=created_uids) self.tr.dm.group_mng.add(g) return self.res.commit() return self.res.invalid_operation_error(group_already_exist=cmd['groups']) def _cmd_groups_remove_groups(self, cmd): groups = [self.tr.dm.group_mng.get_by_id(gid) for gid in cmd['groups']] # remove already un-existing groups from list, remove others if None in groups: groups = [g for g in groups if g is not None] self.res.not_found_warning( groups=[gid for gid in cmd['groups'] if gid not in [g.id for g in groups]] ) for g in groups: self.tr.dm.group_mng.remove(g) return self.res.commit() def _cmd_groups_remove_empty_groups(self, cmd): self.tr.dm.group_mng.remove_empty_groups() return self.res.commit() def _cmd_groups_add_users(self, cmd): group = self.tr.dm.group_mng.get_by_id(cmd['groups'][0]) users = [] created_uids = [] for uid in cmd['users']: user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True) users.append(user) if created: created_uids.append(uid) if group is None: return self.res.void_not_found_error() if created_uids: self.res.not_found_warning(new_users=created_uids) for u in users: group.add_user(u) return self.res.commit() def _cmd_groups_remove_user(self, cmd): users = [self.tr.dm.user_mng.get_by_id(uid) for uid in cmd['users']] group = self.tr.dm.group_mng.get_by_id(cmd['groups'][0]) if group is None: return self.res.void_not_found_error() users = [u for u in users if u is not None] do_not_exist = [uid for uid in cmd['users'] if uid not in [u.id for u in users]] # those which exist but do not belong to the group not_in_group = [u.id for u in users if not group.has_user(u)] if do_not_exist: self.res.not_found_warning(do_not_exist=do_not_exist) if not_in_group: self.res.not_found_warning(not_in_group=not_in_group) n_removed = 0 for u in users: n_removed += group.rm_user(u) return self.res.commit() def _cmd_groups_remove_users(self, cmd): return self._cmd_groups_remove_user(cmd) # endregion # region users def _cmd_users_users(self, cmd): if cmd['users']: users = [self.tr.dm.user_mng.get_by_id(uid) for uid in cmd['users']] if None in users: users = [u for u in users if u is not None] self.res.not_found_warning( do_not_exist=[uid for uid in cmd['users'] if uid not in [u.id for u in users]], ) else: users = None return self.res.write(users=users) # endregion # region greetings def _cmd_greetings_goodbye(self, cmd): self.sender.status = None return self.res.commit() # endregion # region payment def _cmd_payment_pay(self, cmd): # properly collect all users in the cmd for gid in cmd['groups_order']: modifier = cmd['groups'][gid]['modifier'] amount = cmd['groups'][gid]['amount'] g_uids = self.tr.dm.group_mng.get_by_id(gid) if g_uids is None: return self.res.void_not_found_error() g_uids = g_uids.uids for uid in g_uids: if uid not in cmd['users']: cmd['users'][uid] = { 'modifier': modifier, 'amount': amount, } if None in cmd['users']: if self.sender.id not in cmd['users']: cmd['users'][self.sender.id] = cmd['users'][None] del cmd['users'][None] # prepare total amount amount = cmd['amount'] # select currency to use and update amount if auto-converting cur = self.tr.dm.cur_mng.get_cur_for_user(self.sender, cid=cmd['currency']) self.log.cur = cur if cur is None: return self.res.not_found_error(currency=[cmd['currency']]) # pre-process payment n_sharer = 0 for uid in cmd['users']: u = cmd['users'][uid] u['payed'] = 0 if u['modifier'] is None: n_sharer += 1 elif u['modifier'] == '*': n_sharer += u['amount'] elif u['modifier'] == '+': u['payed'] += u['amount'] amount -= u['amount'] n_sharer += 1 elif u['modifier'] == '.': u['payed'] = u['amount'] amount -= u['amount'] # share and pay created_uids = [] share = float(amount) / n_sharer for uid in cmd['users']: u = cmd['users'][uid] if u['modifier'] is None: u['payed'] += share elif u['modifier'] == '*': u['payed'] += u['amount'] * share elif u['modifier'] == '+': u['payed'] += share elif u['modifier'] == '.': pass user, created = self.tr.dm.user_mng.get_by_id(uid, auto_create=True) user.add_debit(cur, u['payed']) self.log.add_action('debit', user, u['payed']) if created: created_uids.append(uid) if len(created_uids): self.res.not_found_warning(new_users=created_uids) # give credit to payer payer_id = cmd['payer'] if cmd['payer'] is not None else self.sender.id payer = self.tr.dm.user_mng.get_by_id(payer_id) payer.add_credit(cur, cmd['amount']) self.log.add_action('credit', payer, cmd['amount']) return self.res.commit(cur_id=[cur.id]) # endregion def interpret(self, message, cmd): """ :param message: instance of Message :param cmd: the PARSED command to execute :return: """ # make sure sender is known to Treasurer sender, created = self.tr.dm.user_mng.get_by_id( message.sender.lower(), auto_create=True) if sender.status is None: sender.status = 'confirmed' # fix special cases (BEFORE creating Log instance!) if 'currencies' in cmd and cmd['currencies'] == 'all': cmd['currencies'] = self.tr.dm.cur_mng.currencies.keys() if 'groups' in cmd and cmd['domain'] == 'groups' and cmd['groups'] == 'all': cmd['groups'] = self.tr.dm.group_mng.groups.keys() self.sender = sender self.res = Result(treasurer=self.tr, sender=sender.clone(), cmd=cmd) self.log = self.tr.lm.log_factory(message, cmd) try: f = getattr(self, '_cmd_' + cmd['domain'] + '_' + cmd['cmd']) f(cmd) except (AttributeError, TypeError): # includes those which need no processing. Incomplete list: # easter_egg (who_is_your_daddy) # error # greetings, 'hi' # stats / stats_plus # help # default self.res.none() # if IMP.DEBUG: # raise return self.res, self.log