Exemple #1
0
class AddressType(Model):
    '''
    The different types of addresses. You won't need to change these.
    The initial data fixture makes sure you get BILLING, DELIVERY,
    POSTAL, VISIT and OTHER.

    The Relation model uses these types to get the billing_address and
    so forth from the available addresses.
    '''
    identifier = SafeCharField(
        _('identifier'),
        max_length=16,
        help_text=_('An identifier for machine lookups: e.g. "BILLING".'))
    description = SafeCharField(
        _('description'),
        max_length=63,
        help_text=_('A descriptive name: e.g. "Postal address".'))

    def __repr__(self):
        return 'AddressType::%s' % self.identifier

    def __unicode__(self):
        return self.description

    class Meta:
        """Django metaclass information."""
        verbose_name = _('address type')
        verbose_name_plural = _('address types')
Exemple #2
0
class Country(Model):
    '''
    The country. These are filled in by a fixture. No need to touch them.
    '''
    code = SafeCharField(_('code'),
                         max_length=2,
                         primary_key=True,
                         help_text=_('The ISO 3166 alpha2 code in lowercase.'))
    name = SafeCharField(_('name'),
                         max_length=63,
                         help_text=_('The country name.'))
    order = models.PositiveIntegerField(
        _('order'),
        default=0,
        help_text=_(
            'A non-zero number orders the countries highest first in select boxes '
            '(use this for commonly used countries).'))

    def __unicode__(self):
        return self.name

    class Meta:
        """Django metaclass information."""
        ordering = ('-order', 'name')
        verbose_name = _('country')
        verbose_name_plural = _('countries')
Exemple #3
0
class Address(Model):
    '''
    Every relation can have zero or more addresses. Most often, the
    relation will have only one address, with the types BILLING,
    DELIVERY, POSTAL and VISIT set. Using the address_type many2many
    field, you don't need to keep four addresses in sync.
    '''
    relation = models.ForeignKey(
        Relation,
        verbose_name=_('relation'),
        help_text=_('The relation this address belongs to.'))
    address_type = models.ManyToManyField(
        AddressType,
        verbose_name=_('address type'),
        help_text=_('Select one or more types of addresses.'))

    number = models.PositiveIntegerField(
        _('number'),
        help_text=
        _('The house number must be an integer, see the next field for extensions.'
          ))
    complement = SafeCharField(_('complement'),
                               max_length=32,
                               blank=True,
                               help_text=_('Optional house number suffixes.'))
    street = SafeCharField(_('street'),
                           max_length=127,
                           help_text=_('The street name, without number.'))
    city = models.ForeignKey(City,
                             verbose_name=_('city'),
                             help_text=_('The city.'))
    zipcode = SafeCharField(_('zip code'),
                            max_length=16,
                            help_text=_('Zip/postal code.'))

    contact = models.ForeignKey(Contact,
                                blank=True,
                                null=True,
                                help_text=_('For the attention of'))

    def __unicode__(self):
        return _(u'%(relation)s addresses: %(address_types)s') % {
            'relation':
            self.relation,
            'address_types':
            ', '.join(unicode(addr) for addr in self.address_type.all()),
        }

    class Meta:
        """Django metaclass information."""
        ordering = ('relation__name', )
        permissions = (('view_address', 'Can view address'), )
        verbose_name = _('address')
        verbose_name_plural = _('addresses')
Exemple #4
0
class Payout(Model):
    '''
    Payout contains the relation between the telecom operator, the
    shortcode, the MO/MT (Mobile Originated/Terminated) text message
    tariff and the payout to this business (which can be significantly
    lower than the tariff).
    '''
    operator = models.ForeignKey(Operator, help_text=_('The GSM operator.'))
    local_address = SafeCharField(
        max_length=32,
        blank=True,
        help_text=_(
            'Local phone number, e.g. a shortcode. Leave empty to match all.'))
    tariff_cent = models.PositiveIntegerField(
        blank=True,
        null=True,
        help_text=_(
            'The MT SMS tariff in cents. Leave NULL to set the MO payout. '
            '(Watch out for dupes. The unique constraint will not work with NULL values.)'
        ))
    payout_cent = DecimalField(help_text=_(
        'The Payout (in cents!) by the GSM operator for this tariff.'))

    class Meta:
        unique_together = ('operator', 'local_address', 'tariff_cent')
Exemple #5
0
class TextMessageExtra(models.Model):
    '''
    Model that contains SMS-provider specific information. The meta
    thing was a failed idea.

    Make sure you use a transactional database when using this for SMS
    sending. If you don't and you use a separate thread/process to
    empty your SMS queue, you might end up with messages sent for free
    because this object didn't exist yet when polling for new messages.
    '''
    textmessage = models.OneToOneField(
        TextMessage,
        related_name='extra',
        help_text=_('The textmessage that the extra info is about.'))
    shortcode = models.PositiveIntegerField(
        blank=True,
        null=True,
        help_text=_(
            'Shortcode that this message was received on, or is sent from.'))
    keyword = SafeCharField(
        max_length=63,
        blank=True,
        null=True,
        help_text=_(
            'Keyword that this message was received for, or is sent from.'))
    tariff_cent = models.PositiveIntegerField(
        default=0, help_text=_('Consumer price (MT) for sent SMS.'))
    foreign_reference = SafeCharField(
        max_length=31,
        blank=True,
        db_index=True,
        help_text=_('Foreign reference (e.g. mid for Mollie).'))
    foreign_status = SafeCharField(
        max_length=31,
        blank=True,
        default='',
        help_text=_('Same as status, but SMS-provider specific.'))

    @property
    def tariff(self):
        return float(self.tariff_cent) / 100

    def __str__(self):
        return 'SMS Info #%d @ %s %s \u00a4 %d (%s, %s)' % (
            self.textmessage_id, self.shortcode, self.keyword,
            self.tariff_cent, self.foreign_reference, self.foreign_status
            or '...')
Exemple #6
0
class DeliveryReportForwardLog(models.Model):
    datetime = models.DateTimeField(
        auto_now_add=True,
        help_text=_('The datetime the response was forwarded.'))
    post_data = SafeCharField(max_length=512, help_text=_('The POST data.'))
    destination = SafeCharField(
        max_length=200,
        help_text=_('The destination the delivery report was forwarded to.'))
    batch_prefix = SafeCharField(
        max_length=64, help_text=_('The batch prefix that was matched.'))
    response = SafeCharField(
        max_length=512,
        blank=True,
        help_text=_('The response from the DLR destination.'))

    def __unicode__(self):
        return '%s: %s => %s' % (self.datetime, self.batch_prefix,
                                 self.destination)
Exemple #7
0
class DeliveryReportForward(models.Model):
    batch_prefix = SafeCharField(
        max_length=64,
        unique=True,
        help_text=_('The batch prefix to match delivery reports.'))
    destination = models.URLField(
        help_text=_('The URL to forward the delivery report to.'))

    def __unicode__(self):
        return '%s => %s' % (self.batch_prefix, self.destination)
Exemple #8
0
class PhoneNumber(Model):
    '''
    Phone usage and thereby assumptions based on phone numbers and phone
    number types are blurring. Therefore this PhoneNumber lacks a
    PhoneNumberType field. Instead, you get to pour your heart out in
    the freeform comment field. ("Work", "Home", "Mobile", "Only on
    sundays between 12:05 and 13:05" are all fine ;-) One special case
    could be "Fax" which you might want to pick automatically.)

    Because the comments and the activity-flag may differ, the phone
    numbers are not unique+m2m, but non-unique+one2one.
    '''
    relation = models.ForeignKey(
        Relation,
        verbose_name=_('relation'),
        related_name='phonenumber_set',
        help_text=_('The relation this phone number belongs to.'))
    number = PhoneNumberField(_('number'),
                              help_text=_('The telephone number.'))
    active = models.BooleanField(
        _('active'),
        blank=True,
        default=True,
        help_text=_('Whether one should use this number.'))
    comment = SafeCharField(
        _('comment'),
        max_length=63,
        blank=True,
        help_text=_('Optional comments about the number\'s use (or "Fax").'))

    def __unicode__(self):
        return _(
            u'%(relation)s phone number: %(number)s%(comment)s%(active)s') % {
                'relation': self.relation,
                'number': self.number,
                'comment': (u'', u' (%s)' % self.comment)[self.comment != ''],
                'active': (_(' INACTIVE'), u'')[self.active],
            }

    class Meta:
        """Django metaclass information."""
        ordering = ('relation__name', )
        verbose_name = _('phone number')
        verbose_name_plural = _('phone numbers')
Exemple #9
0
class Contact(Model):
    '''
    A contact (a person) tied to a Relation.
    '''
    # XXX: 'name' should really sync itself with the user.forename /
    # user.surname if it is an authenticatable contact (and.. should we
    # split this name into a fore-/surname?)
    relation = models.ForeignKey(Relation, verbose_name=_('relation'))
    name = SafeCharField(_('name'),
                         max_length=63,
                         help_text=_('The full name of the contact.'))
    email = models.EmailField(_('e-mail address'), blank=True)

    def __unicode__(self):
        return self.name

    class Meta:
        """Django metaclass information."""
        permissions = (('view_contact', 'Can view contact'), )
        verbose_name = _('contact')
        verbose_name_plural = _('contacts')
Exemple #10
0
class Operator(Model):
    '''
    A GSM operator.

    E.g. KPN Telecom (204-08) in the Netherlands.
    '''
    if Country:
        country = models.ForeignKey(
            Country,
            help_text=_('The country (found through the first part '
                        'of the code).'))
    else:
        country = models.CharField(max_length=2,
                                   help_text=_('Two letter country code.'))
    code = models.PositiveIntegerField(
        help_text=_('The GSM operator code, e.g. 8 for 204-08 KPN Telecom.'))
    name = SafeCharField(max_length=64,
                         blank=True,
                         help_text=_('A friendly name, e.g. "KPN Telecom".'))

    objects = OperatorManager()

    def entire_code(self, separator='-'):
        return '%3d%s%02d' % (OperatorCountryCode.objects.get(
            country=self.country).code, separator, self.code)

    if Country:

        def __unicode__(self):
            return u'%s %s (%s)' % (self.entire_code(), self.name,
                                    self.country.code)
    else:

        def __unicode__(self):
            return u'%s %s (%s)' % (self.entire_code(), self.name,
                                    self.country)

    class Meta:
        unique_together = ('code', 'country')
Exemple #11
0
class City(Model):
    '''
    The city. You may need to add lots of these.
    '''
    country = models.ForeignKey(
        Country,
        verbose_name=_('country'),
        help_text=_('Select the country the city lies in.'))
    name = SafeCharField(_('name'),
                         max_length=63,
                         help_text=_('The city name.'))

    def __unicode__(self):
        return (_(u'%(city)s (%(countrycode)s)') % {
            'city': self.name,
            'countrycode': self.country.code
        })

    class Meta:
        """Django metaclass information."""
        ordering = ('name', )
        verbose_name = _('city')
        verbose_name_plural = _('cities')
Exemple #12
0
class Item(Model):
    '''
    Generic model to dynamically store various additional database fields.

    The TEXT datatype strips excess spaces from the value at save time.

    The datatype used to be a separate model, but (a) no other data types have
    been defined since the inception of this app and (b) MongoDB does not like
    the fact that it used integers as primary keys. At the time, the best fix
    was to replace the 'datatype' foreign key with a 'datatype_id' integer.
    This maintains backwards compatibility and fixes so stuff just works on
    Mongo. As an added bonus, the save() is now quicker, because it doesn't
    have to look up the PK for TEXT any more.
    '''
    # One of DATATYPE_TEXT, ...
    datatype_id = models.IntegerField(verbose_name=_('data type'), default=1)
    # Key can be something like "asterisk.management.username"
    key = SafeCharField(_('key'), max_length=63, primary_key=True)
    # The value, stored in a text field. Other data types may at one point fix
    # type casting when using the aboutconfig() utility function.
    value = models.TextField(_('value'), blank=True)

    def save(self, *args, **kwargs):
        if self.datatype_id == DATATYPE_TEXT:
            self.value = self.value.strip()
        super(Item, self).save(*args, **kwargs)

    def __unicode__(self):
        return self.key

    if not py2:
        __str__ = __unicode__  # py3

    class Meta:
        ordering = ('key', )
        verbose_name = _('advanced/hidden configuration item')
        verbose_name_plural = _('advanced/hidden configuration items')
Exemple #13
0
class TextMessage(models.Model):
    '''
    A text message of the short message service (SMS) kind.

    If you need to store more information, use a one2one mapping that
    stores e.g. whether you're done with the message or who owns the
    message an so on.
    '''
    created = models.DateTimeField(
        auto_now_add=True,
        db_index=True,
        help_text=_('When the text message was created in this system.'))
    modified = models.DateTimeField(
        auto_now=True, help_text=_('When the text message was last modified.'))
    status = models.CharField(
        choices=STATUS_CHOICES,
        max_length=max(len(i[0]) for i in STATUS_CHOICES),
        help_text=_('The status of the message (includes direction).'))
    local_address = SafeCharField(
        max_length=32,
        blank=True,
        help_text=_('Local phone number. This does not necessarily '
                    'need to be a phone number.'))
    remote_address = PhoneNumberField(
        help_text=_('The phone number of the remote end: the originator '
                    'on inbound and the recipient on outbound '
                    '(with country code, without leading zeroes).'))
    remote_operator = models.ForeignKey(
        Operator,
        blank=True,
        null=True,
        help_text=_('Optionally the GSM operator of the remote end.'))
    body = models.TextField(
        help_text=_('The message body. In case of a simple messaging '
                    'server, this should be at most 160 characters long.'))
    body_count = models.PositiveIntegerField(
        default=1,
        help_text=_('How many messages this message is composed of.')
    )  # could be important for reply counts or billing purposes
    delivery_date = models.DateTimeField(
        blank=True,
        null=True,
        db_index=True,
        help_text=_('The delivery date. On an outbound message, this '
                    'should be set first on acknowledgement of receipt.'))
    metadata = models.TextField(
        blank=True,
        help_text=_('Optional metadata as a pickled python object. By '
                    'convention this is either None or a list of '
                    'dictionaries.'))

    objects = TextMessageManager()

    def __init__(self, *args, **kwargs):
        self.connection = kwargs.pop('connection', None)
        super(TextMessage, self).__init__(*args, **kwargs)

    def _meta_read(self):
        if self.metadata == '':
            return None
        # Python pickle routines using protocol 0 use both low and high
        # bytestring characters (<0x20 and >0x7f). We could do
        #   return pickle.loads(self.metadata.encode('latin1'))
        # here and
        #   self.metadata = pickle.dumps(value).decode('latin1')
        # in _meta_write. (latin1 is just an example, any encoding that
        # uses the all 8bit characters is fine.) But then we still get
        # the low ascii which is fragile in browser textboxes.
        #
        # See python issue http://bugs.python.org/issue2980 for the
        # >0x7f part of the issue.
        return pickle.loadascii(self.metadata)

    def _meta_write(self, value):
        if value in (None, ''):
            self.metadata = ''
        else:
            self.metadata = pickle.dumpascii(value)

    def _meta_delete(self):
        self.metadata = ''

    meta = property(_meta_read, _meta_write, _meta_delete,
                    'Get or set freeform metadata.')

    def meta_append(self, dict_or_none, commit=True):
        '''
        Append a dictionary to the metadata. If dict_or_none is None,
        nothing is appended. Note that the object is saved anyway if
        commit is True.
        '''
        if dict_or_none is not None:
            meta = self.meta
            if meta is None:
                meta = [dict_or_none]
            else:
                assert isinstance(
                    meta, list
                ), 'Expected meta to be a list of dictionaries, not %r.' % meta
                meta.append(dict_or_none)
            self.meta = meta
        if commit:
            self.save()

    def create_reply(self, body):
        return TextMessage.objects.create(
            status='out',
            local_address=self.local_address.split(
                ' ', 1)[0],  # use only shortcode, not any keywords
            remote_address=self.remote_address,
            remote_operator=self.remote_operator,
            body=body)

    def get_connection(self, fail_silently=False):
        from osso.sms import get_connection
        if not self.connection:
            self.connection = get_connection(fail_silently=fail_silently)
        return self.connection

    def get_keywords(self, max_keywords, mode='loose'):
        '''
        Get the N keywords starting at the shortcode, moving on to the
        words in the SMS body. Yes, get_keywords(1)[0] is the shortcode!

        Observe that we ignore the keywords supplied by the provider. If
        you want to use that, you can get it from the local_address, but
        in most cases these keywords are still in the message body.

        This is the preferred way to match messages by keyword, as
        upstream providers may or may not know about your particular
        keyword, so checking the local_address will probably not be
        enough.

        Keywords returned from here are guaranteed to be in [0-9A-Z].

        The mode flag can be one of 'loose', 'normal', 'strict'. They
        separate the keywords by '\\W+', '\\s+' and ' ' respectively.

        >>> from osso.sms.models import TextMessage
        >>> t = TextMessage(local_address='1008 X', body="i -wouldn't  t99")
        >>> t.get_keywords(0)
        []
        >>> t.get_keywords(1)
        ['1008']
        >>> t.get_keywords(2)
        ['1008', 'I']
        >>> t.get_keywords(3)
        ['1008', 'I', 'WOULDN']
        >>> t.get_keywords(4)
        ['1008', 'I', 'WOULDN', 'T']
        >>> t.get_keywords(5)
        ['1008', 'I', 'WOULDN', 'T', 'T99']
        >>> t.get_keywords(6)
        ['1008', 'I', 'WOULDN', 'T', 'T99']

        >>> t = TextMessage(local_address='1008', body=" i    -wouldn't  t99")
        >>> t.get_keywords(3, mode='loose')
        ['1008', 'I', 'WOULDN']
        >>> t.get_keywords(3, mode='normal')
        ['1008', 'I', "-WOULDN'T"]
        >>> t.get_keywords(3, mode='strict')
        ['1008', '', 'I']

        >>> t = TextMessage(local_address='1008', body="\\n\\r\\t\\v pleenty\\tspace\\n\\r\\t\\v ")
        >>> t.get_keywords(4, mode='loose')
        ['1008', 'PLEENTY', 'SPACE']
        >>> t.get_keywords(4, mode='normal')
        ['1008', 'PLEENTY', 'SPACE']

        >>> t = TextMessage(local_address='1008 STOP', body="stop")
        >>> t.get_keywords(3)
        ['1008', 'STOP']
        >>> t = TextMessage(local_address='1008', body="sToP!!.@")
        >>> t.get_keywords(3)
        ['1008', 'STOP']

        Test the non-loose (sloppy) modes.

        >>> t = TextMessage(local_address='1008 X', body="i -wouldn't  t99 ")
        >>> t.get_keywords(0, mode='normal'), t.get_keywords(0, mode='strict')
        ([], [])
        >>> t.get_keywords(1, mode='normal'), t.get_keywords(1, mode='strict')
        (['1008'], ['1008'])
        >>> t.get_keywords(3, mode='normal'), t.get_keywords(3, mode='strict')
        (['1008', 'I', "-WOULDN'T"], ['1008', 'I', "-WOULDN'T"])
        >>> t.get_keywords(4, mode='normal')
        ['1008', 'I', "-WOULDN'T", 'T99']
        >>> t.get_keywords(4, mode='strict')
        ['1008', 'I', "-WOULDN'T", '']
        >>> t.get_keywords(9, mode='normal')
        ['1008', 'I', "-WOULDN'T", 'T99']
        >>> t.get_keywords(9, mode='strict')
        ['1008', 'I', "-WOULDN'T", '', 'T99', '']

        >>> t = TextMessage(local_address='1008', body="sToP!!.@")
        >>> t.get_keywords(3, mode='normal'), t.get_keywords(3, mode='strict')
        (['1008', 'STOP!!.@'], ['1008', 'STOP!!.@'])
        '''
        assert mode in ('loose', 'normal', 'strict')
        assert max_keywords >= 0

        if max_keywords == 0:
            return []

        shortcode = self.local_address.split(' ')[0]
        if max_keywords == 1:
            return [shortcode]
        if mode == 'strict':
            tmp = self.body.split(' ', max_keywords - 1)[0:(max_keywords - 1)]
            return [shortcode] + [i.upper() for i in tmp]
        split_re = (NON_ALNUM_RE, BLANK_RE)[mode == 'normal']
        return (
            [shortcode] +
            [i.upper()
             for i in split_re.split(self.body) if i != ''])[0:max_keywords]

    @property
    def is_inbound(self):
        return not self.is_outbound

    @property
    def is_outbound(self):
        return self.status not in ('in', 'rd')

    def send(self,
             reply_to=None,
             shortcode_keyword=None,
             tariff_cent=None,
             fail_silently=False):
        '''
        Send out the message. Don't call this unless you're prepared to
        wait a while and prepared to handle errors. See the sms
        management command for a sample sms sending cron job.
        '''
        # XXX: hope that we can obsolete reply_to, shortcode_keyword,
        # tariff_cent and have everyone use TextMessageExtra for that
        # purpose.
        assert self.pk is not None, 'Attempting to a send a message without a primary key.'
        assert self.status == 'out', 'Attempting to a send a message that is not in state outbound-unsent.'
        if not self.remote_address:
            if fail_silently:
                return 0
            else:
                raise DestinationError('Empty remote address')
        return self.get_connection(fail_silently).send_messages(
            [self],
            reply_to=reply_to,
            shortcode_keyword=shortcode_keyword,
            tariff_cent=tariff_cent)

    def __repr__(self):
        return 'TextMessage(id=%d)' % self.id

    def __str__(self):
        if self.status in ('in', 'rd'):
            return 'Inbound SMS from %s at %s' % (
                self.remote_address, self.created.strftime('%Y-%m-%d %H:%M'))
        else:
            return 'Outbound SMS to %s at %s' % (
                self.remote_address, self.created.strftime('%Y-%m-%d %H:%M'))

    class Meta:
        ordering = ('-id', 'remote_address')
        permissions = (('view_textmessagestatus',
                        'Can view textmessage status'), )
Exemple #14
0
class Relation(Model):
    '''
    A relation model: this can be a customer company, a business
    partner, a private person and even yourself. The owner is an
    optional parent to allow for recursive relationships. The name is
    freeform. The code is an optional code you use to identify your
    relation by. The foreign code is an optional code the relation uses
    to identify you by.

    >>> from osso.relation.models import Relation, Country, City, Address, AddressType

    >>> osso, osso_is_new = Relation.objects.get_or_create(
    ...     owner=None, name=u'OSSO B.V.', code=u'', foreign_code=u'')
    >>> gntel, gntel_is_new = Relation.objects.get_or_create(
    ...     owner=osso, name=u'gnTel B.V.', code=u'006', foreign_code=u'040055')
    >>> nl, is_new = Country.objects.get_or_create(code='nl', defaults={'name': 'NL'})
    >>> groningen, groningen_is_new = City.objects.get_or_create(
    ...     country=nl, name=u'Groningen')
    >>> mediacentrale, is_new = Address.objects.get_or_create(
    ...   relation=gntel,
    ...   number=294,
    ...   complement=u'A',
    ...   street=u'Helperpark',
    ...   city=groningen
    ... )
    >>> at, is_new = AddressType.objects.get_or_create(identifier='POSTAL')
    >>> mediacentrale.address_type.add(*AddressType.objects.exclude(identifier='OTHER'))
    >>> gntel.postal_address.street
    u'Helperpark'
    >>> int(gntel.postal_address.number)
    294

    Assert that nameless relations get their pk as name.

    >>> nameless = Relation.objects.create(owner=gntel, code=u'123')
    >>> nameless.name == u'relation%d' % nameless.id
    True
    >>> nameless.code
    u'123'

    Clean up after ourself.

    >>> nameless.delete()
    >>> if gntel_is_new: gntel.delete()
    >>> if osso_is_new: osso.delete()
    >>> if groningen_is_new: groningen.delete()
    '''
    owner = ParentField(_('owner'),
                        related_name='owned_set',
                        help_text=_(
                            'This allows for reseller-style relationships. '
                            'Set to NULL for the system owner.'))
    name = SafeCharField(_('name'),
                         max_length=63,
                         help_text=_(
                             'The relation name: a company name or a person '
                             'name in case of a private person.'))
    code = SafeCharField(
        _('code'),
        max_length=16,
        blank=True,
        help_text=
        _('A human readable short relation identifier; should be unique per owner.'
          ))
    foreign_code = SafeCharField(
        _('foreign code'),
        max_length=16,
        blank=True,
        help_text=
        _('A human readable identifier that the relation uses to identify you by.'
          ))

    objects = RelationManager()

    @property
    def billing_address(self):
        """Return the billing address for this relation."""
        return car(
            self.address_set.filter(
                address_type__identifier='BILLING').order_by('created'))

    @property
    def delivery_address(self):
        """Return the delivery address for this relation."""
        return car(
            self.address_set.filter(
                address_type__identifier='DELIVERY').order_by('created'))

    @property
    def postal_address(self):
        """Return the postal address for this relation."""
        return car(
            self.address_set.filter(
                address_type__identifier='POSTAL').order_by('created'))

    @property
    def visiting_address(self):
        """Return the visiting address for this relation."""
        return car(
            self.address_set.filter(
                address_type__identifier='VISITING').order_by('created'))

    def is_descendant_of(self, relation, include=False):
        ''' Whether this is a child or grandchild of relation. Currently
        does N queries for N levels of grandparents. Beware! '''
        parent = (self.owner, self)[include]
        while parent and parent != relation:
            parent = parent.owner
        return parent is not None

    def save(self, *args, **kwargs):
        """Save the model to the database."""
        ParentField.check(self, 'owner')
        super(Relation, self).save(*args, **kwargs)
        if self.name == '':
            self.name = 'relation%d' % self.id
            super(Relation, self).save(force_update=True)

    def __unicode__(self):
        if self.code == '':
            return self.name
        return u'%s - %s' % (self.code, self.name)

    class Meta:
        """Django metaclass information."""
        ordering = ('name', )
        permissions = (('view_relation', 'Can view relation'), )
        verbose_name = _('relation')
        verbose_name_plural = _('relations')
Exemple #15
0
class Channel(Model):
    '''
    A channel that holds a set of chat messages and binds them to a
    particular relation (company).
    '''
    relation = models.ForeignKey(
        Relation,
        help_text=_('The relation whose authenticated contacts can read/'
                    'write to this channel.'))
    name = SafeCharField(
        max_length=32,
        help_text=_('The name of the channel, e.g. "Operator chat".'))
    groups = models.ManyToManyField(
        Group,
        help_text=_('Users must be a member of one of these groups to '
                    'read/write to this channel.'))
    max_age = models.PositiveIntegerField(
        default=86400,
        help_text=_('The max age of the messages (in seconds) that are '
                    'kept for this channel. Set to 0 for eternity.'))
    max_count = models.PositiveIntegerField(
        default=2000,
        help_text=_('The max amount of messages that are kept for this '
                    'channel. Set to 0 for unlimited.'))

    def create_message(self, body, sender=None):
        return Message.objects.create(channel=self, sender=sender, body=body)

    def prune(self):
        '''
        Make sure the channel gets cleared of old messages.
        '''
        if self.max_age != 0:
            old = datetime.now() - timedelta(seconds=self.max_age)
            self.messages.filter(timestamp__lt=old).delete()
        if self.max_count != 0:
            count = self.messages.count()
            if count > self.max_count:
                # Don't delete all (count - max_count) for two reasons:
                # (1) If we're in a race condition, it's possible we'll
                #     delete way too many records.
                # (2) The queries might take too long.
                # (note, these two cases only happen when someone has
                # reduced the max_count value recently)
                limit = min(100, count - self.max_count)
                # Wrap the queryset in a list because of django-1.1 bug #12328
                # http://code.djangoproject.com/ticket/12328
                qs = Message.objects.order_by('timestamp')[0:limit]
                message_ids = list(qs.values_list('id', flat=True))
                Message.objects.filter(id__in=message_ids).delete()

    def get_absolute_url(self):
        return reverse('userchat_channel', kwargs={'channel_id': self.id})

    def __unicode__(self):
        return repr(self)

    def __repr__(self):
        return (u'Channel(name=%s, relation_id=%d)' %
                (repr(self.name), self.relation.id))

    class Meta:
        ordering = ('id', )
        unique_together = ('relation', 'name')