def test_choices_num_enum(self): choices_num = ChoicesNumEnum( ('A', 'a'), ('B', 'b'), ) assert_equal(choices_num.A, 1) assert_equal(choices_num.B, 2) assert_equal(list(choices_num.choices), [(1, 'a'), (2, 'b')]) assert_equal(tuple(choices_num.all), (1, 2)) assert_equal(choices_num.get_label(1), 'a') assert_equal(choices_num.get_label(2), 'b') assert_raises(AttributeError, choices_num.get_label, 3)
def test_choices_num_enum_should_return_right_values_and_choices(self): choices_num = ChoicesNumEnum( ('A', 'label a'), ('B', 'label b'), ) assert_equal(choices_num.A, 1) assert_equal(choices_num.B, 2) assert_equal(list(choices_num.choices), [(1, 'label a'), (2, 'label b')]) assert_equal(tuple(choices_num.all), (1, 2)) assert_equal(choices_num.get_label(1), 'label a') assert_equal(choices_num.get_label(2), 'label b') assert_raises(AttributeError, choices_num.get_label, 3)
def test_choices_num_enum_with_defined_ids_should_return_right_values_and_choices( self): choices_num = ChoicesNumEnum(('A', 'label a', 4), ('B', 'label b', 2), ('C', 'label c')) assert_equal(choices_num.A, 4) assert_equal(choices_num.B, 2) assert_equal(choices_num.C, 3) assert_equal(list(choices_num.choices), [(4, 'label a'), (2, 'label b'), (3, 'label c')]) assert_equal(choices_num.all, (4, 2, 3)) assert_equal(choices_num.get_label(4), 'label a') assert_equal(choices_num.get_label(2), 'label b') assert_equal(choices_num.get_label(3), 'label c')
def test_choices_num_enum_with_defined_ids_should_return_right_values_and_choices(self): choices_num = ChoicesNumEnum( ('A', 'label a', 4), ('B', 'label b', 2), ('C', 'label c') ) assert_equal(choices_num.A, 4) assert_equal(choices_num.B, 2) assert_equal(choices_num.C, 3) assert_equal(list(choices_num.choices), [(4, 'label a'), (2, 'label b'), (3, 'label c')]) assert_equal(choices_num.all, (4, 2, 3)) assert_equal(choices_num.get_label(4), 'label a') assert_equal(choices_num.get_label(2), 'label b') assert_equal(choices_num.get_label(3), 'label c')
def test_choices_num_enum_with_defined_ids(self): choices_num = ChoicesNumEnum( ('A', 'a', 4), ('B', 'b', 2), ('C', 'c') ) assert_equal(choices_num.A, 4) assert_equal(choices_num.B, 2) assert_equal(choices_num.C, 3) assert_equal(list(choices_num.choices), [(4, 'a'), (2, 'b'), (3, 'c')]) assert_equal(tuple(choices_num.all), (4, 2, 3)) assert_equal(choices_num.get_label(4), 'a') assert_equal(choices_num.get_label(2), 'b') assert_equal(choices_num.get_label(3), 'c')
class AbstractPushNotificationMessage(BaseMessage): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('SENT', _('sent'), 2), ('ERROR', _('error'), 3), ('DEBUG', _('debug'), 4), ('ERROR_RETRY', _('error retry'), 5), ) template = models.ForeignKey(settings.PUSH_NOTIFICATION_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='push_notifications') state = models.PositiveIntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False, db_index=True) heading = models.TextField(verbose_name=_('heading')) url = models.URLField(verbose_name=_('URL'), null=True, blank=True) class Meta(BaseMessage.Meta): abstract = True verbose_name = _('push notification') verbose_name_plural = _('push notifications') @property def failed(self): return self.state in {self.STATE.ERROR, self.STATE.ERROR_RETRY}
class TestDispatchersModel(chamber_models.SmartModel): STATE = ChoicesNumEnum( ('FIRST', _('first'), 1), ('SECOND', _('second'), 2), ) state = models.IntegerField(null=True, blank=False, choices=STATE.choices, default=STATE.FIRST) dispatchers = ( CreatedDispatcher(create_csv_record_handler, signal=dispatcher_pre_save), StateDispatcher(create_test_smart_model_handler, STATE, state, STATE.SECOND, signal=dispatcher_pre_save), PropertyDispatcher(create_test_fields_model_handler, 'always_dispatch', signal=dispatcher_post_save), PropertyDispatcher(create_test_dispatchers_model_handler, 'never_dispatch', signal=dispatcher_post_save), OneTimeStateChangedHandler(), ) @property def always_dispatch(self): return True @property def never_dispatch(self): return False
class Gear(SmartModel): TYPE = ChoicesNumEnum( ('SHOE', 'Shoe', 1), ('BIKE', 'Bike', 2), ('SKI', 'Ski', 3), ('OTHER', 'Other', 4), ) name = models.CharField(verbose_name='name', max_length=50, null=False, blank=False) type = models.PositiveSmallIntegerField(verbose_name='gear type', choices=TYPE.choices, null=False, blank=False) strava_id = models.CharField(verbose_name='strava ID', max_length=10, null=True, blank=True) def __str__(self): return self.name class Meta: ordering = ('-created_at', ) verbose_name = 'gear' verbose_name_plural = 'gears'
class BaseMessage(SmartModel): STATE = ChoicesNumEnum() sent_at = models.DateTimeField(verbose_name=_('sent at'), null=True, blank=True, editable=False) recipient = models.CharField(verbose_name=_('recipient'), null=False, blank=False, max_length=20, db_index=True) content = models.TextField(verbose_name=_('content'), null=False, blank=False) template_slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=True, blank=True, editable=False, db_index=True) state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) backend = models.CharField(verbose_name=_('backend'), null=True, blank=True, editable=False, max_length=250) error = models.TextField(verbose_name=_('error'), null=True, blank=True, editable=False) extra_data = models.JSONField(verbose_name=_('extra data'), null=True, blank=True, editable=False, encoder=DjangoJSONEncoder) extra_sender_data = models.JSONField(verbose_name=_('extra sender data'), null=True, blank=True, editable=False, encoder=DjangoJSONEncoder) tag = models.SlugField(verbose_name=_('tag'), null=True, blank=True, editable=False) number_of_send_attempts = models.PositiveIntegerField(verbose_name=_('number of send attempts'), null=False, blank=False, default=0) priority = models.PositiveSmallIntegerField(verbose_name=_('priority'), null=False, blank=False, default=settings.DEFAULT_MESSAGE_PRIORITY) objects = MessageManager.from_queryset(MessageQueryset)() def __str__(self): return self.recipient class Meta: abstract = True ordering = ('-created_at',)
class EmailMessage(BaseMessage): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('SENDING', _('sending'), 2), ('SENT', _('sent'), 3), ('ERROR', _('error'), 4), ('DEBUG', _('debug'), 5), ) recipient = models.EmailField(verbose_name=_('recipient'), blank=False, null=False) template = models.ForeignKey(settings.EMAIL_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='email_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) sender = models.EmailField(verbose_name=_('sender'), blank=False, null=False) sender_name = models.CharField(verbose_name=_('sender name'), blank=True, null=True, max_length=250) subject = models.TextField(verbose_name=_('subject'), blank=False, null=False) number_of_send_attempts = models.PositiveIntegerField( verbose_name=_('number of send attempts'), null=False, blank=False, default=0) @property def friendly_sender(self): """ returns sender with sender name in standard address format if sender name was defined """ return '{} <{}>'.format( self.sender_name, self.sender) if self.sender_name else self.sender @property def failed(self): return self.state == self.STATE.ERROR def __str__(self): return '{}: {}'.format(self.recipient, self.subject) class Meta(BaseMessage.Meta): verbose_name = _('e-mail message') verbose_name_plural = _('e-mail messages')
class BaseMessage(SmartModel): STATE = ChoicesNumEnum() sent_at = models.DateTimeField(verbose_name=_('sent at'), null=True, blank=True, editable=False) recipient = models.CharField(verbose_name=_('recipient'), null=False, blank=False, max_length=20) content = models.TextField(verbose_name=_('content'), null=False, blank=False) template_slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=True, blank=True, editable=False) state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) backend = models.CharField(verbose_name=_('backend'), null=True, blank=True, editable=False, max_length=250) error = models.TextField(verbose_name=_('error'), null=True, blank=True, editable=False) extra_data = JSONField(verbose_name=_('extra data'), null=True, blank=True, editable=False) extra_sender_data = JSONField(verbose_name=_('extra sender data'), null=True, blank=True, editable=False) tag = models.SlugField(verbose_name=_('tag'), null=True, blank=True, editable=False) def __str__(self): return self.recipient class Meta: abstract = True ordering = ('-created_at', )
class OutputSMSMessage(BaseMessage): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('UNKNOWN', _('unknown'), 2), ('SENDING', _('sending'), 3), ('SENT', _('sent'), 4), ('ERROR_UPDATE', _('error message update'), 5), ('DEBUG', _('debug'), 6), ('DELIVERED', _('delivered'), 7), ('ERROR', _('error'), 8), ('ERROR_RETRY', _('error retry'), 9), ) content = models.TextField(verbose_name=_('content'), null=False, blank=False, max_length=700) template = models.ForeignKey(settings.SMS_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='output_sms_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False, db_index=True) sender = models.CharField(verbose_name=_('sender'), null=True, blank=True, max_length=20) class Meta(BaseMessage.Meta): verbose_name = _('output SMS') verbose_name_plural = _('output SMS') def clean_recipient(self): self.recipient = normalize_phone_number(force_text(self.recipient)) def clean_content(self): if not settings.SMS_USE_ACCENT: self.content = str(remove_accent(str(self.content))) @property def failed(self): return self.state in {self.STATE.ERROR, self.STATE.ERROR_RETRY}
class OutputSMSMessage(SmartModel): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('UNKNOWN', _('unknown'), 2), ('SENDING', _('sending'), 3), ('SENT', _('sent'), 4), ('ERROR', _('error'), 5), ('DEBUG', _('debug'), 6), ('DELIVERED', _('delivered'), 7), ) sent_at = models.DateTimeField(verbose_name=_('sent at'), null=True, blank=True, editable=False) sender = models.CharField(verbose_name=_('sender'), null=True, blank=True, max_length=20) recipient = models.CharField(verbose_name=_('recipient'), null=False, blank=False, max_length=20) content = models.TextField(verbose_name=_('content'), null=False, blank=False, max_length=700) template_slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=True, blank=True, editable=False) template = models.ForeignKey(settings.SMS_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='output_sms_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) backend = models.CharField(verbose_name=_('backend'), null=True, blank=True, editable=False, max_length=250) error = models.TextField(verbose_name=_('error'), null=True, blank=True, editable=False) extra_data = JSONField(verbose_name=_('extra data'), null=True, blank=True, editable=False) extra_sender_data = JSONField(verbose_name=_('extra sender data'), null=True, blank=True, editable=False) tag = models.SlugField(verbose_name=_('tag'), null=True, blank=True, editable=False) def clean_recipient(self): self.recipient = normalize_phone_number(force_text(self.recipient)) def clean_content(self): if not settings.SMS_USE_ACCENT: self.content = six.text_type(remove_accent(six.text_type(self.content))) @property def failed(self): return self.state == self.STATE.ERROR def __str__(self): return str(self.recipient) class Meta: verbose_name = _('output SMS') verbose_name_plural = _('output SMS') ordering = ('-created_at',)
class AbstractDialerMessage(BaseMessage): STATE = ChoicesNumEnum( ('NOT_ASSIGNED', _('not assigned'), 0), ('READY', _('ready'), 1), ('RESCHEDULED_BY_DIALER', _('rescheduled by dialer'), 2), ('CALL_IN_PROGRESS', _('call in progress'), 3), ('HANGUP', _('hangup'), 4), ('DONE', _('done'), 5), ('RESCHEDULED', _('rescheduled'), 6), ('ANSWERED_COMPLETE', _('listened up complete message'), 7), ('ANSWERED_PARTIAL', _('listened up partial message'), 8), ('UNREACHABLE', _('unreachable'), 9), ('DECLINED', _('declined'), 10), ('UNANSWERED', _('unanswered'), 11), ('ERROR', _('error'), 66), ('DEBUG', _('debug'), 77), ) template = models.ForeignKey(settings.DIALER_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='dialer_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) def clean_recipient(self): self.recipient = normalize_phone_number(force_text(self.recipient)) @property def failed(self): return self.state == self.STATE.ERROR class Meta(BaseMessage.Meta): abstract = True verbose_name = _('dialer message') verbose_name_plural = _('dialer messages')
class EmailMessage(SmartModel): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('SENDING', _('sending'), 2), ('SENT', _('sent'), 3), ('ERROR', _('error'), 4), ('DEBUG', _('debug'), 5), ) sent_at = models.DateTimeField(verbose_name=_('sent at'), null=True, blank=True, editable=False) recipient = models.EmailField(verbose_name=_('recipient'), blank=False, null=False) sender = models.EmailField(verbose_name=_('sender'), blank=False, null=False) sender_name = models.CharField(verbose_name=_('sender name'), blank=True, null=True, max_length=250) subject = models.TextField(verbose_name=_('subject'), blank=False, null=False) content = models.TextField(verbose_name=_('content'), null=False, blank=False) template_slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=True, blank=True, editable=False) template = models.ForeignKey(settings.EMAIL_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='email_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) backend = models.CharField(verbose_name=_('backend'), null=True, blank=True, editable=False, max_length=250) error = models.TextField(verbose_name=_('error'), null=True, blank=True, editable=False) extra_data = JSONField(verbose_name=_('extra data'), null=True, blank=True, editable=False) extra_sender_data = JSONField(verbose_name=_('extra sender data'), null=True, blank=True, editable=False) tag = models.SlugField(verbose_name=_('tag'), null=True, blank=True, editable=False) @property def friendly_sender(self): """ returns sender with sender name in standard address format if sender name was defined """ return '{} <{}>'.format(self.sender_name, self.sender) if self.sender_name else self.sender @property def failed(self): return self.state == self.STATE.ERROR class Meta: verbose_name = _('e-mail message') verbose_name_plural = _('e-mail messages') ordering = ('-created_at',)
class TestFieldsModel(chamber_models.SmartModel): STATE = ChoicesNumEnum( ('OK', _('ok'), 1), ('NOT_OK', _('not ok'), 2), ) STATE_REASON = SubstatesChoicesNumEnum({ STATE.OK: ( ('SUB_OK_1', _('1st substate'), 1), ('SUB_OK_2', _('2nd substate'), 2), ), STATE.NOT_OK: ( ('SUB_NOT_OK_1', _('1st not ok substate'), 3), ('SUB_NOT_OK_2', _('2nd not ok substate'), 4), ), }) GRAPH = SequenceChoicesNumEnum(( ('FIRST', _('first'), 1, ('SECOND',)), ('SECOND', _('second'), 2, ('THIRD',)), ('THIRD', _('third'), 3, ()), ), initial_states=('FIRST',)) decimal = chamber_fields.DecimalField(null=True, blank=True, min=3, max=10, max_digits=5, decimal_places=3) state = models.IntegerField(null=True, blank=False, choices=STATE.choices, default=STATE.OK) state_reason = chamber_models.SubchoicesPositiveIntegerField(null=True, blank=True, enum=STATE_REASON, supchoices_field_name='state', default=STATE_REASON.SUB_OK_1) state_prev = chamber_models.PrevValuePositiveIntegerField(verbose_name=_('previous state'), null=False, blank=False, copy_field_name='state', choices=STATE.choices, default=STATE.NOT_OK) state_graph = chamber_models.EnumSequencePositiveIntegerField(verbose_name=_('graph'), null=True, blank=True, enum=GRAPH) file = chamber_models.FileField(verbose_name=_('file'), null=True, blank=True, allowed_content_types=('application/pdf', 'text/plain', 'text/csv')) image = chamber_models.ImageField(verbose_name=_('image'), null=True, blank=True, max_upload_size=1) price = chamber_models.PriceField(verbose_name=_('price'), null=True, blank=True, currency=_('EUR')) total_price = chamber_models.PositivePriceField(verbose_name=_('total price'), null=True, blank=True)
class ATSSMSBackend(SMSBackend): """ SMS backend that implements ATS operator service https://www.atspraha.cz/ Backend supports check SMS delivery """ REQUEST_TYPES = Enum( 'SMS', 'DELIVERY_REQUEST', ) TEMPLATES = { 'base': 'pymess/sms/ats/base.xml', REQUEST_TYPES.SMS: 'pymess/sms/ats/sms.xml', REQUEST_TYPES.DELIVERY_REQUEST: 'pymess/sms/ats/delivery_request.xml', } class ATSSendingError(Exception): pass ATS_STATES = ChoicesNumEnum( # SMS delivery receipts ('NOT_FOUND', _('not found'), 20), ('NOT_SENT', _('not sent yet'), 21), ('SENT', _('sent'), 22), ('DELIVERED', _('delivered'), 23), ('NOT_DELIVERED', _('not delivered'), 24), ('UNKNOWN', _('not able to determine the state'), 25), # Authentication ('AUTHENTICATION_FAILED', _('authentication failed'), 100), # Internal errors ('DB_ERROR', _('DB error'), 200), # Request states ('OK', _('SMS is OK and ready to be sent'), 0), ('UNSPECIFIED_ERROR', _('unspecified error'), 1), ('BATCH_WITH_NOT_UNIQUE_UNIQ', _('one of the requests has not unique "uniq"'), 300), ('SMS_NOT_UNIQUE_UNIQ', _('SMS has not unique "uniq"'), 310), ('SMS_NO_KW', _('SMS lacks keyword'), 320), ('KW_INVALID', _('keyword not valid'), 321), ('NO_SENDER', _('no sender specified'), 330), ('SENDER_INVALID', _('sender not valid'), 331), ('MO_PR_NOT_ALLOWED', _('MO PR SMS not allowed'), 332), ('MT_PR_NOT_ALLOWED', _('MT PR SMS not allowed'), 333), ('MT_PR_DAILY_LIMIT', _('MT PR SMS daily limit exceeded'), 334), ('MT_PR_TOTAL_LIMIT', _('MT PR SMS total limit exceeded'), 335), ('GEOGRAPHIC_NOT_ALLOWED', _('geographic number is not allowed'), 336), ('MT_SK_NOT_ALLOWED', _('MT SMS to Slovakia not allowed'), 337), ('SHORTCODES_NOT_ALLOWED', _('shortcodes not allowed'), 338), ('UNKNOWN_SENDER', _('sender is unknown'), 339), ('UNSPECIFIED_SMS_TYPE', _('type of SMS not specified'), 340), ('TOO_LONG', _('SMS too long'), 341), ('TOO_MANY_PARTS', _('too many SMS parts (max. is 10)'), 342), ('WRONG_SENDER_OR_RECEIVER', _('wrong number of sender/receiver'), 343), ('NO_RECIPIENT_OR_WRONG_FORMAT', _('recipient is missing or in wrong format'), 350), ('TEXTID_NOT_ALLOWED', _('using "textid" is not allowed'), 360), ('WRONG_TEXTID', _('"textid" is in wrong format'), 361), ('LONG_SMS_TEXTID_NOT_ALLOWED', _('long SMS with "textid" not allowed'), 362), # XML errors ('XML_MISSING', _('XML body missing'), 701), ('XML_UNREADABLE', _('XML is not readable'), 702), ('WRONG_HTTP_METHOD', _('unknown HTTP method or not HTTP POST'), 703), ('XML_INVALID', _('XML invalid'), 705), ) ATS_STATES_MAPPING = { ATS_STATES.NOT_FOUND: OutputSMSMessage.STATE.ERROR, ATS_STATES.NOT_SENT: OutputSMSMessage.STATE.SENDING, ATS_STATES.SENT: OutputSMSMessage.STATE.SENT, ATS_STATES.DELIVERED: OutputSMSMessage.STATE.DELIVERED, ATS_STATES.NOT_DELIVERED: OutputSMSMessage.STATE.ERROR, ATS_STATES.OK: OutputSMSMessage.STATE.SENDING, ATS_STATES.UNSPECIFIED_ERROR: OutputSMSMessage.STATE.ERROR, ATS_STATES.BATCH_WITH_NOT_UNIQUE_UNIQ: OutputSMSMessage.STATE.ERROR, ATS_STATES.SMS_NOT_UNIQUE_UNIQ: OutputSMSMessage.STATE.ERROR, ATS_STATES.SMS_NO_KW: OutputSMSMessage.STATE.ERROR, ATS_STATES.KW_INVALID: OutputSMSMessage.STATE.ERROR, ATS_STATES.NO_SENDER: OutputSMSMessage.STATE.ERROR, ATS_STATES.SENDER_INVALID: OutputSMSMessage.STATE.ERROR, ATS_STATES.MO_PR_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR, ATS_STATES.MT_SK_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR, ATS_STATES.SHORTCODES_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR, ATS_STATES.UNKNOWN_SENDER: OutputSMSMessage.STATE.ERROR, ATS_STATES.UNSPECIFIED_SMS_TYPE: OutputSMSMessage.STATE.ERROR, ATS_STATES.TOO_LONG: OutputSMSMessage.STATE.ERROR, ATS_STATES.TOO_MANY_PARTS: OutputSMSMessage.STATE.ERROR, ATS_STATES.WRONG_SENDER_OR_RECEIVER: OutputSMSMessage.STATE.ERROR, ATS_STATES.NO_RECIPIENT_OR_WRONG_FORMAT: OutputSMSMessage.STATE.ERROR, ATS_STATES.TEXTID_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR, ATS_STATES.WRONG_TEXTID: OutputSMSMessage.STATE.ERROR, ATS_STATES.LONG_SMS_TEXTID_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR, } config = AttrDict({ 'UNIQ_PREFIX': '', 'VALIDITY': 60, 'TEXTID': None, 'URL': 'http://fik.atspraha.cz/gwfcgi/XMLServerWrapper.fcgi', 'OPTID': '', 'TIMEOUT': 5, # 5s }) def _get_extra_sender_data(self): return { 'prefix': self.config.UNIQ_PREFIX, 'validity': self.config.VALIDITY, 'kw': self.config.PROJECT_KEYWORD, 'textid': self.config.TEXTID, } def get_extra_message_kwargs(self): return { 'sender': self.config.OUTPUT_SENDER_NUMBER, } def _serialize_messages(self, messages, request_type): """ Serialize SMS messages to the XML :param messages: list of SMS messages :param request_type: type of the request to the ATS operator :return: serialized XML message that will be sent to the ATS service """ return render_to_string( self.TEMPLATES['base'], { 'username': self.config.USERNAME, 'password': self.config.PASSWORD, 'template_type': self.TEMPLATES[request_type], 'messages': messages, 'prefix': str(self.config.UNIQ_PREFIX) + '-', 'sender': self.config.OUTPUT_SENDER_NUMBER, 'dlr': 1, 'validity': self.config.VALIDITY, 'kw': self.config.PROJECT_KEYWORD, 'billing': 0, 'extra': mark_safe(' textid="{textid}"'.format( textid=self.config.TEXTID)) if self.config.TEXTID else '', }) def _send_requests(self, messages, request_type, is_sending=False, **change_sms_kwargs): """ Performs the actual POST request for input messages and request type. :param messages: list of SMS messages :param request_type: type of the request :param is_sending: True if method is called after sending message :param change_sms_kwargs: extra kwargs that will be stored to the message object """ requests_xml = self._serialize_messages(messages, request_type) try: resp = generate_session(slug='pymess - ATS SMS', related_objects=list(messages)).post( self.config.URL, data=requests_xml, headers={'Content-Type': 'text/xml'}, timeout=self.config.TIMEOUT) if resp.status_code != 200: raise self.ATSSendingError( 'ATS operator returned invalid response status code: {}'. format(resp.status_code)) self._update_sms_states_from_response( messages, self._parse_response_codes(resp.text), is_sending, **change_sms_kwargs) except requests.exceptions.RequestException as ex: raise self.ATSSendingError( 'ATS operator returned returned exception: {}'.format(str(ex))) def _update_sms_states_from_response(self, messages, parsed_response, is_sending=False, **change_sms_kwargs): """ Higher-level function performing serialization of ATS requests, parsing ATS server response and updating SMS messages state according the received response. :param messages: list of SMS messages :param parsed_response: parsed HTTP response from the ATS service :param is_sending: True if update is called after sending message :param change_sms_kwargs: extra kwargs that will be stored to the message object """ messages_dict = {message.pk: message for message in messages} missing_uniq = set(messages_dict.keys()) - set(parsed_response.keys()) if missing_uniq: raise self.ATSSendingError( 'ATS operator not returned SMS info with uniq: {}'.format( ', '.join(map(str, missing_uniq)))) extra_uniq = set(parsed_response.keys()) - set(messages_dict.keys()) if extra_uniq: raise self.ATSSendingError( 'ATS operator returned SMS info about unknown uniq: {}'.format( ', '.join(map(str, extra_uniq)))) for uniq, ats_state in parsed_response.items(): sms = messages_dict[uniq] state = self.ATS_STATES_MAPPING.get(ats_state) error = self.ATS_STATES.get_label( ats_state) if state == OutputSMSMessage.STATE.ERROR else None if is_sending: if error: self._update_message_after_sending_error( sms, state=state, error=error, extra_sender_data={'sender_state': ats_state}, **change_sms_kwargs) else: self._update_message_after_sending( sms, state=state, extra_sender_data={'sender_state': ats_state}, **change_sms_kwargs) else: self._update_message( sms, state=state, error=error, extra_sender_data={'sender_state': ats_state}, **change_sms_kwargs) def publish_messages(self, messages): self._send_requests(messages, request_type=self.REQUEST_TYPES.SMS, is_sending=True, sent_at=timezone.now()) def publish_message(self, message): try: self._send_requests([message], request_type=self.REQUEST_TYPES.SMS, is_sending=True, sent_at=timezone.now()) except self.ATSSendingError as ex: self._update_message_after_sending_error( message, state=OutputSMSMessage.STATE.ERROR, error=str(ex), ) except requests.exceptions.RequestException as ex: # Service is probably unavailable sending will be retried self._update_message_after_sending_error(message, error=str(ex)) # Do not re-raise caught exception. Re-raise exception causes transaction rollback (lost of information # about exception). def _parse_response_codes(self, xml): """ Finds all <code> tags in the given XML and returns a mapping "uniq" -> "response code" for all SMS. In case of an error, the error is logged. :param xml: XML from the ATL response :return: dictionary with pair {SMS uniq: response status code} """ soup = BeautifulSoup(xml, 'html.parser') code_tags = soup.find_all('code') error_message = ', '.join([ (str(self.ATS_STATES.get_label(c)) if c in self.ATS_STATES.all else 'ATS returned an unknown state {}.'.format(c)) for c in [ int(error_code.string) for error_code in code_tags if not error_code.attrs.get('uniq') ] ], ) if error_message: raise self.ATSSendingError( 'Error returned from ATS operator: {}'.format(error_message)) return { int(code.attrs['uniq'].lstrip(str(self.config.UNIQ_PREFIX) + '-')): int(code.string) for code in code_tags if code.attrs.get('uniq') } def update_sms_states(self, messages): self._send_requests(messages, request_type=self.REQUEST_TYPES.DELIVERY_REQUEST)
class SMSOperatorBackend(SMSBackend): """ SMS backend that implements ATS operator service https://www.sms-operator.cz/ Backend supports check SMS delivery """ class SMSOperatorSendingError(Exception): pass REQUEST_TYPES = Enum( 'SMS', 'DELIVERY_REQUEST', ) TEMPLATES = { 'base': 'pymess/sms/sms_operator/base.xml', REQUEST_TYPES.SMS: 'pymess/sms/sms_operator/sms.xml', REQUEST_TYPES.DELIVERY_REQUEST: 'pymess/sms/sms_operator/delivery_request.xml', } SMS_OPERATOR_STATES = ChoicesNumEnum( # SMS states ('DELIVERED', _('delivered'), 0), ('NOT_DELIVERED', _('not delivered'), 1), ('PHONE_NUMBER_NOT_EXISTS', _('number not exists'), 2), # SMS not moved to GSM operator ('TIMEOUTED', _('timeouted'), 3), ('INVALID_PHONE_NUMBER', _('wrong number format'), 4), ('ANOTHER_ERROR', _('another error'), 5), ('EVENT_ERROR', _('event error'), 6), ('SMS_TEXT_TOO_LONG', _('SMS text too long'), 7), # SMS with more parts ('PARTLY_DELIVERED', _('partly delivered'), 10), ('UNKNOWN', _('unknown'), 11), ('PARLY_DELIVERED_PARTLY_UNKNOWN', _('partly delivered, partly unknown'), 12), ('PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN', _('partly not delivered, partly unknown'), 13), ('PARTLY_DELIVERED_PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN', _('partly delivered, partly not delivered, partly unknown'), 14), ('NOT_FOUND', _('not found'), 15), ) SMS_OPERATOR_STATES_MAPPING = { SMS_OPERATOR_STATES.DELIVERED: OutputSMSMessage.STATE.DELIVERED, SMS_OPERATOR_STATES.NOT_DELIVERED: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.PHONE_NUMBER_NOT_EXISTS: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.TIMEOUTED: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.INVALID_PHONE_NUMBER: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.ANOTHER_ERROR: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.EVENT_ERROR: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.SMS_TEXT_TOO_LONG: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.PARTLY_DELIVERED: OutputSMSMessage.STATE.ERROR_UPDATE, SMS_OPERATOR_STATES.UNKNOWN: OutputSMSMessage.STATE.SENDING, SMS_OPERATOR_STATES.PARLY_DELIVERED_PARTLY_UNKNOWN: OutputSMSMessage.STATE.SENDING, SMS_OPERATOR_STATES.PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN: OutputSMSMessage.STATE.SENDING, SMS_OPERATOR_STATES.PARTLY_DELIVERED_PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN: OutputSMSMessage.STATE.SENDING, SMS_OPERATOR_STATES.NOT_FOUND: OutputSMSMessage.STATE.ERROR_UPDATE, } config = AttrDict({ 'URL': 'https://www.sms-operator.cz/webservices/webservice.aspx', 'UNIQ_PREFIX': '', 'TIMEOUT': 5, # 5s }) def _get_extra_sender_data(self): return { 'prefix': self.config.UNIQ_PREFIX, } def _serialize_messages(self, messages, request_type): """ Serialize SMS messages to the XML :param messages: list of SMS messages :param request_type: type of the request to the SMS operator :return: serialized XML message that will be sent to the SMS operator service """ return render_to_string( self.TEMPLATES['base'], { 'username': self.config.USERNAME, 'password': self.config.PASSWORD, 'prefix': str(self.config.UNIQ_PREFIX) + '-', 'template_type': self.TEMPLATES[request_type], 'messages': messages, 'type': 'SMS' if request_type == self.REQUEST_TYPES.SMS else 'SMS-Status', }) def _send_requests(self, messages, request_type, is_sending=False, **change_sms_kwargs): """ Performs the actual POST request for input messages and request type. :param messages: list of SMS messages :param request_type: type of the request :param is_sending: True if method is called after sending message :param change_sms_kwargs: extra kwargs that will be stored to the message object """ requests_xml = self._serialize_messages(messages, request_type) try: resp = generate_session(slug='pymess - SMS operator', related_objects=list(messages)).post( self.config.URL, data=requests_xml, headers={'Content-Type': 'text/xml'}, timeout=self.config.TIMEOUT) if resp.status_code != 200: raise self.SMSOperatorSendingError( 'SMS operator returned invalid response status code: {}'. format(resp.status_code)) self._update_sms_states_from_response( messages, self._parse_response_codes(resp.text), is_sending, **change_sms_kwargs) except requests.exceptions.RequestException as ex: raise self.SMSOperatorSendingError( 'SMS operator returned returned exception: {}'.format(str(ex))) def _update_sms_states_from_response(self, messages, parsed_response, is_sending=False, **change_sms_kwargs): """ Higher-level function performing serialization of SMS operator requests, parsing ATS server response and updating SMS messages state according the received response. :param messages: list of SMS messages :param parsed_response: parsed HTTP response from the SMS operator service :param is_sending: True if update is called after sending message :param change_sms_kwargs: extra kwargs that will be stored to the message object """ messages_dict = {message.pk: message for message in messages} missing_uniq = set(messages_dict.keys()) - set(parsed_response.keys()) if missing_uniq: raise self.SMSOperatorSendingError( 'SMS operator not returned SMS info with uniq: {}'.format( ', '.join(map(str, missing_uniq)))) extra_uniq = set(parsed_response.keys()) - set(messages_dict.keys()) if extra_uniq: raise self.SMSOperatorSendingError( 'SMS operator returned SMS info about unknown uniq: {}'.format( ', '.join(map(str, extra_uniq)))) for uniq, sms_operator_state in parsed_response.items(): sms = messages_dict[uniq] state = self.SMS_OPERATOR_STATES_MAPPING.get(sms_operator_state) error = (self.SMS_OPERATOR_STATES.get_label(sms_operator_state) if state == OutputSMSMessage.STATE.ERROR_UPDATE else None) if is_sending: if error: self._update_message_after_sending_error( sms, state=state, error=error, extra_sender_data={'sender_state': sms_operator_state}, **change_sms_kwargs) else: self._update_message_after_sending( sms, state=state, extra_sender_data={'sender_state': sms_operator_state}, **change_sms_kwargs) else: self._update_message( sms, state=state, error=error, extra_sender_data={'sender_state': sms_operator_state}, **change_sms_kwargs) def publish_message(self, message): try: self._send_requests([message], request_type=self.REQUEST_TYPES.SMS, is_sending=True, sent_at=timezone.now()) except self.SMSOperatorSendingError as ex: self._update_message_after_sending_error( message, state=OutputSMSMessage.STATE.ERROR, error=str(ex)) except requests.exceptions.RequestException as ex: self._update_message_after_sending_error(message, error=str(ex)) # Do not re-raise caught exception. Re-raise exception causes transaction rollback (lost of information # about exception). def publish_messages(self, messages): self._send_requests(messages, request_type=self.REQUEST_TYPES.SMS, is_sending=True, sent_at=timezone.now()) def _parse_response_codes(self, xml): """ Finds all <dataitem> tags in the given XML and returns a mapping "uniq" -> "response code" for all SMS. In case of an error, the error is logged. :param xml: XML from the SMS operator response :return: dictionary with pair {SMS uniq: response status code} """ soup = BeautifulSoup(xml, 'html.parser') return { int(item.smsid.string.lstrip(self.config.UNIQ_PREFIX + '-')): int(item.status.string) for item in soup.find_all('dataitem') } def update_sms_states(self, messages): self._send_requests(messages, request_type=self.REQUEST_TYPES.DELIVERY_REQUEST)
class EmailMessage(BaseMessage): STATE = ChoicesNumEnum( ('WAITING', _('waiting'), 1), ('SENDING', _('sending'), 2), ('SENT', _('sent'), 3), ('ERROR', _('error'), 4), ('DEBUG', _('debug'), 5), ('ERROR_RETRY', _('error retry'), 6), ) recipient = models.EmailField(verbose_name=_('recipient'), blank=False, null=False, db_index=True) template = models.ForeignKey(settings.EMAIL_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='email_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False, db_index=True) sender = models.EmailField(verbose_name=_('sender'), blank=False, null=False) sender_name = models.CharField(verbose_name=_('sender name'), blank=True, null=True, max_length=250) subject = models.TextField(verbose_name=_('subject'), blank=False, null=False) external_id = models.CharField(verbose_name=_('external ID'), blank=True, null=True, db_index=True, max_length=250) last_webhook_received_at = models.DateTimeField( verbose_name=_('last webhook received at'), null=True, blank=True, editable=False, ) info_changed_at = models.DateTimeField( verbose_name=_('info changed at'), null=True, blank=True, editable=False, ) class Meta(BaseMessage.Meta): verbose_name = _('e-mail message') verbose_name_plural = _('e-mail messages') def __str__(self): return '{}: {}'.format(self.recipient, self.subject) @property def friendly_sender(self): """ returns sender with sender name in standard address format if sender name was defined """ return '{} <{}>'.format( self.sender_name, self.sender) if self.sender_name else self.sender @property def failed(self): return self.state in {self.STATE.ERROR, self.STATE.ERROR_RETRY}
ATS_STATES = ChoicesNumEnum( # Registration ('REGISTRATION_OK', _('registration successful'), 10), ('REREGISTRATION_OK', _('re-registration successful'), 11), # SMS delivery receipts ('NOT_FOUND', _('not found'), 20), ('NOT_SENT', _('not sent yet'), 21), ('SENT', _('sent'), 22), ('DELIVERED', _('delivered'), 23), ('NOT_DELIVERED', _('not delivered'), 24), ('UNKNOWN', _('not able to determine the state'), 25), # Authentication ('AUTHENTICATION_FAILED', _('authentication failed'), 100), # Internal errors ('DB_ERROR', _('DB error'), 200), # Request states ('OK', _('SMS is OK and ready to be sent'), 0), ('UNSPECIFIED_ERROR', _('unspecified error'), 1), ('BATCH_WITH_NOT_UNIQUE_UNIQ', _('one of the requests has not unique "uniq"'), 300), ('SMS_NOT_UNIQUE_UNIQ', _('SMS has not unique "uniq"'), 310), ('SMS_NO_KW', _('SMS lacks keyword'), 320), ('KW_INVALID', _('keyword not valid'), 321), ('NO_SENDER', _('no sender specified'), 330), ('SENDER_INVALID', _('sender not valid'), 331), ('MO_PR_NOT_ALLOWED', _('MO PR SMS not allowed'), 332), ('MT_PR_NOT_ALLOWED', _('MT PR SMS not allowed'), 333), ('MT_PR_DAILY_LIMIT', _('MT PR SMS daily limit exceeded'), 334), ('MT_PR_TOTAL_LIMIT', _('MT PR SMS total limit exceeded'), 335), ('GEOGRAPHIC_NOT_ALLOWED', _('geographic number is not allowed'), 336), ('MT_SK_NOT_ALLOWED', _('MT SMS to Slovakia not allowed'), 337), ('SHORTCODES_NOT_ALLOWED', _('shortcodes not allowed'), 338), ('UNKNOWN_SENDER', _('sender is unknown'), 339), ('UNSPECIFIED_SMS_TYPE', _('type of SMS not specified'), 340), ('TOO_LONG', _('SMS too long'), 341), ('TOO_MANY_PARTS', _('too many SMS parts (max. is 10)'), 342), ('WRONG_SENDER_OR_RECEIVER', _('wrong number of sender/receiver'), 343), ('NO_RECIPIENT_OR_WRONG_FORMAT', _('recipient is missing or in wrong format'), 350), ('TEXTID_NOT_ALLOWED', _('using "textid" is not allowed'), 360), ('WRONG_TEXTID', _('"textid" is in wrong format'), 361), ('LONG_SMS_TEXTID_NOT_ALLOWED', _('long SMS with "textid" not allowed'), 362), # XML errors ('XML_MISSING', _('XML body missing'), 701), ('XML_UNREADABLE', _('XML is not readable'), 702), ('WRONG_HTTP_METHOD', _('unknown HTTP method or not HTTP POST'), 703), ('XML_INVALID', _('XML invalid'), 705), # Local states not mapped to ATS states ('LOCAL_UNKNOWN_ATS_STATE', _('ATS returned state not known to us'), -1), ('LOCAL_TO_SEND', _('to be sent to ATS'), -2), ('DEBUG', _('debug SMS'), -3), )
class AbstractDialerMessage(BaseMessage): STATE = ChoicesNumEnum( # numbers are matching predefined state values in Daktela ('WAITING', _('waiting'), -1), ('NOT_ASSIGNED', _('not assigned'), 0), ('READY', _('ready'), 1), ('RESCHEDULED_BY_DIALER', _('rescheduled by dialer'), 2), ('CALL_IN_PROGRESS', _('call in progress'), 3), ('HANGUP', _('hangup'), 4), ('DONE', _('done'), 5), ('RESCHEDULED', _('rescheduled'), 6), ('ANSWERED_COMPLETE', _('listened up complete message'), 7), ('ANSWERED_PARTIAL', _('listened up partial message'), 8), ('UNREACHABLE', _('unreachable'), 9), ('DECLINED', _('declined'), 10), ('UNANSWERED', _('unanswered'), 11), ('HANGUP_BY_DIALER', _('unanswered - hangup by dialer'), 12), ('HANGUP_BY_CUSTOMER', _('answered - hangup by customer'), 13), ('ERROR_UPDATE', _('error message update'), 66), ('DEBUG', _('debug'), 77), ('ERROR', _('error'), 88), ('ERROR_RETRY', _('error retry'), 99), ) template = models.ForeignKey(settings.DIALER_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='dialer_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False, db_index=True) is_autodialer = models.BooleanField(verbose_name=_('is autodialer'), null=False, default=True) number_of_status_check_attempts = models.PositiveIntegerField( verbose_name=_('number of status check attempts'), null=False, blank=False, default=0) content = models.TextField(verbose_name=_('content'), null=True, blank=True) class Meta(BaseMessage.Meta): abstract = True verbose_name = _('dialer message') verbose_name_plural = _('dialer messages') def clean(self): if self.is_autodialer and not self.content: raise ValidationError( _('Autodialer message must contain content.')) super().clean() def clean_recipient(self): self.recipient = normalize_phone_number(force_text(self.recipient)) @property def failed(self): return self.state in {self.STATE.ERROR, self.STATE.ERROR_RETRY}
class Activity(SmartModel): TYPE = ChoicesNumEnum( ('RUN', 'Run', 1), ('RIDE', 'Ride', 2), ('HIKE', 'Hike', 3), ('XC_SKI', 'Nordic Ski', 4), ('ROLLER_SKI', 'Roller Ski', 5), ('ALPINE_SKI', 'Alpine Ski', 6), ('SWIM', 'Swim', 7), ('WALK', 'Walk', 8), ('CANOEING', 'Canoeing', 9), ('CLIMBING', 'Rock Climbing', 10), ('ICE_SKATE', 'Ice Skate', 11), ('WORKOUT', 'Workout', 12), ('OTHER', 'Other', 13), ) name = models.CharField(verbose_name='name', max_length=255, null=False, blank=False) strava_id = models.PositiveIntegerField(verbose_name='strava ID', null=True, blank=True) distance = models.DecimalField(verbose_name='distance (m)', decimal_places=2, max_digits=9, null=True, blank=True) average_speed = models.DecimalField(verbose_name='average speed (m/s)', decimal_places=2, max_digits=7, null=True, blank=True) start = models.DateTimeField(verbose_name='start', null=False, blank=False) moving_time = models.DurationField(verbose_name='moving time', null=True, blank=True) elapsed_time = models.DurationField(verbose_name='elapsed time', null=False, blank=False) elevation_gain = models.IntegerField(verbose_name='elevation gain', null=True, blank=True) type = models.PositiveSmallIntegerField(verbose_name='type', choices=TYPE.choices, null=False, blank=False) gear = models.ManyToManyField('Gear', verbose_name='gear', related_name='activities', blank=True) kudos = models.IntegerField(verbose_name='kudos count', null=True, blank=True) achievements = models.IntegerField(verbose_name='achievements count', null=True, blank=True) comments = models.IntegerField(verbose_name='comments count', null=True, blank=True) race = models.BooleanField(verbose_name='is race', default=False) commute = models.BooleanField(verbose_name='is commute', default=False) # Strava does not enable to get related athletes, just the count. Therefore athletes has to be connected # manually with activity and the count of connected athletes and athlete count may differ athlete_count = models.PositiveSmallIntegerField( verbose_name='strava athletes count', null=True, blank=True) athletes = models.ManyToManyField('Athlete', verbose_name='athletes', related_name='activities', blank=True) tags = models.ManyToManyField('Tag', verbose_name='tags', related_name='activities', blank=True) def __str__(self): return f'{self.name} #{self.tags.all()}' # return f'{map(lambda name: '#' + name, Tag.objects.all().values_list('name', flat=True))}' class Meta: ordering = ('-start', ) verbose_name = 'activity' verbose_name_plural = 'activities'
def test_choices_num_enum_same_generated_numbers_should_raise_exception( self): with assert_raises(ValueError): ChoicesNumEnum(('A', 'label a', 3), ('B', 'label b', 2), ('C', 'label c'))
def test_choices_num_enum_invalid_num_raise_exception(self): with assert_raises(ValueError): ChoicesNumEnum(('A', 'label a', 'e'), ('B', 'label b', 2))
class Version(models.Model): """A saved version of a database model.""" TYPE = ChoicesNumEnum( ('CREATED', _('Created'), 1), ('CHANGED', _('Changed'), 2), ('DELETED', _('Deleted'), 3), ('FOLLOW', _('Follow'), 4), ) objects = VersionQuerySet.as_manager() revision = models.ForeignKey( Revision, verbose_name=_('revision'), help_text=_('The revision that contains this version.'), related_name='versions') object_id = models.TextField( verbose_name=_('object id'), help_text=_('Primary key of the model under version control.')) object_id_int = models.IntegerField( verbose_name=_('object id int'), blank=True, null=True, db_index=True, help_text= _('An indexed, integer version of the stored model\'s primary key, used for faster lookups.' ), ) content_type = models.ForeignKey( ContentType, help_text=_('Content type of the model under version control.')) # A link to the current instance, not the version stored in this Version! object = GenericForeignKey() format = models.CharField( verbose_name=_('format'), max_length=255, help_text=_('The serialization format used by this model.')) serialized_data = models.TextField( verbose_name=_('serialized data'), help_text=_('The serialized form of this version of the model.')) object_repr = models.TextField( verbose_name=_('object representation'), help_text=_('A string representation of the object.')) type = models.PositiveIntegerField(verbose_name=_('version type'), choices=TYPE.choices) @property def object_version(self): """The stored version of the model.""" data = self.serialized_data data = force_text(data.encode('utf8')) return list( serializers.deserialize(self.format, data, ignorenonexistent=True))[0] @property def flat_field_dict(self): object_version = self.object_version obj = object_version.object result = {} not_parent_fields = obj._meta.get_fields(include_parents=False) for field in obj._meta.fields: if field in not_parent_fields: result[field.name] = field.value_from_object(obj) result.update(object_version.m2m_data) return result @property def field_dict(self): """ A dictionary mapping field names to field values in this version of the model. This method will follow parent links, if present. """ if not hasattr(self, '_field_dict_cache'): object_version = self.object_version obj = object_version.object result = {} for field in obj._meta.fields: result[field.name] = field.value_from_object(obj) result.update(object_version.m2m_data) # Add parent data. for parent_class, field in obj._meta.concrete_model._meta.parents.items( ): if obj._meta.proxy and parent_class == obj._meta.concrete_model: continue content_type = ContentType.objects.get_for_model(parent_class) if field: parent_id = force_text(getattr(obj, field.attname)) else: parent_id = obj.pk try: parent_version = Version.objects.get( revision__id=self.revision_id, content_type=content_type, object_id=parent_id) except Version.DoesNotExist: # pragma: no cover pass else: result.update(parent_version.field_dict) setattr(self, '_field_dict_cache', result) return getattr(self, '_field_dict_cache') def revert(self): """Recovers the model in this version.""" self.object_version.save() @cached_property def cached_instances(self): """ Return and cache instance with its parents """ obj = self.object_version.object result = [obj] for parent_class in obj._meta.get_parent_list(): content_type = ContentType.objects.get_for_model(parent_class) parent_id = obj.pk try: parent_version = Version.objects.get( revision__id=self.revision_id, content_type=content_type, object_id=parent_id) except Version.DoesNotExist: pass else: result.append(parent_version.object_version.object) return result def reversion_editor(self): if self.revision.user: return self.revision.user.email def __getattr__(self, attr): # If child inst has attribute it only means that this attribute exists, but can be None and only set in parent if hasattr(self.cached_instances[0], attr): val = None for inst in self.cached_instances: val = getattr(inst, attr, None) if val is not None: break return val else: raise AttributeError("%r object has no attribute %r" % (self.__class__, attr)) def __str__(self): """Returns a unicode representation.""" return self.object_repr #Meta class Meta: app_label = 'reversion' verbose_name = _('data version') verbose_name_plural = _('data versions')
class AbstractDialerMessage(SmartModel): STATE = ChoicesNumEnum( ('NOT_ASSIGNED', _('not assigned'), 0), ('READY', _('ready'), 1), ('RESCHEDULED_BY_DIALER', _('rescheduled by dialer'), 2), ('CALL_IN_PROGRESS', _('call in progress'), 3), ('HANGUP', _('hangup'), 4), ('DONE', _('done'), 5), ('RESCHEDULED', _('rescheduled'), 6), ('ANSWERED_COMPLETE', _('listened up complete message'), 7), ('ANSWERED_PARTIAL', _('listened up partial message'), 8), ('UNREACHABLE', _('unreachable'), 9), ('DECLINED', _('declined'), 10), ('UNANSWERED', _('unanswered'), 11), ('ERROR', _('error'), 66), ('DEBUG', _('debug'), 77), ) sent_at = models.DateTimeField(verbose_name=_('sent at'), null=True, blank=True, editable=False) recipient = models.CharField(verbose_name=_('recipient'), null=False, blank=False, max_length=20) content = models.TextField(verbose_name=_('content'), null=False, blank=False) template_slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=True, blank=True, editable=False) template = models.ForeignKey(settings.DIALER_TEMPLATE_MODEL, verbose_name=_('template'), blank=True, null=True, on_delete=models.SET_NULL, related_name='dialer_messages') state = models.IntegerField(verbose_name=_('state'), null=False, blank=False, choices=STATE.choices, editable=False) backend = models.CharField(verbose_name=_('backend'), null=True, blank=True, editable=False, max_length=250) error = models.TextField(verbose_name=_('error'), null=True, blank=True, editable=False) extra_data = JSONField(verbose_name=_('extra data'), null=True, blank=True, editable=False) extra_sender_data = JSONField(verbose_name=_('extra sender data'), null=True, blank=True, editable=False) tag = models.SlugField(verbose_name=_('tag'), null=True, blank=True, editable=False) def clean_recipient(self): self.recipient = normalize_phone_number(force_text(self.recipient)) def clean_content(self): pass @property def failed(self): return self.state == self.STATE.ERROR def __str__(self): return self.recipient class Meta: abstract = True verbose_name = _('dialer message') verbose_name_plural = _('dialer messages') ordering = ('-created_at', )