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 @@
{{ p.user.fullname }} - +
{% with projects = p.user.projects_in(event) %} {%- if projects %} @@ -87,11 +87,21 @@

{{ self.title() }}

{%- elif current_participant.status == 4 -%}
  • Withdrawn
  • {%- endif -%} + + {% if workflow.can_apply() -%} + {% if event.is_tickets_available() -%} + {% if not current_participant.purchased_ticket -%} +
  • Purchase Ticket
  • + {% else %} +
  • Withdraw registration
  • + {%- endif %} + {% else %} +
  • Withdraw registration
  • + {%- endif %} + {%- endif %} {%- endif %} - {% if workflow.can_apply() -%} -
  • Withdraw registration
  • - {%- endif %} - {% endif %} + {%- endif %} + {%- if event.owner_is(g.user) %} @@ -110,7 +120,7 @@

    {{ self.title() }}

    {% endif -%} {% endif -%} - +