예제 #1
0
 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)
예제 #2
0
 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)
예제 #3
0
 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')
예제 #4
0
 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)
예제 #5
0
 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')
예제 #6
0
 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')
예제 #7
0
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}
예제 #8
0
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
예제 #9
0
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'
예제 #10
0
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',)
예제 #11
0
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')
예제 #12
0
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', )
예제 #13
0
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}
예제 #14
0
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',)
예제 #15
0
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')
예제 #16
0
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',)
예제 #17
0
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)
예제 #18
0
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)
예제 #19
0
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)
예제 #20
0
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),
)
예제 #22
0
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}
예제 #23
0
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'
예제 #24
0
 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'))
예제 #25
0
 def test_choices_num_enum_invalid_num_raise_exception(self):
     with assert_raises(ValueError):
         ChoicesNumEnum(('A', 'label a', 'e'), ('B', 'label b', 2))
예제 #26
0
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')
예제 #27
0
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', )