diff --git a/pinaxcon/registrasion/admin.py b/pinaxcon/registrasion/admin.py index 1229ed9..831b3e7 100644 --- a/pinaxcon/registrasion/admin.py +++ b/pinaxcon/registrasion/admin.py @@ -7,7 +7,3 @@ from django.utils.translation import ugettext_lazy as _ class UserProfileAdmin(admin.ModelAdmin): model = models.AttendeeProfile list_display = ("name", "company", "name_per_invoice") - -@admin.register(models.DynamicValues) -class DynamicValuesAdmin(admin.ModelAdmin): - pass diff --git a/pinaxcon/registrasion/management/__init__.py b/pinaxcon/registrasion/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pinaxcon/registrasion/management/commands/__init__.py b/pinaxcon/registrasion/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pinaxcon/registrasion/management/commands/populate_inventory.py b/pinaxcon/registrasion/management/commands/populate_inventory.py new file mode 100644 index 0000000..a0bffcc --- /dev/null +++ b/pinaxcon/registrasion/management/commands/populate_inventory.py @@ -0,0 +1,417 @@ +from collections import namedtuple +from datetime import datetime +from datetime import timedelta +from decimal import Decimal +from django.contrib.auth.models import Group +from django.core.exceptions import ObjectDoesNotExist +from django.core.management.base import BaseCommand, CommandError + +from registrasion.models import inventory as inv +from registrasion.models import conditions as cond +from symposion import proposals + +class Command(BaseCommand): + help = 'Populates the inventory with the NBPy2017 inventory model' + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + + kinds = [] + for i in ("talk", ): + kinds.append(proposals.models.ProposalKind.objects.get(name=i)) + self.main_conference_proposals = kinds + + self.populate_groups() + self.populate_inventory() + self.populate_restrictions() + self.populate_discounts() + + def populate_groups(self): + self.group_team = self.find_or_make( + Group, + ("name", ), + name="Conference organisers", + ) + self.group_volunteers = self.find_or_make( + Group, + ("name", ), + name="Conference volunteers", + ) + self.group_unpublish = self.find_or_make( + Group, + ("name", ), + name="Can see unpublished products", + ) + + def populate_inventory(self): + # Categories + + self.ticket = self.find_or_make( + inv.Category, + ("name",), + name="Ticket", + description="Each type of ticket has different included products. " + "For details of what products are included, see our " + "ticket sales page.", + required = True, + render_type=inv.Category.RENDER_TYPE_RADIO, + limit_per_user=1, + order=1, + ) + self.t_shirt = self.find_or_make( + inv.Category, + ("name",), + name="T-Shirt", + description="Commemorative conference t-shirts, featuring secret " + "North Bay Python 2017 artwork. Details of sizing and " + "manufacturer are on our " + "t-shirts page", + required = False, + render_type=inv.Category.RENDER_TYPE_ITEM_QUANTITY, + order=40, + ) + self.extras = self.find_or_make( + inv.Category, + ("name",), + name="Extras", + description="Other items that can improve your conference " + "experience.", + required = False, + render_type=inv.Category.RENDER_TYPE_QUANTITY, + order=60, + ) + + # Tickets + + self.ticket_ind_sponsor = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Individual Sponsor", + price=Decimal("500.00"), + reservation_duration=hours(24), + order=1, + ) + self.ticket_corporate = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Corporate", + price=Decimal("200.00"), + reservation_duration=hours(24), + order=10, + ) + self.ticket_supporter = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Individual Supporter", + price=Decimal("100.00"), + reservation_duration=hours(24), + order=20, + ) + self.ticket_unaffiliated = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Unaffiliated Individual", + price=Decimal("50.00"), + reservation_duration=hours(24), + order=30, + ) + self.ticket_speaker = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Speaker", + price=Decimal("00.00"), + reservation_duration=hours(24), + order=50, + ) + self.ticket_media = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Media", + price=Decimal("00.00"), + reservation_duration=hours(24), + order=60, + ) + self.ticket_sponsor = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Sponsor", + price=Decimal("00.00"), + reservation_duration=hours(24), + order=70, + ) + self.ticket_team = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Conference Organiser", + price=Decimal("00.00"), + reservation_duration=hours(24), + order=80, + ) + self.ticket_volunteer = self.find_or_make( + inv.Product, + ("name", "category",), + category=self.ticket, + name="Conference Volunteer", + price=Decimal("00.00"), + reservation_duration=hours(24), + order=90, + ) + + # Shirts + ShirtGroup = namedtuple("ShirtGroup", ("prefix", "sizes")) + shirt_names = { + "mens": ShirtGroup( + "Men's/Straight Cut Size", + ("S", "M", "L", "XL", "2XL", "3XL", "5XL"), + ), + "womens_classic": ShirtGroup( + "Women's Classic Fit", + ("XS", "S", "M", "L", "XL", "2XL", "3XL"), + ), + "womens_semi": ShirtGroup( + "Women's Semi-Fitted", + ("S", "M", "L", "XL", "2XL", "3XL"), + ), + } + + self.shirts = {} + order = 0 + for name, group in shirt_names.items(): + self.shirts[name] = {} + prefix = group.prefix + for size in group.sizes: + product_name = "%s %s" % (prefix, size) + order += 10 + self.shirts[name][size] = self.find_or_make( + inv.Product, + ("name", "category",), + name=product_name, + category=self.t_shirt, + price=Decimal("30.00"), + reservation_duration=hours(1), + order=order, + ) + + def populate_restrictions(self): + + # Hide the products that will eventually need a voucher + hide_voucher_products = self.find_or_make( + cond.GroupMemberFlag, + ("description", ), + description="Can see hidden products", + condition=cond.FlagBase.ENABLE_IF_TRUE, + ) + hide_voucher_products.group.set([self.group_unpublish]) + hide_voucher_products.products.set([ + self.ticket_media, + self.ticket_sponsor, + ]) + + # Set limits. + public_ticket_cap = self.find_or_make( + cond.TimeOrStockLimitFlag, + ("description", ), + description="Public ticket cap", + condition=cond.FlagBase.DISABLE_IF_FALSE, + limit=350, + ) + public_ticket_cap.products.set([ + self.ticket_ind_sponsor, + self.ticket_corporate, + self.ticket_supporter, + self.ticket_unaffiliated, + ]) + + non_public_ticket_cap = self.find_or_make( + cond.TimeOrStockLimitFlag, + ("description", ), + description="Non-public ticket cap", + condition=cond.FlagBase.DISABLE_IF_FALSE, + limit=200, + ) + non_public_ticket_cap.products.set([ + self.ticket_speaker, + self.ticket_sponsor, + self.ticket_media, + self.ticket_team, + self.ticket_volunteer, + ]) + + # Volunteer tickets are for volunteers only + volunteers = self.find_or_make( + cond.GroupMemberFlag, + ("description", ), + description="Volunteer tickets", + condition=cond.FlagBase.ENABLE_IF_TRUE, + ) + volunteers.group.set([self.group_volunteers]) + volunteers.products.set([ + self.ticket_volunteer, + ]) + + # Team tickets are for team members only + team = self.find_or_make( + cond.GroupMemberFlag, + ("description", ), + description="Team tickets", + condition=cond.FlagBase.ENABLE_IF_TRUE, + ) + team.group.set([self.group_team]) + team.products.set([ + self.ticket_team, + ]) + + # Speaker tickets are for primary speakers only + speaker_tickets = self.find_or_make( + cond.SpeakerFlag, + ("description", ), + description="Speaker tickets", + condition=cond.FlagBase.ENABLE_IF_TRUE, + is_presenter=True, + is_copresenter=False, + ) + speaker_tickets.proposal_kind.set(self.main_conference_proposals) + speaker_tickets.products.set([self.ticket_speaker, ]) + + def populate_discounts(self): + + def add_early_birds(discount): + self.find_or_make( + cond.DiscountForProduct, + ("discount", "product"), + discount=discount, + product=self.ticket_ind_sponsor, + price=Decimal("50.00"), + quantity=1, # Per user + ) + self.find_or_make( + cond.DiscountForProduct, + ("discount", "product"), + discount=discount, + product=self.ticket_corporate, + price=Decimal("20.00"), + quantity=1, # Per user + ) + self.find_or_make( + cond.DiscountForProduct, + ("discount", "product"), + discount=discount, + product=self.ticket_supporter, + price=Decimal("20.00"), + quantity=1, # Per user + ) + self.find_or_make( + cond.DiscountForProduct, + ("discount", "product"), + discount=discount, + product=self.ticket_unaffiliated, + price=Decimal("25.00"), + quantity=1, # Per user + ) + + def free_category(parent_discount, category, quantity=1): + self.find_or_make( + cond.DiscountForCategory, + ("discount", "category",), + discount=parent_discount, + category=category, + percentage=Decimal("100.00"), + quantity=quantity, + ) + + # Early Bird Discount (general public) + early_bird = self.find_or_make( + cond.TimeOrStockLimitDiscount, + ("description", ), + description="Early Bird", + end_time=datetime(year=2017, month=10, day=20), + limit=100, # Across all users + ) + add_early_birds(early_bird) + + # Early bird rates for speakers + speaker_ticket_discounts = self.find_or_make( + cond.SpeakerDiscount, + ("description", ), + description="Speaker Ticket Discount", + is_presenter=True, + is_copresenter=True, + ) + speaker_ticket_discounts.proposal_kind.set( + self.main_conference_proposals, + ) + add_early_birds(speaker_ticket_discounts) + + # Professional-Like ticket inclusions + ticket_prolike_inclusions = self.find_or_make( + cond.IncludedProductDiscount, + ("description", ), + description="Complimentary for ticket holder (Supporter-level and above)", + ) + ticket_prolike_inclusions.enabling_products.set([ + self.ticket_ind_sponsor, + self.ticket_corporate, + self.ticket_supporter, + self.ticket_sponsor, + self.ticket_speaker, + ]) + free_category(ticket_prolike_inclusions, self.t_shirt) + + # Team & volunteer ticket inclusions + ticket_staff_inclusions = self.find_or_make( + cond.IncludedProductDiscount, + ("description", ), + description="Complimentary for ticket holder (staff/volunteer)", + ) + ticket_staff_inclusions.enabling_products.set([ + self.ticket_team, + self.ticket_volunteer, + ]) + + # Team & volunteer t-shirts, regardless of ticket type + staff_t_shirts = self.find_or_make( + cond.GroupMemberDiscount, + ("description", ), + description="T-shirts complimentary for staff and volunteers", + ) + staff_t_shirts.group.set([ + self.group_team, + self.group_volunteers, + ]) + free_category(staff_t_shirts, self.t_shirt, quantity=2) + + def find_or_make(self, model, search_keys, **k): + ''' Either makes or finds an object of type _model_, with the given + kwargs. + + Arguments: + search_keys ([str, ...]): A sequence of keys that are used to search + for an existing version in the database. The remaining arguments are + only used when creating a new object. + ''' + + try: + keys = dict((key, k[key]) for key in search_keys) + a = model.objects.get(**keys) + self.stdout.write("FOUND : " + str(keys)) + model.objects.filter(id=a.id).update(**k) + a.refresh_from_db() + return a + except ObjectDoesNotExist: + a = model.objects.create(**k) + self.stdout.write("CREATED: " + str(k)) + return a + + +def hours(n): + return timedelta(hours=n) diff --git a/pinaxcon/registrasion/migrations/0003_auto_20171002_1719.py b/pinaxcon/registrasion/migrations/0003_auto_20171002_1719.py new file mode 100644 index 0000000..84280d6 --- /dev/null +++ b/pinaxcon/registrasion/migrations/0003_auto_20171002_1719.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-10-03 00:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinaxcon_registrasion', '0002_auto_20161005_1823'), + ] + + operations = [ + migrations.RemoveField( + model_name='attendeeprofile', + name='db_defined_values', + ), + migrations.RemoveField( + model_name='attendeeprofile', + name='dietary_requirements', + ), + migrations.RemoveField( + model_name='attendeeprofile', + name='free_text_1', + ), + migrations.RemoveField( + model_name='attendeeprofile', + name='free_text_2', + ), + migrations.RemoveField( + model_name='attendeeprofile', + name='of_legal_age', + ), + migrations.AddField( + model_name='attendeeprofile', + name='address_line_1', + field=models.CharField(blank=True, help_text=b'This address, if provided, will appear on your receipt.', max_length=1024, verbose_name=b'Address line 1'), + ), + migrations.AddField( + model_name='attendeeprofile', + name='address_line_2', + field=models.CharField(blank=True, max_length=1024, verbose_name=b'Address line 2'), + ), + migrations.AddField( + model_name='attendeeprofile', + name='address_postcode', + field=models.CharField(blank=True, max_length=1024, verbose_name=b'Postal/Zip code'), + ), + migrations.AddField( + model_name='attendeeprofile', + name='address_suburb', + field=models.CharField(blank=True, max_length=1024, verbose_name=b'City/Town/Suburb'), + ), + migrations.AddField( + model_name='attendeeprofile', + name='country', + field=django_countries.fields.CountryField(default=b'US', max_length=2), + ), + migrations.AddField( + model_name='attendeeprofile', + name='dietary_restrictions', + field=models.CharField(blank=True, max_length=256, verbose_name=b'Food allergies, intolerances, or dietary restrictions'), + ), + migrations.AddField( + model_name='attendeeprofile', + name='newsletter', + field=models.BooleanField(default=False, help_text=b'Select to be subscribed to the low-volume North Bay Python announcements newsletter', verbose_name=b'Subscribe to North Bay Python newsletter'), + preserve_default=False, + ), + migrations.AddField( + model_name='attendeeprofile', + name='state', + field=models.CharField(blank=True, max_length=256, verbose_name=b'State/Territory/Province'), + ), + migrations.AlterField( + model_name='attendeeprofile', + name='accessibility_requirements', + field=models.CharField(blank=True, max_length=256, verbose_name=b'Accessibility-related requirements'), + ), + migrations.AlterField( + model_name='attendeeprofile', + name='company', + field=models.CharField(blank=True, help_text=b"The name of your company, as you'd like it on your badge and receipt", max_length=64), + ), + migrations.AlterField( + model_name='attendeeprofile', + name='gender', + field=models.CharField(blank=True, help_text=b'Gender data will only be used for demographic purposes.', max_length=64), + ), + migrations.AlterField( + model_name='attendeeprofile', + name='name_per_invoice', + field=models.CharField(blank=True, help_text=b"If your legal name is different to the name on your badge, fill this in, and we'll put it on your receipt. Otherwise, leave it blank.", max_length=256, verbose_name=b'Your legal name (for your receipt)'), + ), + migrations.DeleteModel( + name='DemoPayment', + ), + migrations.DeleteModel( + name='DynamicValues', + ), + ] diff --git a/pinaxcon/registrasion/models.py b/pinaxcon/registrasion/models.py index 77952fe..c8a2a1d 100644 --- a/pinaxcon/registrasion/models.py +++ b/pinaxcon/registrasion/models.py @@ -1,18 +1,10 @@ +from django.core.exceptions import ValidationError from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django_countries.fields import CountryField from registrasion import models as rego -@python_2_unicode_compatible -class DynamicValues(models.Model): - - name = models.CharField(max_length=64) - value = models.IntegerField() - - def __str__(self): - return "%s - %d" % (self.name, self.value) - - class AttendeeProfile(rego.AttendeeProfileBase): @classmethod @@ -22,11 +14,49 @@ class AttendeeProfile(rego.AttendeeProfileBase): return "name" def invoice_recipient(self): + + lines = [ + self.name_per_invoice, + ] + if self.company: - base = "\n%(company)s\nAttention: %(name_per_invoice)s" - else: - base = "%(name_per_invoice)s" - return base % self.__dict__ + lines.append("C/- " + self.company) + + if self.address_line_1: + lines.append(self.address_line_1) + + if self.address_line_2: + lines.append(self.address_line_2) + + if self.address_suburb or self.address_postcode: + lines.append("%s %s" % ( + self.address_suburb or "", + self.address_postcode or "", + )) + + if self.state: + lines.append(self.state) + + if self.country: + lines.append(self.country.name) + + return "\n".join(unicode(line) for line in lines) + + def clean(self): + errors = [] + if self.country == "US" and not self.state: + errors.append( + ("state", "US-based attendees must list their state"), + ) + + if self.address_line_2 and not self.address_line_1: + errors.append(( + "address_line_1", + "Please fill in line 1 before filling line 2", + )) + + if errors: + raise ValidationError(dict(errors)) def save(self): if not self.name_per_invoice: @@ -42,56 +72,68 @@ class AttendeeProfile(rego.AttendeeProfileBase): company = models.CharField( max_length=64, - help_text="The name of your company, as you'd like it on your badge", - blank=True, - ) - free_text_1 = models.CharField( - max_length=64, - verbose_name="Free text line 1", - help_text="A line of free text that will appear on your badge. Use " - "this for your Twitter handle, IRC nick, your preferred " - "pronouns or anything else you'd like people to see on " - "your badge.", - blank=True, - ) - free_text_2 = models.CharField( - max_length=64, - verbose_name="Free text line 2", + help_text="The name of your company, as you'd like it on your badge and receipt", blank=True, ) - # Other important Information name_per_invoice = models.CharField( - verbose_name="Your legal name (for invoicing purposes)", - max_length=64, + verbose_name="Your legal name (for your receipt)", + max_length=256, help_text="If your legal name is different to the name on your badge, " - "fill this in, and we'll put it on your invoice. Otherwise, " + "fill this in, and we'll put it on your receipt. Otherwise, " "leave it blank.", blank=True, ) - of_legal_age = models.BooleanField( - default=False, - verbose_name="18+?", + + address_line_1 = models.CharField( + verbose_name="Address line 1", + help_text="This address, if provided, will appear on your receipt.", + max_length=1024, blank=True, ) - dietary_requirements = models.CharField( + address_line_2 = models.CharField( + verbose_name="Address line 2", + max_length=1024, + blank=True, + ) + address_suburb = models.CharField( + verbose_name="City/Town/Suburb", + max_length=1024, + blank=True, + ) + address_postcode = models.CharField( + verbose_name="Postal/Zip code", + max_length=1024, + blank=True, + ) + country = CountryField( + default="US", + ) + state = models.CharField( + max_length=256, + verbose_name="State/Territory/Province", + blank=True, + ) + + dietary_restrictions = models.CharField( + verbose_name="Food allergies, intolerances, or dietary restrictions", max_length=256, blank=True, ) accessibility_requirements = models.CharField( + verbose_name="Accessibility-related requirements", max_length=256, blank=True, ) gender = models.CharField( + help_text="Gender data will only be used for demographic purposes.", max_length=64, blank=True, ) - db_defined_values = models.ManyToManyField( - DynamicValues + + newsletter = models.BooleanField( + verbose_name="Subscribe to North Bay Python newsletter", + help_text="Select to be subscribed to the low-volume North Bay Python " + "announcements newsletter", + blank=True, ) - - -class DemoPayment(rego.PaymentBase): - ''' A subclass of PaymentBase for use in our demo payments function. ''' - - pass # No custom features here, but yours could be here. diff --git a/pinaxcon/settings.py b/pinaxcon/settings.py index 0b031fa..e6c6db7 100644 --- a/pinaxcon/settings.py +++ b/pinaxcon/settings.py @@ -289,6 +289,7 @@ PINAX_BOXES_HOOKSET = "pinaxcon.hooks.PinaxBoxesHookSet" PINAX_STRIPE_PUBLIC_KEY = os.environ.get("STRIPE_PUBLIC_KEY", "your test public key") PINAX_STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "your test secret key") +TUOKCEHC_BASE_URL = os.environ.get("TUOKCEHC_BASE_URL", None) PINAX_STRIPE_SEND_EMAIL_RECEIPTS = False SYMPOSION_SPEAKER_MODEL = "pinaxcon.proposals.models.ConferenceSpeaker" @@ -305,7 +306,7 @@ ATTENDEE_PROFILE_MODEL = "pinaxcon.registrasion.models.AttendeeProfile" TICKET_PRODUCT_CATEGORY = 1 -INVOICE_CURRENCY = "AUD" +INVOICE_CURRENCY = "USD" # Use nose to run all tests TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' @@ -320,5 +321,8 @@ NOSE_ARGS = [ MARKDOWN_DEUX_STYLES = { "default": { "safe_mode": False, + "extras": { + "tables": 1, + } }, } diff --git a/pinaxcon/templates/dashboard.html b/pinaxcon/templates/dashboard.html index 6e337f5..7696abb 100644 --- a/pinaxcon/templates/dashboard.html +++ b/pinaxcon/templates/dashboard.html @@ -30,6 +30,8 @@ + {% include "registrasion/dashboard_widget.html" %} +
Direct inquiries to spam@northbaypython.org
+North Bay Python is run by North Bay and Bay Area locals, as a member project of Software Freedom Conservancy, a 501(c)(3) public charity registered in New York.
+ + Mailing Address + + Software Freedom Conservancy, Inc.NOTICE: The below statement is automatically generated, and will be voided if you amend your registration before payment, or if discounts or products contained in the statement become unavailable. The items and discounts are only reserved until the invoice due time.
+ +{% url "invoice_access" invoice.user.attendee.access_code as access_url %} +{% url "invoice" invoice.id invoice.user.attendee.access_code as invoice_url %} + +You can send the following links to your accounts department to pay for your registration:
+ +To buy a ticket, create an account, and go to the dashboard. If you've already bought a ticket, you can check out our information on where to stay if you want to come up for the weekend, and how to get here.
+ +Early Bird discounts are available for the first 100 tickets sold, or until October 20, whichever comes first. T-shirts are only available for tickets bought before November 7.
+ +For company employees, and individuals who can legitimately claim the cost of attending the conference as a business expense or a tax deduction.
+ +Includes a free t-shirt, and recognition of your affiliation on your conference badge and on the conference supporters list. Group discounts are available for organizations that buy 5 or more tickets.
+ +For individuals who want to financially contribute to the success of the conference.
+ +This ticket includes a free t-shirt, and recognition of your Free and Open Source Software, hobby, or nonprofit project on your conference badge.
+ +For students, hobbyists, and unemployed/underemployed people who are coming to North Bay Python at their own expense.
+ +The cheapest ticket we can offer. You can add a t-shirt for $30.
+ + +This ticket includes all of the benefits of a Corporate ticket, but we’ll also give the ticket holder special thanks during our conference plenary sessions. You can also provide us with a promotional item to put in each attendee’s swag bag.
+ +This ticket is for individuals who want to sponsor the conference. For company-level sponsorships, please see our sponsorships page.
+ + +For companies sending multiple attendees, you can get a 10% discount off the regular price on purchases of 5 tickets or more.
+ +To claim, buy your first four tickets, and send us an email with the names and receipt numbers for those attendees. We’ll send you vouchers for a discount on further tickets.
+ + ++ | Unaffiliated Individual | Individual Supporter | Corporate | Individual Sponsor |
---|---|---|---|---|
Regular Price | +$50 | $100 | $200 | $500 + |
Early Bird | +$25 | $80 | $180 | $450 |
Group Discount | +- | - | $180/ticket for 5+ tickets | - |
Conference access | +Yes | Yes | Yes | Yes |
Morning refreshments | +TBA | TBA | TBA | TBA |
Free Lunch | +No | No | No | No |
T-Shirt | +$30 each | 1 freeExtras $30 each | 1 freeExtras $30 each | 1 freeExtras $30 each |
Affiliation on your badge | +No | Personal projects only | Yes | Yes |
Supporter recognition | +None | For you | For you and your company | Top billing for you and your company or project |
Sponsor benefits | +No | No | No | Yes |
If you can’t afford to attend on these prices, please email spam@northbaypython.org – we’ll enthusiastically waive ticket fees for people who need it.
+ +Two days of high-caliber talks about Python, and meeting new Pythonistas at North Bay Python.
+ +In order to keep ticket costs as low as possible, we won’t be catering lunch this year.
+ +To make up for it, we’ve located our conference right in the middle of Historic Downtown Petaluma’s restaurant district. You can find everything from market delis and barbecue, through to Michelin-rated restaurants, all within 5 minutes walk. You’ll get a better lunch than we’d ever be able to cater, for much less. We'll have a locals' guide to Petaluma to help you find places to eat.
+ +If budget permits, or if we find a sponsor, we’ll provide coffee, tea, hot chocolate, and some light snacks in the morning before proceedings kick off on both days.
+ +We’ll be designing a collectible North Bay Python t-shirt for you to pick up at the conference, and they’ll be available in a variety of cuts and colors. Each t-shirt costs $30, and for supporter, corporate, and sponsor ticket holders, you’ll get your first t-shirt free!
+ +T-shirts are available only for tickets purchased by Tuesday 7 November.
+ + +Every attendee gets their very own lanyard and a badge with their name on it. As a paying ticket holder, you’ll get your project’s name (supporter and above) or company name (corporate or sponsor levels only) on your badge, just below your name.
+ +On our website, we’ll have a list of our conference supporters. You can choose to have your name on that list.
+ +For our corporate and sponsor ticket holders, we’ll also include your company name as part of those thanks.
+ +Sponsor tickets come with sponsor benefits. To find out more, see our Sponsors page.
{% endblock %} diff --git a/pinaxcon/templates/static_pages/attend/hotels.html b/pinaxcon/templates/static_pages/attend/hotels.html index 5a67583..78d90d3 100644 --- a/pinaxcon/templates/static_pages/attend/hotels.html +++ b/pinaxcon/templates/static_pages/attend/hotels.html @@ -9,54 +9,110 @@ {% block body_class %}attend{% endblock %} {% block lede %} - Lede + If you're coming from out of town, we'd love you to stay the night! We've made arrangements with the best hotels in Petaluma, with exclusive rates for North Bay Python attendees. {% endblock %} {% block content %} -Content +If you're driving up, Downtown Petaluma is at exit 472A on Highway 101, 35 miles north of the Golden Gate Bridge.
-If you're driving up, Downtown Petaluma is at exit 472A on Highway 101, 35 miles north of the Golden Gate Bridge. All parking is free in Petaluma, including in the undercover garages at Keller St and Theatre Square. Both garages are in short walking distance of the Mystic.
+All parking is free in Petaluma, however near the Mystic, street-level parking is time-limited. All-day parking is available at street level west of 5th St (towards 6th St), and at the the undercover garages at Keller St and at Theatre Square. Both garages are in short walking distance of the Mystic.
-Public transit to Petaluma is not great. You can take the 101 bus operated by Golden Gate Transit from downtown San Francisco, or south from Santa Rosa. Depending on sponsorship, we hope to run a free shuttle with BART and Caltrain connections for people from further out of town.
+Public transit to Petaluma is currently not great.
-If you're coming from out of the area, you may want to consider Sonoma County Airport (STS). STS is 30 minutes out of Petaluma, and has nonstop flights to most major west coast cities. If you can't make it to STS, you can also try San Francisco (SFO) or Oakland (OAK) international airports.
+You can take the 101 bus operated by Golden Gate Transit from downtown San Francisco, or south from Santa Rosa. Depending on sponsorship, we hope to run a free shuttle with BART and Caltrain connections for people from further out of town.
+ +SMART, the new train service that runs along the 101 corridor, recently started operations. SMART is not suitable for getting to North Bay Python if you travel on weekends, as the first train leaves after proceedings start. SMART may be a better option than taking the bus between San Rafael and Petaluma if you travel up on weekdays.
+ + +Petaluma is within driving distance of all Bay Area Airports, and each airport has varying levels of public transit links to Petaluma.
+ +STS is 30 minutes out of Petaluma, and has nonstop flights to most major west coast cities on Alaska, United, and American.
+ +On weekdays and weekend afternoons, SMART train runs from STS to Downtown Petaluma Station, 1/4mi away from the North Bay Python venue. STS is also serviced by the Sonoma County Airport Express bus.
+ +If you can't make it to STS, you can also try San Francisco (SFO) or Oakland (OAK) international airports. These have many more flights than STS, but are twice the distance away, and are subject to more highway traffic between the airport and Petaluma.
+ +Transfers to Petaluma are available through the Sonoma County Airport Express.
+ +If you're planning on renting a car, San Jose (SJC) or Sacramento (SMF) are both two hours drive away.
+ +If you happen to have an aircraft of your own, Petaluma Municipal Airport is 3 miles down the road.
diff --git a/pinaxcon/templates/static_pages/homepage.html b/pinaxcon/templates/static_pages/homepage.html index 537abd6..a00be3c 100644 --- a/pinaxcon/templates/static_pages/homepage.html +++ b/pinaxcon/templates/static_pages/homepage.html @@ -28,7 +28,7 @@