"""Attendee Registration Models

We are using the PayPal PayFlow Link Express Checkout via the Verisign partner
using API 3. PayFlow Link is no longer offered by PayPal, which now only
offers PayFlow Pro. We are also using the old Verisign V3 interface and API.
The current PayFlow Link API is V5. This is different from the normal Express
Checkout and does not support IPN, though there is a 'Return URL' in the
management interface.

With this we create a form of hidden inputs with the total cost and some
management information. When the user clicks the 'submit' button,
they post to a verisign page, which presents a form for doing the CCard
processing. We never see the CCard information. If the process succeeds,
then PayPal posts data back to us at a set URL. Because this URL is set for
ALL CCard processing for the PSF account, we will recieve these for
the PSF Donation form on the python website. We will most likely also
get POSTs from people who are not verisign.

As part of the PayFlow Link management interface we are enabling the
following features:

* Email Reciept To Customers
* Email To Merchant Address (copy)
* Silent POST Url (set to a view for creting the PayPalAck)

This means that our django view is called once the payment is successfully
completed, and an e-mail is sent to both the customer and to the treasurer.
We use gnucash to process the merchant e-mail and do the book keeping.
The management interface is used for doing refunds and other processing.

The Silent POST Url is set to a django view for processing the acknoledgement.
Information from the form processing is passed back to us and we connect it
up to the invoice. We only get this POST if the transaction succeeds.

Optional options of note:

* Force Silent Post Confirmation

   This one is special. If this is turned on, payments will fail
   (be canceled) unless the django Silent POST view returns a status 200 OK.
   That means if the django view is broken or down, all payments will fail
   including those from the donations page. We should not turn this on.

* Return URL

   This is an interesting one. We should be able to set it to do IPN
   and set up what page the user is sent back to on completion. We should
   be able to get transaction data back via a post on this URL as well.
   Talking with PayPal support this is not the case for PayFlow Link accounts.
   Very confusing.

As part of the form with the hidden input there are pieces of required
information, and some optional information. Some of this information will be
included in the customer and merchant e-mails, some will be stored in the
PayPal management interface, and some will only be sent back via the Silent
POST. We need to fill out enough information that we can reliably connect up
all the different information stored in Django, the e-mails, and the
management interface.

================== === = == == == ==
Variable           Len R CE ME MI SP
================== === = == == == ==
**Base Information**
------------------------------------
LOGIN   (<secret>)  60 X
PARTNER (Verisign)  60 X
TYPE    ("S")        1 X
AMOUNT  (total $$)     X

**Previous System**
------------------------------------
DESCRIPTION        255    X  X  X  X
COMMENT1           255    X  X
COMMENT2           255    X  X

**Django Ack Needs**
------------------------------------
NAME                60    X  X  X  X
EMAIL               40    X  X  X  X
INVOICE              9    X  X  X  X

**Optional Helpers**
------------------------------------
CUSTID              11             X
USER1 => USER10    255             X

**Not supplied by us** *(see below)*
------------------------------------
AUTHCODE            ??          X  X
PNREF               12    X  X  X  X
RESULT              ??          X  X
RESPMSG             ??    X  X  X  X
================== === = == == == ==

:Key:
  :Len: Max length of data
  :R:   required by PayPal
  :CE:  Included in Customer Email
  :ME:  Included in Merchant Email
  :MI:  Included in Management Interface
  :SP:  Included in Silent POST Data

The old system used the DESCRIPTION COMMENT1 and COMMENT2 to encode what
was purchased by the customer, includeing T-Shirt, food, and tutorial
options for multiple registrants. the COMMENT1 and COMMENT2 fields are not
stored in the management interface which causes issues. We will be using
the DESCRIPTION field to encode some of that information but for
human readable purposes. The real data will be sotred in the Django DB in the
Invoice Model. We will use the 9 character, alpha-neumeric INVOICE field
to connect the transaction to the Invoice in the DB. As this field is stored
everywhere it will become the primary ID for a transaction. The NAME and EMAIL
fields will be used for additional verification. The CUSTID will map to the
Django User model. The USER1=>USER10 will map to the Django Registration
Model, for verification. This means a max of 10 registrants on a transaction.
This seems reasonable to me, but we might change it in the future.

The Silent POST Ack includes a bunch of other information we want to keep
track of, and some we do not. The merchant account and Bank routing numbers
and other sensitive information is returned. We do not want to store that
information. What we do want to store:

:AUTHCODE: Bank Authorization Code
:RESULT:   0 for success. Message moard mentions this returning non-0 sometimes
:PNREF:    PayPal Transaction ID (included in CE, ME, MI, and SP)
:RESPMSG:
    Response Message. This one is a hoot! From the docs

    NOTE: Be sure to look at the response message for your transaction.
          Even if your result code is 0, your response message might say that
          the transaction has failed.

WTF?!?!? we only get the Silent POST if the transaction succeeded, yet we
can get non-0 result codes, and even if we do get a 0 success code, the
transaction might have failed anyway???? And the format of the result message
is undefined, except to say that 'it varies' and 'Sometimes a colon will
apear after the initial message with a more detailed description'. Gee, thanks.

"""

from django.db import models
from django.conf import settings
from django.core.cache import cache
from django.utils import encoding
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User, Group
from pycon.restructuredtext.validators import IsValidReST
from django.template import loader, Context
from django.contrib.sites.models import Site
from django.contrib.auth.models import Group
from django.template.defaultfilters import slugify
from django.utils.encoding import force_unicode, smart_str
from django.core.urlresolvers import reverse
from pycon.core.validators import *
from django.core.validators import *
from datetime import *
from decimal import Decimal
import re
import urllib

INVID_PREFIX = 'P08'
assert(len(INVID_PREFIX) < 4,
       'Invintory ID prefix must be less than 4 characters')
assert(INVID_PREFIX.isalnum(), 'Invintory ID prefix must be alpha-neumeric')

INVID_PAT = INVID_PREFIX + u'%%.%dd' % (9-len(INVID_PREFIX))
INVID_MATCH = INVID_PREFIX + '\d{%d}' % (9-len(INVID_PREFIX))
INVIDRE = re.compile('^' + INVID_PREFIX + '\d{%d}$' % (9-len(INVID_PREFIX)))

SESSIONS = ( ('1:AM', _('Morning Session (9:00am-12:20pm)')),
             ('2:PM', _('Afternoon Session (1:20pm-4:40pm)')),
             ('3:EV', _('Evening Session (6:10pm-9:30pm)')) )

TUT_STATUS = ( ('Open',     _('Open')),
               ('Full',     _('Full')),
               ('Canceled', _('Canceled')), )

INV_STATUS = ( ('WP', _('Waiting for Manual Payment Notification')),
               ('PM', _('Manually Marked Paid')),
               ('WA', _('Waiting for Credit Card Payment Notification')),
               ('CO', _('Allow Checkout')),
               ('PA', _('Paid via Verisign')),
               ('WR', _('Awaiting Refund Notification')),
               ('CA', _('Canceled')),
               ('IV', _('Invalidated')),)

SHIRT_SIZES = ( ('S',    _('Small')),
                ('M',    _('Medium')),
                ('L',    _('Large')),
                ('XL',   _('XLarge')),
                ('XXL',  _('XXLarge')),
                ('XXXL', _('XXXLarge')) )

SHIRT_TYPES = ( ('M', _('Mens')),
                ('W', _('Womens (fitted)')))

ROOM_SIZES = ( ('Midway',       90),
               ('Kitty Hawk ',  90),
               ('Kennedy',      72),
               ('Love A',       72),
               ('Love B',       72),
               ('Haneda A+B',   36),
               ('Kai Tak',      27),
               ('La Guardia',   27),
               ('Heathrow',     18),
               ('Templehof',    18),
               ('Sydney',       18),
               ('Da Vinci',     18),
               ('Orly',         12),
               ('Dorval',       12),
             )

def safename(value):
    """
    Normalizes unicode string to a form which is ascii safe as best it can.
    This is a form of the slugify filter.
    """
    import unicodedata
    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
    value = unicode(re.sub('[^\w\s\-_]', '', value).strip())
    return re.sub('[_\s]+', '_', value)

class Tutorial(models.Model):
    """Tutorial being offered.
    """
    name    = models.SlugField(max_length=20, unique=True)
    session = models.CharField(max_length=4, choices=SESSIONS)
    title   = models.CharField(max_length=150)
    status  = models.CharField(max_length=25, choices=TUT_STATUS,
                               default='Open')
    pageurl = models.CharField(max_length=100, blank=True,
                               help_text="url to the schedule page")
    popurl  = models.CharField(max_length=100, blank=True,
                               help_text="url to the schedule pop-up")
    desc    = models.TextField(blank=True, validator_list=[
                        IsValidReST("ReStructuredTextErrors: ", True, True)])
    order   = models.IntegerField(default=0)
    min_reg = models.IntegerField(default=0)
    max_reg = models.IntegerField(default=100)

    def __unicode__(self):
        return self.title

    @property
    def active_attendees(self):
        return self.registration_set.filter(active=True).distinct()

    @property
    def pending_attendees(self):
        return self.registration_set.filter(active=False,
                                invoice__status__in=['CO', 'WA']).distinct()

    class Meta:
        ordering = ('session', 'order', 'name')

    class Admin:
        list_filter = ('session', 'status')
        list_display_links = ('name','title')
        list_display = ('session', 'order', 'name', 'title', 'status',
                        'min_reg', 'max_reg')
        search_fields = ('name', 'title', 'desc',)

class InvoiceManager(models.Manager):
    def get_invoice(self, invstr):
        if INVIDRE.match(invstr) is None:
            raise self.model.DoesNotExist
        try:
            return self.get(pk=int(invstr[len(INVID_PREFIX):], 10))
        except:
            raise self.model.DoesNotExist

class Invoice(models.Model):
    """Invoice for registration

    Multiple registrations are handled in one invoice, which represents
    a single CCard transaction via the registration form.

    The invoice text is restructured text and normally auto-generated
    from the form. The text is not 100% of the information displayed
    on the invoice page however.
    """
    objects    = InvoiceManager()
    user       = models.ForeignKey(User)
    status     = models.CharField(max_length=2,
                                  choices=INV_STATUS, default='WP')
    donation   = models.DecimalField(max_digits=6, decimal_places=2,
                    default=Decimal("0.00"))
    donation_name = models.CharField(max_length="60", blank=True)
    total_cost = models.DecimalField(max_digits=6, decimal_places=2,
                    help_text=_("keep at 0.00 to recalculate on save"))
    text       = models.TextField(validator_list=[
                        IsValidReST("ReStructuredTextErrors: ", True)])
    regenerate = models.BooleanField(verbose_name=_("regenerate invoice text"),
                    default=False, help_text=_(
                        "Select then hit 'save and continue editing' to "
                        "regenerate the invoice text from the DB data. "
                        "It can then be edited from there. "
                        "WARNING: this will overwrite existing invoice text."))
    org_notes  = models.TextField(verbose_name=_('organizer notes'),
                                  blank=True)
    created    = models.DateTimeField(auto_now_add=True)
    updated    = models.DateTimeField(auto_now=True)

    def __unicode__(self):
        return INVID_PAT % int(self.id)

    @property
    def is_paid(self):
        return self.status in ['PM', 'PA', 'WR']

    @property
    def is_waiting_for_payment(self):
        return self.status in ['WP', 'WA']

    def get_desc(self):
        # RED_FLAG: add a description generator
        # This needs to work with PayPal Description field which has a
        # restricted character set.
        desc = ''
        if self.donation > Decimal('0.00'):
            desc += 'Donation: $' + ("%.2f " % self.donation)
            desc += safename(self.donation_name)
            desc += ' || '
        desc += 'PyCon: $' + ("%.2f" % (self.total_cost - self.donation)) + ' '
        desc += unicode(self) + ' '
        desc += self.get_site_absolute_url() + ' || '
        return desc

    class Meta:
        ordering = ('id',)

    class Admin:
        list_select_related = True
        date_hierarchy = 'created'
        list_filter = ('status',)
        #list_display_links = ('__unicode__',)
        list_display = ('__unicode__', 'user', 'total_cost', 'donation',
                        'status', 'created', 'updated')
        search_fields = ('text', 'org_notes')

    @models.permalink
    def get_absolute_url(self):
        return ('reg-invoice', (), {'invid': unicode(self)})

    def get_site_absolute_url(self):
        site = Site.objects.get_current()
        return 'http://' + site.domain + self.get_absolute_url()

    def save(self):
        resave = False
        if not self.text:
            self.text = 'Auto Generating...'
            self.regenerate = True
        ## save once to save M2M relations
        super(Invoice, self).save()
        if self.total_cost == Decimal("0.00") or self.regenerate == True:
            resave = True
            self.total_cost = sum((r.cost for r in self.registrations.all()),
                                  self.donation)
        if self.regenerate:
            self.regenerate = False
            resave = True
            self.text = self.generate_invoice_rest(False)
        if resave: super(Invoice, self).save()
        if self.status.startswith('P'):
            for reg in self.registrations.filter(paid=False):
                reg.paid=True
                reg.active=True
                reg.save()
        pay_group = Group.objects.get(name='Payors')
        self.user.groups.add(pay_group)
        self.user.save()

    def get_payment_status(self):
        if self.status.startswith('P'):
            return unicode(_('Paid In Full'))
        if self.status == 'WR':
            return unicode(_('Paid, with Pending Refund or Change'))
        if self.status == 'CA':
            return unicode(_('Canceled: Refundable Portion Refunded'))
        if self.status == 'IV':
            return unicode(_('Invalidated: No payment was processed.'))
        return unicode(_('Unpaid: ')) + unicode(self.get_status_display())

    def generate_invoice_rest(self, include_status=True):
        ## we use the template system to generate the invoice text because
        ## that is the easiest way to do it! (and it's updatable without
        ## restarting the server!!)
        ## we do not reproduce exactly the registration types and full break
        ## out that the reg form has, as this could be for a hand created
        ## invoice. So the exact breakout of the reg cost and tutorials
        ## is not listed. (there are too many variables).
        ##
        ## also only a basic context is used.
        return loader.render_to_string("attendeereg/invoice.rst",
                                       {'invoice': self,
                                        'include_status': include_status,
                                        'site': Site.objects.get_current()},
                                       Context())


class PayPalAck(models.Model):
    """Verification data from PayPal.

    We may get more than one of these per invoice, and we may get some
    with no invoice, as the donation system will also cause acknowledgements.

    This stores the fields we are interested in, if they were supplied.
    Other fields are dropped, as some contain sensitive information,
    including merchant account and bank routing numbers. We only want to store
    enough information so that we can reliably tie the Ack to our invoices,
    the emails sent by paypal to the registrant, the email paypal sends to
    the treasurer, and the entry in the Verisign management interface.

    There is a boolean field so that we can enter these in ourselves for
    things like checks, phone orders, and at conference registrations.
    This way we can keep things properly connected.

    """
    invoice     = models.ForeignKey(Invoice, null=True, blank=True)
    created     = models.DateTimeField(auto_now_add=True)
    updated     = models.DateTimeField(auto_now=True)

    ## for use in the admin
    org_notes   = models.TextField(verbose_name=_('organizer notes'),
                                   blank=True)
    manual_reg  = models.BooleanField(default=True)
    recieved_ack = models.BooleanField(default=False)

    ## we want to know where the ack was from
    from_ip     = models.IPAddressField(blank=True)

    ## ACK post data
    post_fields = ['INVOICE', 'NAME', 'EMAIL', 'DESCRIPTION', 'CUSTID',
                   'AUTHCODE', 'PNREF', 'RESULT', 'RESULTMSG']
    INVOICE     = models.CharField(max_length=10,  blank=True)
    NAME        = models.CharField(max_length=61,  blank=True)
    EMAIL       = models.CharField(max_length=41,  blank=True)
    DESCRIPTION = models.CharField(max_length=256, blank=True)
    CUSTID      = models.CharField(max_length=12,  blank=True)
    AUTHCODE    = models.TextField(blank=True)
    PNREF       = models.CharField(max_length=13)
    RESULT      = models.IntegerField(default=-1)
    RESULTMSG   = models.TextField(blank=True)

    def __unicode__(self):
        invid = ''
        if self.invoice_id:
            invid = INVID_PAT % int(self.invoice_id)
        elif self.INVOICE:
            return self.INVOICE
        return ':'.join([unicode(self.id),self.PNREF, invid])

    class Meta:
        ordering = ('created','PNREF')

    class Admin:
        list_select_related = True
        list_filter = ('created', 'manual_reg', 'recieved_ack')
        list_display = ('PNREF', 'invoice', 'INVOICE', 'RESULT',
                        'NAME', 'EMAIL', 'manual_reg', 'recieved_ack',
                        'created')
        search_fields = ('INVOICE', 'PNREF', 'CUSTID', 'DESCRIPTION',
                         'RESULTMSG', 'NAME')

class Registration(models.Model):
    """Attendee Registration

    Only needs an invoice if it was entered via the form for CC processing.
    Multiple registrations can be processed on a single invoice.
    This is only tied to a user if the registration was for the person
    who filled in the form, or the user acknowledges the connection.
    The e-mail is used to verify the registration/user connection, and the
    user is prompted to verify the connection in their e-mail notification,
    and on the main registration page.

    You can create a registration in the admin, and tie it to
    a new Invoice which is to be paid. Then the user associated with
    that invoice can log in and pay said invoice. With this any amount
    can be specified for the cost owed. NOTE: it is up to the admin
    to set the computed total cost on the Invoice. It should match
    what it on the connected registrations.

    Also a user can sign up for some tutorials, and then the admin can
    later add vendor or comp reg status.

    Registrations can be created for people with no invoice and no user.
    People like Guido for instance who sould have a registration and badge,
    but do not need to do anything to get that.

    badge_name will be set to name if not supplied.
    the 'name' will be set to unicode(user) when user is set.
    e-mail is used to connect to user later on.
    """
    invoice     = models.ForeignKey(Invoice, null=True, blank=True,
                                    related_name='registrations')

    user        = models.ForeignKey(User, null=True, blank=True)
    org_notes   = models.TextField(verbose_name=_('organizer notes'),
                                   blank=True)

    created     = models.DateTimeField(auto_now_add=True)
    updated     = models.DateTimeField(auto_now=True)

    tutorials   = models.ManyToManyField(Tutorial, null=True, blank=True,
                                         validator_list=[])

    ## payment control
    corporate   = models.BooleanField(verbose_name=_('corporate registration'),
                                      default=False)
    student     = models.BooleanField(verbose_name=_('student registration'),
                                      default=False)
    early_reg   = models.BooleanField(verbose_name=_('early-bird registration'),
                                      default=False)
    door_reg    = models.BooleanField(verbose_name=_('door registration'),
                                      default=False)
    conference  = models.BooleanField(verbose_name=_('conference registration'),
                                      default=True)
    tutorial    = models.BooleanField(verbose_name=_('tutorial registration'),
                            help_text=_('Auto-Set from tutorials selection'),
                            default=False)

    group_flag_map = {'Vendors': 'vendor', 'Sponsors': 'sponsor',
                      'Session Chairs': 'session', 'Presenters': 'speaker',
                      'Keynotes': 'keynote' }
    vendor      = models.BooleanField(default=False)
    sponsor     = models.BooleanField(default=False)
    session     = models.BooleanField(default=False)
    speaker     = models.BooleanField(default=False)
    keynote     = models.BooleanField(default=False)

    cost        = models.DecimalField(max_digits=6, decimal_places=2)
    paid        = models.BooleanField(default=False)
    active      = models.BooleanField(default=False)

    ## can be changed later
    changable   = ('badge_name', 'badge_text1', 'badge_text2',
                   'shirt_size', 'shirt_type',
                   'vegetarian', 'vegan', 'kosher', 'halal',
                   'email_ok', 'listing_ok', 'org_text1', 'org_text2',
                   'extra_info')
    ## badge preview
    badge_fields = ('name', 'badge_name', 'badge_text1', 'badge_text2',
                    'vendor', 'sponsor', 'session', 'speaker', 'keynote',
                    'conference', 'tutorial', 'shirt_type', 'shirt_size', 'email')
    badge_field_map = dict([
        # mine, carls
        ('badge_name',  'full_name'),
        ('badge_text1', 'badge_line1'),
        ('badge_text2', 'badge_line2'),
        ('keynote',     'key_note'),
        ('session',     'session_chair'),])

    name        = models.CharField(max_length="60")
    email       = models.EmailField()
    badge_name  = models.CharField(max_length="60", blank=True)
    badge_text1 = models.CharField(max_length="60", blank=True)
    badge_text2 = models.CharField(max_length="60", blank=True)

    shirt_size  = models.CharField(max_length=4,
                                   choices=SHIRT_SIZES, default='L')
    shirt_type  = models.CharField(max_length=3,
                                   choices=SHIRT_TYPES, default='M')
    ## food options
    food_options = ('vegetarian', 'vegan', 'kosher', 'halal')

    ## food option labels (with correct spelling ;-)
    food_option_labels = ('vegetarian', 'vegan', 'kosher', 'halal')

    vegetarian  = models.BooleanField(default=False)
    vegan       = models.BooleanField(default=False)
    kosher      = models.BooleanField(default=False)
    halal       = models.BooleanField(default=False)

    email_ok    = models.BooleanField(verbose_name=_('ok to e-mail'),
                                      default=True)
    listing_ok  = models.BooleanField(verbose_name=_('ok to list name'),
                                      default=True)

    # These should be enabled some day (not PyCon08)

    # Hotel is used to reduce attrition charges,
    #  typically $500 per person, so even one makes it worth the trouble..
    # hotel       = models.CharField(required=False,
    #                       help_text=_('Hotell the person is staying at.') )

    # checkin is mainly for staff/speakers.
    #  it answers the question "is this person here?"
    # checkin     = models.DateTimeField(required=False)
    #                       help_text=_('When did this person show up.')

    extra_info  = models.TextField(blank=True)

    def __unicode__(self):
        return self.name + ' ' + self.email

    class Meta:
        ordering = ('created',)
    class Admin:
        list_select_related = True
        list_filter = ('paid', 'active', 'student',
                       'shirt_size', 'shirt_type', 'vegan')
        list_display = ('name', 'user', 'badge_name', 'invoice',
                        'paid', 'active', 'created', 'updated')
        search_fields = ('name', 'badge_name', 'badge_text1', 'badge_text2',
                         'extra_info', 'org_notes', 'email')

    @models.permalink
    def get_absolute_url(self):
        return ('reg-view', (), {'regid': str(self.id)})

    def get_site_absolute_url(self):
        site = Site.objects.get_current()
        return 'http://' + site.domain + self.get_absolute_url()

    @models.permalink
    def get_connect_url(self):
        return ('reg-connect', (),
                {'regid': str(self.id), 'conid': self.connect_id})

    def get_site_connect_url(self):
        site = Site.objects.get_current()
        return 'http://' + site.domain + self.get_connect_url()

    def get_sample_badge_url(self):
        rdict = {}
        for name in self.badge_fields:
            value  = getattr(self, name, None)
            if not value: continue
            name = smart_str(self.badge_field_map.get(name, name))
            rdict[name] = smart_str(value)
        badge_url= reverse('badge-preview') + u'?'
        badge_url += urllib.urlencode(rdict)
        return badge_url

    def get_bod_badge_url(self):
        rdict = {}
        rdict['bod']=True
        rdict['attendeeID']=self.id
        for name in self.badge_fields:
            value  = getattr(self, name, None)
            if not value: continue
            name = smart_str(self.badge_field_map.get(name, name))
            rdict[name] = smart_str(value)
        badge_url= reverse('badge-bod') + u'?'
        badge_url += urllib.urlencode(rdict)
        return badge_url

    def list_food_options(self):
        options = []
        for option in self.food_options:
            if getattr(self, option):
                options.append(option)
        if not len(options):
            return _(u'None')
        return u' '.join(options)

    def list_types(self):
        ts = []
        if self.corporate:  ts.append(_('corporate'))
        if self.door_reg:   ts.append(_('on-site'))
        if self.early_reg:  ts.append(_('early-bird'))
        if self.student:    ts.append(_('student'))
        if self.conference: ts.append(_('conference'))
        if self.tutorial:   ts.append(_('tutorial'))
        if self.vendor:     ts.append(_('vendor'))
        if self.sponsor:    ts.append(_('sponsor'))
        return u' '.join(unicode(x) for x in ts)

    def save(self):
        if self.user:
            self.name = unicode(self.user)
            hgs = [ g['name'] for g in self.user.groups.filter(
                name__in=self.group_flag_map.keys()).values('name') ]
            for key, val in self.group_flag_map.iteritems():
                if key in hgs: setattr(self, val, True)
            att_group = Group.objects.get(name='Attendees')
            self.user.groups.add(att_group)
            self.user.save()
        if not self.badge_name:
            self.badge_name = self.name
        super(Registration, self).save()
        if self.tutorials.count():
            self.tutorial = True
            super(Registration, self).save()


    @property
    def connect_id(self):
        id = self.id
        strid = ""
        while id:
            strid+="%.2d" %((id%10) * 9 + 3)
            id /= 10
        cid = hex(int(strid, 10))[2:]
        if len(cid)%2: cid = '0' + cid
        return cid


class ChangeRequest(models.Model):
    """Request by a registered user to change something about
    their registration which is cost or structure related.
    i.e. add a tutorial, change a tutorial, cancel a registration, etc.

    messages can be from organizers or attendees.
    all messages should be sent from the mailing list, and sent TO the
    mailing list, as well as to the from and to users. There should be a
    default to user set somehow.

    While both the invoice and reg are optional, one or the other must
    be supplied. (need to add validator for that.)
    """
    user    = models.ForeignKey(User)
    to      = models.CharField(max_length=400)
    invoice = models.ForeignKey(Invoice, blank=True, null=True,
                validator_list=[RequiredIfOtherFieldNotGiven('reg',
                    _('Required field if registration is not assigned'))])
    reg     = models.ForeignKey(Registration, blank=True, null=True,
                                   verbose_name=_('registration'),
                validator_list=[RequiredIfOtherFieldNotGiven('invoice',
                    _('Required field if invoice is not assigned'))])
    text    = models.TextField()

    handled = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        base = [unicode(self.id)]
        if self.invoice_id:
            base.append(unicode(self.invoice))
        if self.reg_id:
            base.append(unicode(self.reg))
        return u':'.join(base)

    class Admin:
        list_select_related = True
        list_display = ('__unicode__', 'invoice', 'reg',
                        'user', 'to', 'created')
        search_fields = ('text',)

    def save(self):
        if self.reg and not self.invoice:
            self.invoice = self.reg.invoice ## might also be None
        return super(ChangeRequest, self).save()


def split_name(name):
    """
    Return (last, first) for accountless registrants.
    Not perfect, but better than nothing.
    """
    s = name.split()
    if len(s) == 2:
        return (s[1], s[0])
    if len(s) < 2:
        return (name, u'')
    if u'.' in s[0] and len(s[0]) > 2:
        # "Mr.", "Ms.", etc.
        s.pop(0)
    if u'.' in s[-1] and len(s[-1]) > 2:
        # "Esq." etc?
        s.pop()
    if len(s) < 2:
        return (s[0], u'')
    return (s[-1], s[0])
