Ejemplo n.º 1
0
 def test_scrambler(self):
     for offset in [0, 1, 10, 100, 1000, 10000, (2**16 - 1)]:
         scrambler = Scrambler(offset)
         outputs = set()
         for i in range(2**16):
             o = scrambler.forward(i)
             outputs.add(o)
             self.assertEqual(scrambler.backward(o), i)
         self.assertEqual(len(outputs), 2**16)
Ejemplo n.º 2
0
class Nomination(models.Model):

    nominee = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='nomination', on_delete=models.CASCADE)
    statement = models.TextField(validators=[validate_max_300_words])

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(5000)

    class Manager(models.Manager):
        def get_by_nomination_id_or_404(self, nomination_id):
            id = self.model.id_scrambler.backward(nomination_id)
            return get_object_or_404(self.model, pk=id)

    objects = Manager()

    @property
    def nomination_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('ukpa:nomination', args=[self.nomination_id])
Ejemplo n.º 3
0
class Ticket(models.Model):
    order = models.ForeignKey(Order,
                              related_name='tickets',
                              on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    date_of_birth = models.DateField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(7000)

    class Manager(models.Manager):
        def get_by_ticket_id_or_404(self, ticket_id):
            id = self.model.id_scrambler.backward(ticket_id)
            return get_object_or_404(self.model, pk=id)

    objects = Manager()

    def __str__(self):
        return self.ticket_id

    @property
    def ticket_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def details(self):
        return {
            'name': self.name,
            'date_of_birth': self.date_of_birth,
        }
Ejemplo n.º 4
0
class Badge(models.Model):

    ticket = models.ForeignKey(Ticket,
                               related_name='badge',
                               on_delete=models.CASCADE,
                               null=True)
    collected = models.DateTimeField(null=True)
    name = models.CharField(max_length=200, null=True, blank=True)

    id_scrambler = Scrambler(7000)

    class Manager(models.Manager):
        def get_by_badge_id_or_404(self, badge_id):
            id = self.model.id_scrambler.backward(badge_id)
            return get_object_or_404(self.model, pk=id)

    objects = Manager()

    @property
    def badge_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)
Ejemplo n.º 5
0
class Application(models.Model):
    applicant = models.OneToOneField(settings.AUTH_USER_MODEL,
                                     related_name='grant_application',
                                     on_delete=models.CASCADE)
    amount_requested = models.IntegerField()
    would_like_ticket_set_aside = models.BooleanField()
    thu = models.BooleanField()
    fri = models.BooleanField()
    sat = models.BooleanField()
    sun = models.BooleanField()
    mon = models.BooleanField()
    about_you = models.TextField()
    amount_offered = models.IntegerField(default=0)
    requested_ticket_only = models.BooleanField(default=False)
    special_reply_required = models.BooleanField(default=False)

    id_scrambler = Scrambler(4000)

    class Manager(models.Manager):
        def get_by_application_id_or_404(self, application_id):
            id = self.model.id_scrambler.backward(application_id)
            return get_object_or_404(self.model, pk=id)

    objects = Manager()

    @property
    def application_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('grants:application', args=[self.application_id])

    def days(self):
        return [DAYS[day] for day in DAYS if getattr(self, day)]
Ejemplo n.º 6
0
class Ticket(models.Model):
    owner = models.OneToOneField(settings.AUTH_USER_MODEL,
                                 null=True,
                                 on_delete=models.CASCADE)
    rate = models.CharField(max_length=40)
    sat = models.BooleanField()
    sun = models.BooleanField()
    mon = models.BooleanField()
    tue = models.BooleanField()
    wed = models.BooleanField()
    order_rows = GenericRelation('orders.OrderRow')
    free_reason = models.CharField(max_length=100, null=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(2000)

    class Meta:
        permissions = [
            ('create_free_ticket', 'Can create free tickets'),
        ]

    class Manager(models.Manager):
        def get_by_ticket_id_or_404(self, ticket_id):
            id = self.model.id_scrambler.backward(ticket_id)
            return get_object_or_404(self.model, pk=id)

        def build(self, rate, days, owner=None, email_addr=None):
            assert bool(owner) ^ bool(email_addr)
            day_fields = {day: (day in days) for day in DAYS}
            ticket = self.model(rate=rate, owner=owner, **day_fields)

            if email_addr is not None:
                ticket.email_addr = email_addr

            return ticket

        def create_free_with_invitation(self,
                                        email_addr,
                                        free_reason,
                                        days=None):
            if days is None:
                days = {day: False for day in DAYS}
            else:
                days = {day: (day in days) for day in DAYS}
            ticket = self.create(free_reason=free_reason, **days)
            ticket.invitations.create(email_addr=email_addr)
            return ticket

    objects = Manager()

    def __str__(self):
        return self.ticket_id or 'Unsaved'

    @property
    def ticket_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def save(self, **kwargs):
        super().save(**kwargs)
        if hasattr(self, 'email_addr'):
            self.invitations.create(email_addr=self.email_addr)

    def get_absolute_url(self):
        return reverse('tickets:ticket', args=[self.ticket_id])

    def days(self):
        return [DAYS[day] for day in DAYS if getattr(self, day)]

    def num_days(self):
        return len(self.days())

    def ticket_holder_name(self):
        # TODO this is a mess
        if self.owner:
            return self.owner.name
        elif self.pk:
            return self.invitation().email_addr
        else:
            return self.email_addr

    def order_company_name(self):
        if self.rate == 'corporate' and self.order:
            return self.order.billing_name
        return ''

    @property
    def descr_for_order(self):
        return f'{self.num_days()}-day {self.rate}-rate ticket'

    @property
    def descr_extra_for_order(self):
        return self.days_sentence

    @property
    def days_sentence(self):
        return ', '.join(self.days())

    @property
    def is_saved(self):
        return self.pk is not None

    @property
    def order_row(self):
        # We expect there to only ever be a single OrderRow, so this should never fail
        return self.order_rows.get()

    @property
    def order(self):
        if self.is_saved and not self.free_reason:
            return self.order_row.order
        else:
            return None

    @property
    def cost_excl_vat(self):
        if self.free_reason:
            return 0
        elif self.is_saved:
            return self.order_row.cost_excl_vat
        else:
            return prices.cost_excl_vat(self.rate, self.num_days())

    @property
    def cost_incl_vat(self):
        return int(self.cost_excl_vat * 1.2)

    def invitation(self):
        # This will raise an exception if a ticket has multiple invitations
        return self.invitations.get()

    def update_days(self, days):
        if self.is_changeable or len(self.days()) == 0:
            for day in DAYS:
                setattr(self, day, (day in days))
            self.save()

    @property
    def is_free_ticket(self):
        return self.free_reason is not None

    @property
    def is_changeable(self):
        return self.free_reason in CHANGEABLE_REASONS
Ejemplo n.º 7
0
class Proposal(models.Model):
    SESSION_TYPE_CHOICES = (
        ('talk', 'A talk (25 minutes)'),
        ('workshop', 'A workshop (3 hours)'),
        ('poster', 'A poster'),
        ('kidsworkshop', 'Education Summit workshop for young coders (Saturday, 50 mins)'),
        ('teachersworkshop', 'Education Summit workshop for educators (Sunday, 50 mins)'),
        ('teacherstalk', 'Education Summit talk for educators (Sunday, 25 mins)'),
        ('other', 'Something else'),
    )

    STATE_TYPE_CHOICES = (
        ('confirm', 'Confirmed'),
        ('cancel', 'Cancelled'),
        ('accept', 'Accepted'),
        ('reject', 'Plan to Reject'),
        ('withdrawn', 'Withdrawn'),
        ('placeholder', 'Schedule Placeholder'),
    )

    proposer = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='proposals', on_delete=models.CASCADE)
    session_type = models.CharField(max_length=40, choices=SESSION_TYPE_CHOICES)
    title = models.CharField(max_length=60)
    subtitle = models.CharField(max_length=120, blank=True)
    copresenter_names = models.TextField(blank=True)
    description = models.TextField()  # validators=[validate_max_300_words])
    description_private = models.TextField(validators=[validate_max_300_words], blank=True)
    outline = models.TextField(blank=True)
    equipment = models.TextField(blank=True)
    aimed_at_new_programmers = models.BooleanField()
    aimed_at_teachers = models.BooleanField()
    aimed_at_data_scientists = models.BooleanField()
    would_like_mentor = models.BooleanField()
    would_like_longer_slot = models.BooleanField()
    state = models.CharField(max_length=40, blank=True, choices=STATE_TYPE_CHOICES)
    track = models.CharField(max_length=40, blank=True)
    special_reply_required = models.BooleanField(default=False)
    scheduled_room = models.CharField(max_length=40, blank=True)
    scheduled_time = models.DateTimeField(null=True)
    coc_conformity = models.BooleanField()
    ticket = models.BooleanField()
    confirmed = models.DateTimeField(null=True)
    replied_to = models.DateTimeField(null=True)

    break_event = models.BooleanField(default=False)
    conference_event = models.BooleanField(default=False)

    length_override = models.DurationField(blank=True, null=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(3000)

    class Meta:
        permissions = [
            ('review_proposal', 'Can review proposals'),
            ('review_education_proposal', 'Can review education proposals'),
        ]

    class Manager(models.Manager):
        def get_by_proposal_id_or_404(self, proposal_id):
            id = self.model.id_scrambler.backward(proposal_id)
            return get_object_or_404(self.model, pk=id)

        def accepted_talks(self):
            return self.filter(Q(session_type='talk') & Q(state='accepted'))

        def reviewed_by_user(self, user):
            return self.accepted_talks().filter(vote__user=user).order_by('id')

        def unreviewed_by_user(self, user):
            return self.accepted_talks().exclude(vote__user=user).order_by('id')

        def of_interest_to_user(self, user):
            return self.accepted_talks().filter(vote__user=user, vote__is_interested=True).order_by('id')

        def not_of_interest_to_user(self, user):
            return self.accepted_talks().filter(vote__user=user, vote__is_interested=False).order_by('id')

        def get_random_unreviewed_by_user(self, user):
            return self.unreviewed_by_user(user).order_by('?').first()

    objects = Manager()

    def __str__(self):
        return f'{self.title} ({self.proposal_id})'

    @property
    def proposal_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('cfp:proposal', args=[self.proposal_id])

    def full_title(self):
        if self.subtitle:
            return f'{self.title}: {self.subtitle}'
        else:
            return self.title

    @property
    def length(self):
        if self.length_override:
            return self.length_override
        elif self.session_type in ['talk', 'teacherstalk']:
            return timedelta(minutes=30)
        elif self.session_type == 'workshop':
            return timedelta(minutes=180)
        elif self.session_type in ['kidsworkshop', 'teachersworkshop']:
            return timedelta(minutes=60)
        else:
            return timedelta(minutes=0)

    @property
    def all_presenter_names(self):
        if self.copresenter_names:
            return f'{self.proposer.name}, {self.copresenter_names}'
        return self.proposer.name
Ejemplo n.º 8
0
class Application(models.Model):

    REQUESTED_TICKET_ONLY_CHOICES = [
        (True,
         "I'd like to request a free ticket, but don't need other financial assistance"
         ),
        (False,
         "I'd like to request a free ticket and additional financial assistance"
         ),
    ]

    applicant = models.OneToOneField(settings.AUTH_USER_MODEL,
                                     related_name='grant_application',
                                     on_delete=models.CASCADE)

    about_you = models.TextField(blank=False)
    about_why = models.TextField(blank=False)

    requested_ticket_only = models.BooleanField(
        blank=False, choices=REQUESTED_TICKET_ONLY_CHOICES)

    amount_requested = models.TextField(blank=True)
    cost_breakdown = models.TextField(blank=True)
    ticket_awarded = models.BooleanField(default=False)
    amount_awarded = models.DecimalField(null=True,
                                         blank=True,
                                         max_digits=6,
                                         decimal_places=2)
    full_amount_awarded = models.BooleanField(default=False)
    application_declined = models.BooleanField(default=False)
    replied_to = models.DateTimeField(null=True)

    sat = models.BooleanField()
    sun = models.BooleanField()
    mon = models.BooleanField()
    tue = models.BooleanField()
    wed = models.BooleanField()

    id_scrambler = Scrambler(4000)

    class Manager(models.Manager):
        def get_by_application_id_or_404(self, application_id):
            id = self.model.id_scrambler.backward(application_id)
            return get_object_or_404(self.model, pk=id)

    objects = Manager()

    @property
    def application_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('grants:application', args=[self.application_id])

    def days(self):
        return [DAYS[day] for day in DAYS if getattr(self, day)]

    def __str__(self):
        if (self.ticket_awarded
                or self.amount_awarded) and not self.application_declined:
            return f'Accepted application: {self.applicant.name}'
        return f'Declined application: {self.applicant.name}'
Ejemplo n.º 9
0
class Ticket(models.Model):
    order = models.ForeignKey(Order,
                              related_name='tickets',
                              on_delete=models.CASCADE)
    owner = models.OneToOneField(settings.AUTH_USER_MODEL,
                                 null=True,
                                 on_delete=models.CASCADE)
    thu = models.BooleanField()
    fri = models.BooleanField()
    sat = models.BooleanField()
    sun = models.BooleanField()
    mon = models.BooleanField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(2000)

    class Manager(models.Manager):
        def get_by_ticket_id_or_404(self, ticket_id):
            id = self.model.id_scrambler.backward(ticket_id)
            return get_object_or_404(self.model, pk=id)

        def create_for_user(self, user, days):
            day_fields = {day: (day in days) for day in DAYS}
            self.create(owner=user, **day_fields)

        def create_with_invitation(self, email_addr, days):
            day_fields = {day: (day in days) for day in DAYS}
            ticket = self.create(**day_fields)
            ticket.invitations.create(email_addr=email_addr)

    objects = Manager()

    def __str__(self):
        return self.ticket_id

    @property
    def ticket_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('tickets:ticket', args=[self.ticket_id])

    def details(self):
        return {
            'id': self.ticket_id,
            'name': self.ticket_holder_name(),
            'days': ', '.join(self.days()),
            'cost_excl_vat': self.cost_excl_vat(),
            'cost_incl_vat': self.cost_incl_vat(),
        }

    def days(self):
        return [DAYS[day] for day in DAYS if getattr(self, day)]

    def num_days(self):
        return len(self.days())

    def ticket_holder_name(self):
        if self.owner:
            return self.owner.name
        else:
            return self.invitation().email_addr

    def cost_incl_vat(self):
        return cost_incl_vat(self.order.rate, self.num_days())

    def cost_excl_vat(self):
        return cost_excl_vat(self.order.rate, self.num_days())

    def invitation(self):
        # This will raise an exception if a ticket has multiple invitations
        return self.invitations.get()
Ejemplo n.º 10
0
class Order(models.Model):
    purchaser = models.ForeignKey(settings.AUTH_USER_MODEL,
                                  related_name='orders',
                                  on_delete=models.CASCADE)
    rate = models.CharField(max_length=40)
    company_name = models.CharField(max_length=200, null=True)
    company_addr = models.TextField(null=True)
    status = models.CharField(max_length=10)
    stripe_charge_id = models.CharField(max_length=80)
    stripe_charge_created = models.DateTimeField(null=True)
    stripe_charge_failure_reason = models.CharField(max_length=400, blank=True)
    unconfirmed_details = JSONField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(1000)

    class Manager(models.Manager):
        def get_by_order_id_or_404(self, order_id):
            id = self.model.id_scrambler.backward(order_id)
            return get_object_or_404(self.model, pk=id)

        def create_pending(self,
                           purchaser,
                           rate,
                           days_for_self=None,
                           email_addrs_and_days_for_others=None,
                           company_details=None):
            assert days_for_self is not None or email_addrs_and_days_for_others is not None

            if rate == 'corporate':
                assert company_details is not None
                company_name = company_details['name']
                company_addr = company_details['addr']
            elif rate in ['individual', 'education']:
                assert company_details is None
                company_name = None
                company_addr = None
            else:
                assert False

            unconfirmed_details = {
                'days_for_self': days_for_self,
                'email_addrs_and_days_for_others':
                email_addrs_and_days_for_others,
            }

            return self.create(
                purchaser=purchaser,
                rate=rate,
                company_name=company_name,
                company_addr=company_addr,
                status='pending',
                unconfirmed_details=unconfirmed_details,
            )

    objects = Manager()

    def __str__(self):
        return self.order_id

    @property
    def order_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('tickets:order', args=[self.order_id])

    def update(self,
               rate,
               days_for_self=None,
               email_addrs_and_days_for_others=None,
               company_details=None):
        assert self.payment_required()
        assert days_for_self is not None or email_addrs_and_days_for_others is not None

        if rate == 'corporate':
            assert company_details is not None
            self.company_name = company_details['name']
            self.company_addr = company_details['addr']
        elif rate == 'individual':
            assert company_details is None
            self.company_name = None
            self.company_addr = None
        else:
            assert False

        self.rate = rate
        self.unconfirmed_details = {
            'days_for_self': days_for_self,
            'email_addrs_and_days_for_others': email_addrs_and_days_for_others,
        }
        self.save()

    def confirm(self, charge_id, charge_created):
        assert self.payment_required()

        days_for_self = self.unconfirmed_details['days_for_self']
        if days_for_self is not None:
            self.tickets.create_for_user(self.purchaser, days_for_self)

        email_addrs_and_days_for_others = self.unconfirmed_details[
            'email_addrs_and_days_for_others']
        if email_addrs_and_days_for_others is not None:
            for email_addr, days in email_addrs_and_days_for_others:
                self.tickets.create_with_invitation(email_addr, days)

        self.stripe_charge_id = charge_id
        self.stripe_charge_created = datetime.fromtimestamp(charge_created,
                                                            tz=timezone.utc)
        self.stripe_charge_failure_reason = ''
        self.status = 'successful'

        self.save()

    def mark_as_failed(self, charge_failure_reason):
        self.stripe_charge_failure_reason = charge_failure_reason
        self.status = 'failed'

        self.save()

    def march_as_errored_after_charge(self, charge_id):
        self.stripe_charge_id = charge_id
        self.stripe_charge_failure_reason = ''
        self.status = 'errored'

        self.save()

    def all_tickets(self):
        if self.payment_required():
            tickets = []

            days_for_self = self.unconfirmed_details['days_for_self']
            if days_for_self is not None:
                ticket = UnconfirmedTicket(
                    order=self,
                    owner=self.purchaser,
                    days=days_for_self,
                )
                tickets.append(ticket)

            email_addrs_and_days_for_others = self.unconfirmed_details[
                'email_addrs_and_days_for_others']
            if email_addrs_and_days_for_others is not None:
                for email_addr, days in email_addrs_and_days_for_others:
                    ticket = UnconfirmedTicket(
                        order=self,
                        email_addr=email_addr,
                        days=days,
                    )
                    tickets.append(ticket)
            return tickets
        else:
            return self.tickets.all()

    def form_data(self):
        assert self.payment_required()

        data = {'rate': self.rate}

        days_for_self = self.unconfirmed_details['days_for_self']
        email_addrs_and_days_for_others = self.unconfirmed_details[
            'email_addrs_and_days_for_others']

        if days_for_self is None:
            assert email_addrs_and_days_for_others is not None
            data['who'] = 'others'
        elif email_addrs_and_days_for_others is None:
            assert days_for_self is not None
            data['who'] = 'self'
        else:
            data['who'] = 'self and others'

        return data

    def self_form_data(self):
        assert self.payment_required()

        days_for_self = self.unconfirmed_details['days_for_self']
        if days_for_self is None:
            return None

        return {'days': days_for_self}

    def others_formset_data(self):
        assert self.payment_required()

        email_addrs_and_days_for_others = self.unconfirmed_details[
            'email_addrs_and_days_for_others']
        if email_addrs_and_days_for_others is None:
            return None

        data = {
            'form-TOTAL_FORMS': str(len(email_addrs_and_days_for_others)),
            'form-INITIAL_FORMS': str(len(email_addrs_and_days_for_others)),
        }

        for ix, (email_addr,
                 days) in enumerate(email_addrs_and_days_for_others):
            data[f'form-{ix}-email_addr'] = email_addr
            data[f'form-{ix}-days'] = days

        return data

    def company_details_form_data(self):
        if self.rate == 'corporate':
            return {
                'company_name': self.company_name,
                'company_addr': self.company_addr,
            }
        else:
            return None

    def ticket_details(self):
        return [ticket.details() for ticket in self.all_tickets()]

    def ticket_summary(self):
        num_tickets_by_num_days = defaultdict(int)

        for ticket in self.all_tickets():
            num_tickets_by_num_days[ticket.num_days()] += 1

        summary = []

        for ix in range(5):
            num_days = ix + 1
            if num_tickets_by_num_days[num_days]:
                num_tickets = num_tickets_by_num_days[num_days]
                summary.append({
                    'num_days':
                    num_days,
                    'num_tickets':
                    num_tickets,
                    'per_item_cost_excl_vat':
                    cost_excl_vat(self.rate, num_days),
                    'per_item_cost_incl_vat':
                    cost_incl_vat(self.rate, num_days),
                    'total_cost_excl_vat':
                    cost_excl_vat(self.rate, num_days) * num_tickets,
                    'total_cost_incl_vat':
                    cost_incl_vat(self.rate, num_days) * num_tickets,
                })

        return summary

    def brief_summary(self):
        summary = f'{self.num_tickets()} {self.rate}-rate ticket'
        if self.num_tickets() > 1:
            summary += 's'
        return summary

    def cost_excl_vat(self):
        return sum(ticket.cost_excl_vat() for ticket in self.all_tickets())

    def cost_incl_vat(self):
        return sum(ticket.cost_incl_vat() for ticket in self.all_tickets())

    def vat(self):
        return self.cost_incl_vat() - self.cost_excl_vat()

    def cost_pence_incl_vat(self):
        return 100 * self.cost_incl_vat()

    def num_tickets(self):
        return len(self.all_tickets())

    def unclaimed_tickets(self):
        return self.tickets.filter(owner=None)

    def ticket_for_self(self):
        tickets = [
            ticket for ticket in self.all_tickets()
            if ticket.owner == self.purchaser
        ]
        if len(tickets) == 0:
            return None
        elif len(tickets) == 1:
            return tickets[0]
        else:
            assert False

    def tickets_for_others(self):
        return [
            ticket for ticket in self.all_tickets()
            if ticket.owner != self.purchaser
        ]

    def payment_required(self):
        return self.status in ['pending', 'failed']

    def company_addr_formatted(self):
        if self.rate == 'corporate':
            lines = [
                line.strip(',') for line in self.company_addr.splitlines()
                if line
            ]
            return ', '.join(lines)
        else:
            return None
Ejemplo n.º 11
0
class Ticket(models.Model):
    order = models.ForeignKey(Order,
                              related_name='tickets',
                              null=True,
                              on_delete=models.CASCADE)
    pot = models.CharField(max_length=100, null=True)
    owner = models.OneToOneField(settings.AUTH_USER_MODEL,
                                 null=True,
                                 on_delete=models.CASCADE)
    thu = models.BooleanField()
    fri = models.BooleanField()
    sat = models.BooleanField()
    sun = models.BooleanField()
    mon = models.BooleanField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(2000)

    class Manager(models.Manager):
        def get_by_ticket_id_or_404(self, ticket_id):
            id = self.model.id_scrambler.backward(ticket_id)
            return get_object_or_404(self.model, pk=id)

        def create_for_user(self, user, days):
            day_fields = {day: (day in days) for day in DAYS}
            return self.create(owner=user, **day_fields)

        def create_with_invitation(self, email_addr, days):
            day_fields = {day: (day in days) for day in DAYS}
            ticket = self.create(**day_fields)
            ticket.invitations.create(email_addr=email_addr)
            return ticket

        def create_free_with_invitation(self, email_addr, pot):
            days = {day: False for day in DAYS}
            ticket = self.create(pot=pot, **days)
            ticket.invitations.create(email_addr=email_addr)
            return ticket

    objects = Manager()

    def __str__(self):
        return self.ticket_id

    @property
    def ticket_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('tickets:ticket', args=[self.ticket_id])

    def reassign(self, email_addr):
        if self.owner is not None:
            self.owner = None
            self.save()

        try:
            invitation = self.invitation()
            invitation.delete()
        except TicketInvitation.DoesNotExist:
            pass

        self.invitations.create(email_addr=email_addr)

    def details(self):
        return {
            'id': self.ticket_id,
            'name': self.ticket_holder_name(),
            'days': ', '.join(self.days()),
            'cost_excl_vat': self.cost_excl_vat(),
            'cost_incl_vat': self.cost_incl_vat(),
        }

    def days(self):
        return [DAYS[day] for day in DAYS if getattr(self, day)]

    def days_abbrev(self):
        return [day for day in DAYS if getattr(self, day)]

    def num_days(self):
        return len(self.days())

    def ticket_holder_name(self):
        if self.owner:
            return self.owner.name
        else:
            return self.invitation().email_addr

    def rate(self):
        if self.order is None:
            return 'free'
        else:
            return self.order.rate

    def cost_incl_vat(self):
        return cost_incl_vat(self.rate(), self.num_days())

    def cost_excl_vat(self):
        return cost_excl_vat(self.rate(), self.num_days())

    def invitation(self):
        # This will raise an exception if a ticket has multiple invitations
        return self.invitations.get()

    def is_free_ticket(self):
        return not self.order

    def is_incomplete(self):
        return self.days() == []

    def update_days(self, days):
        for day in DAYS:
            setattr(self, day, (day in days))
        self.save()
Ejemplo n.º 12
0
class Proposal(models.Model):
    SESSION_TYPE_CHOICES = (
        ('talk', 'A talk (25 minutes)'),
        ('workshop', 'A workshop (3 hours)'),
        ('poster', 'A poster'),
        ('other', 'Something else'),
    )

    proposer = models.ForeignKey(settings.AUTH_USER_MODEL,
                                 related_name='proposals',
                                 on_delete=models.CASCADE)
    session_type = models.CharField(max_length=40,
                                    choices=SESSION_TYPE_CHOICES)
    title = models.CharField(max_length=60)
    subtitle = models.CharField(max_length=120, blank=True)
    copresenter_names = models.TextField(blank=True)
    description = models.TextField(validators=[validate_max_300_words])
    description_private = models.TextField(validators=[validate_max_300_words])
    aimed_at_new_programmers = models.BooleanField()
    aimed_at_teachers = models.BooleanField()
    aimed_at_data_scientists = models.BooleanField()
    would_like_mentor = models.BooleanField()
    would_like_longer_slot = models.BooleanField()
    state = models.CharField(max_length=40, blank=True)
    track = models.CharField(max_length=40, blank=True)
    special_reply_required = models.BooleanField(default=False)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(3000)

    class Meta:
        permissions = [
            ('review_proposal', 'Can review proposals'),
        ]

    class Manager(models.Manager):
        def get_by_proposal_id_or_404(self, proposal_id):
            id = self.model.id_scrambler.backward(proposal_id)
            return get_object_or_404(self.model, pk=id)

        def reviewed_by_user(self, user):
            return self.filter(vote__user=user).order_by('id')

        def unreviewed_by_user(self, user):
            return self.exclude(vote__user=user).order_by('id')

        def of_interest_to_user(self, user):
            return self.filter(vote__user=user,
                               vote__is_interested=True).order_by('id')

        def not_of_interest_to_user(self, user):
            return self.filter(vote__user=user,
                               vote__is_interested=False).order_by('id')

        def get_random_unreviewed_by_user(self, user):
            return self.unreviewed_by_user(user).order_by('?').first()

    objects = Manager()

    def __str__(self):
        return self.proposal_id

    @property
    def proposal_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('cfp:proposal', args=[self.proposal_id])

    def full_title(self):
        if self.subtitle:
            return f'{self.title}: {self.subtitle}'
        else:
            return self.title

    def is_accepted(self):
        return self.state == 'plan to accept'

    def is_rejected(self):
        return self.state == 'plan to reject'

    def session_type_for_display(self):
        if self.session_type == 'other':
            return 'a one-off session'
        else:
            return dict(self.SESSION_TYPE_CHOICES)[self.session_type].lower()

    def vote(self, user, is_interested):
        self.vote_set.update_or_create(
            user=user,
            defaults={
                'is_interested': is_interested,
            },
        )

    def is_interested(self, user):
        try:
            vote = self.vote_set.get(user=user)
        except Vote.DoesNotExist:
            return None

        return vote.is_interested

    def is_interested_for_form(self, user):
        is_interested = self.is_interested(user)

        if is_interested is True:
            return 'yes'
        elif is_interested is False:
            return 'no'
Ejemplo n.º 13
0
class Order(models.Model, SalesRecord):
    purchaser = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='orders', on_delete=models.CASCADE)
    billing_name = models.CharField(max_length=200, null=True)
    billing_addr = models.TextField(null=True)
    invoice_number = models.IntegerField(null=True, unique=True)
    status = models.CharField(max_length=10)
    stripe_charge_id = models.CharField(max_length=80)
    stripe_charge_created = models.DateTimeField(null=True)
    stripe_charge_failure_reason = models.CharField(max_length=400, blank=True)
    unconfirmed_details = JSONField()
    content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(1000)

    class Manager(models.Manager):
        def get_by_order_id_or_404(self, order_id):
            id = self.model.id_scrambler.backward(order_id)
            return get_object_or_404(self.model, pk=id)

        def create_pending(self, purchaser, billing_details, unconfirmed_details, content_type):
            assert unconfirmed_details is not None

            billing_name = billing_details['name']
            billing_addr = billing_details['addr']

            return self.create(
                purchaser=purchaser,
                billing_name=billing_name,
                billing_addr=billing_addr,
                status='pending',
                unconfirmed_details=unconfirmed_details,
                content_type=content_type
            )

    objects = Manager()

    def __str__(self):
        return self.order_id

    @property
    def order_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('orders:order', args=[self.order_id])

    @property
    def refunds(self):
        row_ids = [row.id for row in self.all_order_rows()]
        return Refund.objects.filter(order_rows__id__in=row_ids)

    def update(self, billing_details, details):
        assert self.payment_required()

        if self.content_type == ContentType.objects.get(app_label="tickets", model="ticket"):
            assert details['days_for_self'] is not None or details['email_addrs_and_days_for_others'] is not None

        self.billing_name = billing_details['name']
        self.billing_addr = billing_details['addr']
        self.unconfirmed_details = details
        self.save()

    def confirm(self, charge_id, charge_created):
        assert self.payment_required()

        for row in self.build_order_rows():
            row.save()

        self.stripe_charge_id = charge_id
        self.stripe_charge_created = datetime.fromtimestamp(charge_created, tz=timezone.utc)
        self.stripe_charge_failure_reason = ''
        self.status = 'successful'
        self.invoice_number = self.get_next_invoice_number()

        self.save()

    @classmethod
    def get_next_invoice_number(cls):
        prev_invoice_number = cls.objects.aggregate(n=Max('invoice_number'))['n'] or 0
        return prev_invoice_number + 1

    @property
    def cost_pence_incl_vat(self):
        return int(100 * self.cost_incl_vat)

    @property
    def full_invoice_number(self):
        return f'S-2018-{self.invoice_number:04d}'

    def mark_as_failed(self, charge_failure_reason):
        self.stripe_charge_failure_reason = charge_failure_reason
        self.status = 'failed'

        self.save()

    def mark_as_errored_after_charge(self, charge_id):
        self.stripe_charge_id = charge_id
        self.stripe_charge_failure_reason = ''
        self.status = 'errored'
        self.invoice_number = None

        self.save()

    def build_order_rows(self):
        assert self.payment_required()

        rows = []

        if self.content_type == ContentType.objects.get(app_label="tickets", model="ticket"):

            days_for_self = self.unconfirmed_details['days_for_self']
            if days_for_self is not None:
                ticket = Ticket.objects.build(
                    rate=self.unconfirmed_details['rate'],
                    owner=self.purchaser,
                    days=days_for_self,
                )
                row = self.order_rows.build_for_item(ticket)
                rows.append(row)

            email_addrs_and_days_for_others = self.unconfirmed_details['email_addrs_and_days_for_others']
            if email_addrs_and_days_for_others is not None:
                for email_addr, name, days in email_addrs_and_days_for_others:
                    ticket = Ticket.objects.build(
                        rate=self.unconfirmed_details['rate'],
                        email_addr=email_addr,
                        days=days,
                    )
                    rows.append(self.order_rows.build_for_item(ticket))

        else:
            ticket = ExtraItem.objects.build(
                content_type=self.content_type,
                owner=self.purchaser,
                details=self.unconfirmed_details,
            )
            row = self.order_rows.build_for_item(ticket)
            rows.append(row)

        return rows

    def all_order_rows(self):
        if self.payment_required():
            return self.build_order_rows()
        else:
            return self.order_rows.order_by('content_type', 'object_id')

    def all_items(self):
        return [order_row.item for order_row in self.all_order_rows()]

    def all_tickets(self):
        return [item for item in self.all_items() if isinstance(item, Ticket)]

    def num_tickets(self):
        return len(self.all_tickets())

    def num_items(self):
        return len(self.all_items())

    def is_ticket_order(self):
        ticket_content_type = ContentType.objects.get(app_label="tickets", model="ticket")
        return any([order_row.content_type == ticket_content_type for order_row in self.all_order_rows()])

    def order_content_type(self):
        return self.all_order_rows()[0].content_type

    def unclaimed_tickets(self):
        return [ticket for ticket in self.all_tickets() if ticket.owner is None]

    def ticket_for_self(self):
        tickets = [ticket for ticket in self.all_tickets() if ticket.owner == self.purchaser]
        if len(tickets) == 0:
            return None
        elif len(tickets) == 1:
            return tickets[0]
        else:  # pragma: no cover
            assert False

    def tickets_for_others(self):
        return [ticket for ticket in self.all_tickets() if ticket.owner != self.purchaser]

    def payment_required(self):
        return self.status in ['pending', 'failed']
Ejemplo n.º 14
0
class Refund(models.Model, SalesRecord):
    reason = models.CharField(max_length=400)
    credit_note_number = models.IntegerField()
    stripe_refund_id = models.CharField(max_length=80)
    stripe_refund_created = models.DateTimeField(null=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(5000)

    class Manager(models.Manager):
        def get_by_refund_id_or_404(self, refund_id):
            id = self.model.id_scrambler.backward(refund_id)
            return get_object_or_404(self.model, pk=id)

        def create_for_item(self, item, reason, stripe_refund_id, stripe_refund_created):
            refund = self.create(
                reason=reason,
                stripe_refund_id=stripe_refund_id,
                stripe_refund_created=datetime.fromtimestamp(
                    stripe_refund_created,
                    tz=timezone.utc
                ),
                credit_note_number=0
            )

            order_row = item.order_row
            order_row.object_id = None
            order_row.content_type = None
            order_row.refund = refund
            order_row.save()

            refund.credit_note_number = refund.get_next_credit_note_number()
            refund.save()

            item.delete()
            return refund

    objects = Manager()

    @property
    def refund_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    @property
    def order(self):
        row_ids = [row.id for row in self.all_order_rows()]
        return Order.objects.get(order_rows__id__in=row_ids)

    @property
    def full_credit_note_number(self):
        return f'R-2018-{self.order.invoice_number:04d}-{self.credit_note_number:02d}'

    def get_next_credit_note_number(self):
        return self.order_rows.count()

    def all_order_rows(self):
        return self.order_rows.order_by('content_type', 'object_id')
Ejemplo n.º 15
0
class User(AbstractBaseUser, PermissionsMixin):
    YEAR_OF_BIRTH_CHOICES = [['not shared', 'prefer not to say']
                             ] + [[str(year), str(year)]
                                  for year in range(1917, 2017)]

    GENDER_CHOICES = [
        ['not shared', 'prefer not to say'],
        ['female', 'female'],
        ['male', 'male'],
        ['other', 'please specify'],
    ]

    COUNTRY_CHOICES = [['not shared', 'prefer not to say']] + [
        [country, country] for country in COUNTRIES
    ] + [['other', 'not listed here (please specify)']]

    NATIONALITY_CHOICES = [['not shared', 'prefer not to say']] + [
        [nationality, nationality] for nationality in NATIONALITIES
    ] + [['other', 'not listed here (please specify)']]

    # Sorry
    ETHNICITY_CHOICES = [['not shared', 'prefer not to say']] + [[
        ethnicity_category,
        [[ethnicity, ethnicity] for ethnicity in ethnicities]
    ] for ethnicity_category, ethnicities in ETHNICITIES]

    email_addr = models.EmailField(
        'email address',
        unique=True,
        error_messages={
            'unique': 'That email address has already been registered',
        },
    )
    name = models.CharField(max_length=200)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    year_of_birth = models.CharField(max_length=10,
                                     null=True,
                                     blank=True,
                                     choices=YEAR_OF_BIRTH_CHOICES)
    gender = models.CharField(max_length=100,
                              null=True,
                              blank=True,
                              choices=GENDER_CHOICES)
    ethnicity = models.CharField(max_length=100,
                                 null=True,
                                 blank=True,
                                 choices=ETHNICITY_CHOICES)
    ethnicity_free_text = models.CharField(max_length=100,
                                           null=True,
                                           blank=True)
    nationality = models.CharField(max_length=100,
                                   null=True,
                                   blank=True,
                                   choices=NATIONALITY_CHOICES)
    country_of_residence = models.CharField(max_length=100,
                                            null=True,
                                            blank=True,
                                            choices=COUNTRY_CHOICES)
    dont_ask_demographics = models.BooleanField(default=False)
    accessibility_reqs_yn = models.NullBooleanField()
    accessibility_reqs = models.TextField(null=True, blank=True)
    childcare_reqs_yn = models.NullBooleanField()
    childcare_reqs = models.TextField(null=True, blank=True)
    dietary_reqs_yn = models.NullBooleanField()
    dietary_reqs = models.TextField(null=True, blank=True)
    is_ukpa_member = models.NullBooleanField(null=True, blank=True)
    has_booked_hotel = models.NullBooleanField()
    volunteer_setup = models.NullBooleanField()
    volunteer_session_chair = models.NullBooleanField()
    volunteer_videoer = models.NullBooleanField()
    volunteer_reg_desk = models.NullBooleanField()
    is_contributor = models.BooleanField(default=False)
    coming_to_dojo = models.NullBooleanField()
    coming_to_board_games = models.NullBooleanField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    USERNAME_FIELD = 'email_addr'
    EMAIL_FIELD = 'email_addr'
    REQUIRED_FIELDS = ['name']

    id_scrambler = Scrambler(8000)

    objects = Manager()

    @property
    def user_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_full_name(self):
        '''This is used by the admin.'''
        return self.name

    def get_short_name(self):
        '''This is used by the admin.'''
        return self.name

    def get_ticket(self):
        try:
            return self.ticket
        except Ticket.DoesNotExist:
            return None

    def get_accommodation_booking(self):
        try:
            return self.booking
        except Booking.DoesNotExist:
            return None

    def get_grant_application(self):
        try:
            return self.grant_application
        except Application.DoesNotExist:
            return None

    def get_nomination(self):
        try:
            return self.nomination
        except Nomination.DoesNotExist:
            return None

    def profile_complete(self):
        if any(v is None for v in [
                self.accessibility_reqs_yn,
                self.childcare_reqs_yn,
                self.dietary_reqs_yn,
        ]):
            return False

        if self.get_ticket() is not None and self.is_ukpa_member is None:
            return False

        if self.dont_ask_demographics:
            return True

        return all(v for v in [
            self.year_of_birth, self.gender, self.ethnicity, self.nationality,
            self.country_of_residence
        ])

    def submitted_single_proposal(self):
        return len(self.proposals.all()) == 1

    def accepted_proposals(self):
        return [p for p in self.proposals.all() if p.is_accepted()]

    def rejected_proposals(self):
        return [p for p in self.proposals.all() if p.is_rejected()]

    def all_proposals_accepted(self):
        return len(self.proposals.all()) > 0 and len(
            self.proposals.all()) == len(self.accepted_proposals())

    def any_proposals_accepted(self):
        return len(self.accepted_proposals()) > 0

    def some_proposals_accepted(self):
        return self.any_proposals_accepted(
        ) and not self.all_proposals_accepted()

    def one_proposal_rejected(self):
        return len(self.rejected_proposals()) == 1

    def one_proposal_accepted(self):
        return len(self.accepted_proposals()) == 1

    def things_volunteered_for(self):
        things = []
        if self.volunteer_setup:
            things.append('Help with setup at 6am on Thursday morning')
        if self.volunteer_session_chair:
            things.append('Chair a session')
        if self.volunteer_videoer:
            things.append('Help with videoing talks')
        if self.volunteer_reg_desk:
            things.append('Staff the registration desk')
        return things

    def get_free_dinner_booking(self):
        if not self.is_contributor:
            return None

        return self.dinner_bookings.filter(stripe_charge_id=None).first()

    def get_contributors_dinner_booking(self):
        return self.dinner_bookings.filter(venue='contributors').first()

    def get_conference_dinner_booking(self):
        return self.dinner_bookings.filter(venue='conference').first()
Ejemplo n.º 16
0
class User(AbstractBaseUser, PermissionsMixin):
    YEAR_OF_BIRTH_CHOICES = [['not shared', 'prefer not to say']
                             ] + [[str(year), str(year)]
                                  for year in range(1917, 2017)]

    GENDER_CHOICES = [
        ['not shared', 'prefer not to say'],
        ['female', 'female'],
        ['male', 'male'],
        ['other', 'please specify'],
    ]

    COUNTRY_CHOICES = [['not shared', 'prefer not to say']] + [
        [country, country] for country in COUNTRIES
    ] + [['other', 'not listed here (please specify)']]

    NATIONALITY_CHOICES = [['not shared', 'prefer not to say']] + [
        [nationality, nationality] for nationality in NATIONALITIES
    ] + [['other', 'not listed here (please specify)']]

    # Sorry
    ETHNICITY_CHOICES = [['not shared', 'prefer not to say']] + [[
        ethnicity_category,
        [[ethnicity, ethnicity] for ethnicity in ethnicities]
    ] for ethnicity_category, ethnicities in ETHNICITIES]

    email_addr = models.EmailField(
        'email address',
        unique=True,
        error_messages={
            'unique': 'That email address has already been registered',
        },
    )
    name = models.CharField(max_length=200)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    year_of_birth = models.CharField(max_length=10,
                                     null=True,
                                     blank=True,
                                     choices=YEAR_OF_BIRTH_CHOICES)
    gender = models.CharField(max_length=100,
                              null=True,
                              blank=True,
                              choices=GENDER_CHOICES)
    ethnicity = models.CharField(max_length=100,
                                 null=True,
                                 blank=True,
                                 choices=ETHNICITY_CHOICES)
    ethnicity_free_text = models.CharField(max_length=100,
                                           null=True,
                                           blank=True)
    nationality = models.CharField(max_length=100,
                                   null=True,
                                   blank=True,
                                   choices=NATIONALITY_CHOICES)
    country_of_residence = models.CharField(max_length=100,
                                            null=True,
                                            blank=True,
                                            choices=COUNTRY_CHOICES)
    dont_ask_demographics = models.BooleanField(default=False)
    accessibility_reqs_yn = models.NullBooleanField()
    accessibility_reqs = models.TextField(null=True, blank=True)
    childcare_reqs_yn = models.NullBooleanField()
    childcare_reqs = models.TextField(null=True, blank=True)
    dietary_reqs_yn = models.NullBooleanField()
    dietary_reqs = models.TextField(null=True, blank=True)
    is_ukpa_member = models.NullBooleanField(null=True, blank=True)
    is_contributor = models.BooleanField(default=False)
    is_organiser = models.BooleanField(default=False)
    accepted_terms = models.DateTimeField(auto_now_add=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    badge_company = models.CharField(max_length=100, blank=True, null=True)
    badge_twitter = models.CharField(max_length=50, blank=True, null=True)
    badge_pronoun = models.CharField(max_length=50, blank=True, null=True)
    badge_snake_colour = models.CharField(max_length=50, blank=True, null=True)
    badge_snake_extras = models.CharField(max_length=200,
                                          blank=True,
                                          null=True)

    ical_token = models.CharField(max_length=24, default=get_ical_token)
    items_of_interest = JSONField(default=[])

    USERNAME_FIELD = 'email_addr'
    EMAIL_FIELD = 'email_addr'
    REQUIRED_FIELDS = ['name']

    id_scrambler = Scrambler(8000)

    objects = Manager()

    def __str__(self):
        return f'{self.name} ({self.user_id})'

    class Meta:
        permissions = [
            ('reg_desk_assistant', 'Registration Desk Assistant'),
        ]

    @property
    def user_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_full_name(self):
        '''This is used by the admin.'''
        return self.name

    def get_short_name(self):
        '''This is used by the admin.'''
        return self.name

    def get_ticket(self):
        try:
            return self.ticket
        except Ticket.DoesNotExist:
            return None

    def profile_complete(self):
        if self.get_ticket() is not None and self.is_ukpa_member is None:
            return False

        if any(v is None for v in [
                self.accessibility_reqs_yn,
                self.childcare_reqs_yn,
                self.dietary_reqs_yn,
        ]):
            return False

        if self.dont_ask_demographics:
            return True

        return all(v for v in [
            self.year_of_birth, self.gender, self.ethnicity, self.nationality,
            self.country_of_residence
        ])

    def get_grant_application(self):
        try:
            return self.grant_application
        except Application.DoesNotExist:
            return None
Ejemplo n.º 17
0
class Order(models.Model):
    purchaser = models.ForeignKey(settings.AUTH_USER_MODEL,
                                  related_name='children_orders',
                                  on_delete=models.CASCADE)
    adult_name = models.CharField(max_length=255)
    adult_email_addr = models.CharField(max_length=255)
    adult_phone_number = models.CharField(max_length=255)
    accessibility_reqs = models.TextField(null=True, blank=True)
    dietary_reqs = models.TextField(null=True, blank=True)
    status = models.CharField(max_length=10)
    stripe_charge_id = models.CharField(max_length=80)
    stripe_charge_created = models.DateTimeField(null=True)
    stripe_charge_failure_reason = models.CharField(max_length=400, blank=True)
    unconfirmed_details = JSONField()

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    id_scrambler = Scrambler(6000)

    class Manager(models.Manager):
        def get_by_order_id_or_404(self, order_id):
            id = self.model.id_scrambler.backward(order_id)
            return get_object_or_404(self.model, pk=id)

        def create_pending(self, purchaser, adult_name, adult_email_addr,
                           adult_phone_number, accessibility_reqs,
                           dietary_reqs, unconfirmed_details):
            assert len(unconfirmed_details) > 0

            return self.create(
                purchaser=purchaser,
                adult_name=adult_name,
                adult_phone_number=adult_phone_number,
                adult_email_addr=adult_email_addr,
                accessibility_reqs=accessibility_reqs,
                dietary_reqs=dietary_reqs,
                unconfirmed_details=unconfirmed_details,
                status='pending',
            )

    objects = Manager()

    def __str__(self):
        return self.order_id

    @property
    def order_id(self):
        if self.id is None:
            return None
        return self.id_scrambler.forward(self.id)

    def get_absolute_url(self):
        return reverse('children:order', args=[self.order_id])

    def update(self, adult_name, adult_email_addr, adult_phone_number,
               accessibility_reqs, dietary_reqs, unconfirmed_details):
        assert self.payment_required()
        assert len(unconfirmed_details) > 0

        self.adult_name = adult_name
        self.adult_phone_number = adult_phone_number
        self.adult_email_addr = adult_email_addr
        self.accessibility_reqs = accessibility_reqs
        self.dietary_reqs = dietary_reqs
        self.unconfirmed_details = unconfirmed_details

        self.save()

    def confirm(self, charge_id, charge_created):
        assert self.payment_required()

        for name, date_of_birth in self.unconfirmed_details:
            self.tickets.create(name=name, date_of_birth=date_of_birth)

        self.stripe_charge_id = charge_id
        self.stripe_charge_created = datetime.fromtimestamp(charge_created,
                                                            tz=timezone.utc)
        self.stripe_charge_failure_reason = ''
        self.status = 'successful'

        self.save()

    def mark_as_failed(self, charge_failure_reason):
        self.stripe_charge_failure_reason = charge_failure_reason
        self.status = 'failed'

        self.save()

    def payment_required(self):
        return self.status in ['pending', 'failed']

    def num_tickets(self):
        return len(self.all_tickets())

    def cost_incl_vat(self):
        return 5 * self.num_tickets()

    def cost_pence_incl_vat(self):
        return 100 * self.cost_incl_vat()

    def all_tickets(self):
        if self.payment_required():
            tickets = []
            for name, date_of_birth in self.unconfirmed_details:
                ticket = Ticket(name=name, date_of_birth=date_of_birth)
                tickets.append(ticket)
            return tickets
        else:
            return self.tickets.all()

    def ticket_details(self):
        return [ticket.details() for ticket in self.all_tickets()]