diff --git a/alembic/versions/38bc5fa8b6d6_added_payment_model.py b/alembic/versions/38bc5fa8b6d6_added_payment_model.py
new file mode 100644
index 0000000..fed865c
--- /dev/null
+++ b/alembic/versions/38bc5fa8b6d6_added_payment_model.py
@@ -0,0 +1,53 @@
+"""Added payment model
+
+Revision ID: 38bc5fa8b6d6
+Revises: 155bdd6d893d
+Create Date: 2013-12-23 19:35:00.124877
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '38bc5fa8b6d6'
+down_revision = '155bdd6d893d'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('payment_gateway_log',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(), nullable=False),
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('event_id', sa.Integer(), nullable=False),
+ sa.Column('status', sa.Integer(), nullable=False),
+ sa.Column('order_no', sa.Unicode(20), nullable=False),
+ sa.Column('server_response', sa.UnicodeText(), nullable=True),
+ sa.Column('start_datetime', sa.DateTime(), nullable=False),
+ sa.Column('end_datetime', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['event_id'], ['event.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+
+ op.add_column(u'event', sa.Column('currency', sa.Unicode(length=3), nullable=True))
+ op.add_column(u'event', sa.Column('payment_credentials', sa.Unicode(length=100), nullable=True))
+ op.add_column(u'event', sa.Column('payment_service', sa.Unicode(length=100), nullable=True))
+
+ op.add_column(u'participant', sa.Column('purchased_ticket', sa.Boolean(), nullable=False, server_default=sa.text(u"'f'")))
+ op.alter_column(u'participant', 'purchased_ticket', server_default=None)
+ conn = op.get_bind()
+ conn.execute("update participant set purchased_ticket='t' where status=2")
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column(u'event', 'payment_service')
+ op.drop_column(u'event', 'payment_credentials')
+ op.drop_column(u'event', 'currency')
+ op.drop_column(u'participant', 'purchased_ticket')
+ op.drop_table('payment_gateway_log')
+ ### end Alembic commands ###
diff --git a/hacknight/forms/event.py b/hacknight/forms/event.py
index 07cc7f8..0dfb1c4 100644
--- a/hacknight/forms/event.py
+++ b/hacknight/forms/event.py
@@ -7,7 +7,7 @@
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from baseframe.forms import Form, RichTextField, DateTimeField, ValidName
from baseframe.forms.sqlalchemy import AvailableName
-from hacknight.models import Venue, EVENT_STATUS, SYNC_SERVICE
+from hacknight.models import Venue, EVENT_STATUS, SYNC_SERVICE, PAYMENT_GATEWAY
__all__ = ['EventForm', 'ConfirmWithdrawForm', 'SendEmailForm']
@@ -27,10 +27,25 @@
SYNC_CHOICES = [
# Empty value for opting out.
- (u"", u""),
+ (u"N/A", u""),
(SYNC_SERVICE.DOATTEND, u"DoAttend"),
]
+PAYMENT_GATEWAY_CHOICES = [
+ # Empty value for opting out.
+ (u"N/A", u""),
+ (PAYMENT_GATEWAY.EXPLARA, u"Explara"),
+]
+
+CURRENCY_CHOICES = [
+ (u"INR", u"INR - India Rupee"),
+ (u"USD", u"USD - United States Dollar"),
+ (u"GBP", u"GBP - Great Britain Pound"),
+ (u"EUR", u"EUR - Euro"),
+ (u"PHP", u"PHP - Philippines Peso"),
+ (u"ZAR", u"ZAR - South Africa Rand"),
+]
+
class EventForm(Form):
title = wtforms.TextField("Title", description="Name of the Event", validators=[wtforms.validators.Required(), wtforms.validators.NoneOf(values=["new"]), wtforms.validators.length(max=250)])
@@ -61,10 +76,13 @@ class EventForm(Form):
maximum_participants = wtforms.IntegerField("Venue capacity", description="The number of people this venue can accommodate.", default=50, validators=[wtforms.validators.Required()])
website = wtforms.fields.html5.URLField("Website", description="Related Website (Optional)", validators=[wtforms.validators.Optional(), wtforms.validators.length(max=250), wtforms.validators.URL()])
status = wtforms.SelectField("Event status", description="Current status of this hacknight", coerce=int, choices=STATUS_CHOICES)
- sync_service = wtforms.SelectField("Sync service name", description="Name of the ticket sync service like doattend", choices= SYNC_CHOICES, validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
+ sync_service = wtforms.SelectField("Sync service name", description="Name of the ticket sync service like doattend", choices=SYNC_CHOICES, validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
sync_eventsid = wtforms.TextField("Sync event ID", description="Sync events id like DoAttend event ID. More than one event ID is allowed separated by ,.", validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
sync_credentials = wtforms.TextField("Sync credentials", description="Sync credentials like API Key for the event", validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
-
+ payment_service = wtforms.SelectField("Payment service", description="Name of the payment gateway service", choices=PAYMENT_GATEWAY_CHOICES, validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
+ payment_credentials = wtforms.TextField("Payment gateway credentials", description="Payment gateway credentials like API Key", validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
+ currency = wtforms.SelectField("Currency", description="Currency in which participant should pay", choices=CURRENCY_CHOICES, validators=[wtforms.validators.Optional(), wtforms.validators.length(max=100)])
+
def validate_end_datetime(self, field):
if field.data < self.start_datetime.data:
raise wtforms.ValidationError(u"Your event can’t end before it starts.")
@@ -85,6 +103,21 @@ def validate_sync_eventsid(self, field):
if events_id:
field.data = ",".join(events_id)
+ def validate_payment_credentials(self, field):
+ if self.payment_service.data == PAYMENT_GATEWAY.EXPLARA:
+ if len(field.data) < 10:
+ raise wtforms.ValidationError(u"Payment credentials must be more than 10 characters")
+
+ def validate_ticket_price(self, field):
+ if self.payment_service.data == PAYMENT_GATEWAY.EXPLARA:
+ try:
+ data = field.data.strip()
+ if data[0] == '-':
+ raise wtforms.ValidationError(u"Event price must be positive number")
+ float(data)
+ except ValueError:
+ raise wtforms.ValidationError(u"Event price must be positive number. E.G: 500.00")
+
class EmailEventParticipantsForm(Form):
pending_message = RichTextField("Pending Message", description="Message to be sent for pending participants. '*|FULLNAME|*' will be replaced with user's fullname.", validators=[wtforms.validators.Optional()], tinymce_options = {'convert_urls': False, 'remove_script_host': False})
diff --git a/hacknight/forms/participant.py b/hacknight/forms/participant.py
index a6eef53..ce23166 100644
--- a/hacknight/forms/participant.py
+++ b/hacknight/forms/participant.py
@@ -3,8 +3,10 @@
import wtforms
import wtforms.fields.html5
from baseframe.forms import Form, RichTextField
+from baseframe.staticdata import country_codes
-__all__ = ['ParticipantForm']
+
+__all__ = ['ParticipantForm', 'ExplaraForm']
class ParticipantForm(Form):
@@ -31,3 +33,21 @@ class ParticipantForm(Form):
validators=[wtforms.validators.Optional(), wtforms.validators.length(max=1200)])
skill_level = wtforms.RadioField("Skill Level", description="What is your skill level?",
choices=skill_levels)
+
+
+class ExplaraForm(Form):
+ name = wtforms.TextField("Name", description="Name of the purchaser",
+ validators=[wtforms.validators.Required(), wtforms.validators.length(max=200)])
+ email = wtforms.fields.html5.EmailField("Email", description="Email address",
+ validators=[wtforms.validators.Required(), wtforms.validators.length(max=200)])
+ phone_no = wtforms.TextField("Telephone No", description="Telephone No",
+ validators=[wtforms.validators.Required(), wtforms.validators.length(max=15)])
+ country = wtforms.SelectField("Country", description="Country", choices=country_codes, validators=[wtforms.validators.Required()])
+ """ Longest state name
+ Taumatawhakatangihangakoauauotamateahaumaitawhitiurehaeaturipuk-
+ akapikimaungahoronukupokaiwhenuakitanatahu in NewZeland.
+ """
+ state = wtforms.TextField("State", description="State", validators=[wtforms.validators.Required(), wtforms.validators.length(max=110)])
+ city = wtforms.TextField("City", description="City", validators=[wtforms.validators.Required(), wtforms.validators.length(max=110)])
+ address = wtforms.TextField("Address", description="Address", validators=[wtforms.validators.Required(), wtforms.validators.length(max=1000)])
+ zip_code = wtforms.TextField("Zip code", description="Zip code", validators=[wtforms.validators.Required(), wtforms.validators.length(max=6)])
diff --git a/hacknight/models/__init__.py b/hacknight/models/__init__.py
index daebcb6..8527aab 100644
--- a/hacknight/models/__init__.py
+++ b/hacknight/models/__init__.py
@@ -12,3 +12,4 @@
from hacknight.models.project import *
from hacknight.models.participant import *
from hacknight.models.sponsor import *
+from hacknight.models.log import *
diff --git a/hacknight/models/event.py b/hacknight/models/event.py
index 37093ff..2b2895f 100644
--- a/hacknight/models/event.py
+++ b/hacknight/models/event.py
@@ -11,7 +11,7 @@
from hacknight.models import db, BaseNameMixin, BaseScopedNameMixin, BaseMixin
-__all__ = ['Profile', 'Event', 'EVENT_STATUS', 'SYNC_SERVICE', 'PROFILE_TYPE', 'EventRedirect']
+__all__ = ['Profile', 'Event', 'EVENT_STATUS', 'SYNC_SERVICE', 'PROFILE_TYPE', 'EventRedirect', 'PAYMENT_GATEWAY']
#need to add EventTurnOut, EventPayment later
@@ -45,6 +45,10 @@ class SYNC_SERVICE:
DOATTEND = u"doattend"
+class PAYMENT_GATEWAY:
+ EXPLARA = u"Explara"
+
+
class SyncException(Exception):
pass
@@ -95,6 +99,11 @@ class Event(BaseScopedNameMixin, db.Model):
sync_credentials = db.Column(db.Unicode(100), nullable=True)
sync_eventsid = db.Column(db.Unicode(100), nullable=True)
+ # Payment gateway details
+ payment_service = db.Column(db.Unicode(100), nullable=True)
+ payment_credentials = db.Column(db.Unicode(100), nullable=True)
+ currency = db.Column(db.Unicode(3), nullable=True)
+
__table_args__ = (db.UniqueConstraint('name', 'profile_id'),)
# List of statuses which are not allowed to be displayed in index page.
@@ -112,6 +121,9 @@ def owner_is(self, user):
"""Check if a user is an owner of this event"""
return user is not None and self.profile.userid in user.user_organizations_owned_ids()
+ def has_payment_gateway(self):
+ return self.payment_service and self.payment_credentials and self.currency
+
def has_sync(self):
return self.sync_service and self.sync_credentials and self.sync_eventsid
@@ -150,6 +162,13 @@ def sync_participants(self, participants):
yield u"Sync credentials missing.\n"
yield Markup(final_msg)
+ def is_tickets_available(self):
+ if self.has_payment_gateway():
+ if self.confirmed_participants_count() < self.maximum_participants:
+ return True
+ return False
+ return False
+
def participant_is(self, user):
from hacknight.models.participant import Participant
return Participant.get(user, self) is not None
@@ -161,7 +180,7 @@ def confirmed_participant_is(self, user):
def confirmed_participants_count(self):
from hacknight.models.participant import Participant, PARTICIPANT_STATUS
- return Participant.query.filter_by(status=PARTICIPANT_STATUS.CONFIRMED, event=self).count()
+ return Participant.query.filter_by(status=PARTICIPANT_STATUS.CONFIRMED, event=self, purchased_ticket=True).count()
def permissions(self, user, inherited=None):
perms = super(Event, self).permissions(user, inherited)
@@ -173,6 +192,8 @@ def permissions(self, user, inherited=None):
perms.add('edit')
perms.add('delete')
perms.add('send-email')
+ if self.has_payment_gateway():
+ perms.add('buy-ticket')
return perms
def url_for(self, action='view', _external=False):
@@ -200,6 +221,10 @@ def url_for(self, action='view', _external=False):
return url_for('email_template_form', profile=self.profile.name, event=self.name, _external=_external)
elif action == 'sync':
return url_for('event_sync', profile=self.profile.name, event=self.name, _external=_external)
+ elif action == 'purchase_ticket':
+ return url_for('purchase_ticket_explara', profile=self.profile.name, event=self.name, _external=_external)
+ elif action == 'payment_redirect':
+ return url_for('payment_redirect_explara', profile=self.profile.name, event=self.name, _external=_external)
class EventRedirect(BaseMixin, db.Model):
diff --git a/hacknight/models/log.py b/hacknight/models/log.py
new file mode 100644
index 0000000..a3da38f
--- /dev/null
+++ b/hacknight/models/log.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+
+from hacknight.models import db, BaseMixin, User, Event
+
+__all__ = ['PaymentGatewayLog', 'TRANSACTION_STATUS', 'transaction_status']
+
+
+class TRANSACTION_STATUS:
+ PENDING = 0
+ FAILURE = 1
+ SUCCESS = 2
+
+
+transaction_status = {
+ u'success': TRANSACTION_STATUS.SUCCESS,
+ u'pending': TRANSACTION_STATUS.PENDING,
+ u'failure': TRANSACTION_STATUS.FAILURE,
+}
+
+
+class PaymentGatewayLog(BaseMixin, db.Model):
+ __tablename__ = "payment_gateway_log"
+
+ user_id = db.Column(None, db.ForeignKey('user.id'), nullable=False)
+ user = db.relationship(User)
+
+ event_id = db.Column(None, db.ForeignKey('event.id'), nullable=False)
+ event = db.relationship(Event, backref=db.backref('payment_gateway_logs'))
+
+ status = db.Column(db.Integer, default=TRANSACTION_STATUS.PENDING, nullable=False)
+ order_no = db.Column(db.Unicode(20), nullable=False)
+
+ server_response = db.Column(db.UnicodeText(), nullable=True)
+ start_datetime = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now)
+ end_datetime = db.Column(db.DateTime, nullable=True)
+
+ @classmethod
+ def get_recent_transaction(cls, user):
+ return cls.query.filter_by(user=user).order_by(PaymentGatewayLog.id.desc()).first()
diff --git a/hacknight/models/participant.py b/hacknight/models/participant.py
index e01bb33..d7ee95d 100644
--- a/hacknight/models/participant.py
+++ b/hacknight/models/participant.py
@@ -24,6 +24,7 @@ class Participant(BaseMixin, db.Model):
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
event = db.relationship(Event, backref=db.backref('participants', cascade='all, delete-orphan'))
status = db.Column(db.Integer, default=PARTICIPANT_STATUS.PENDING, nullable=False)
+ purchased_ticket = db.Column(db.Boolean, default=False, nullable=False)
mentor = db.Column(db.Boolean, default=False, nullable=False)
reason_to_join = db.Column(db.UnicodeText, default=u'', nullable=False)
email = db.Column(db.Unicode(80), default=u'', nullable=False)
@@ -46,6 +47,7 @@ def save_defaults(self):
def confirm(self):
self.status = PARTICIPANT_STATUS.CONFIRMED
+ self.purchased_ticket = True
@classmethod
def get(cls, user, event):
diff --git a/hacknight/templates/event.html b/hacknight/templates/event.html
index 81d9b4d..eedee21 100644
--- a/hacknight/templates/event.html
+++ b/hacknight/templates/event.html
@@ -8,7 +8,7 @@