From f32be5a8d647d9ced35ecc11d309924fd9edd996 Mon Sep 17 00:00:00 2001 From: "@kracekumar" Date: Mon, 23 Dec 2013 19:32:41 +0530 Subject: [PATCH 01/11] Added models for payment gateway --- hacknight/forms/event.py | 10 ++++++++-- hacknight/models/__init__.py | 1 + hacknight/models/event.py | 11 ++++++++++- hacknight/views/profile.py | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/hacknight/forms/event.py b/hacknight/forms/event.py index 07cc7f8..c1f89e0 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'] @@ -31,6 +31,12 @@ (SYNC_SERVICE.DOATTEND, u"DoAttend"), ] +PAYMENT_GATEWAY_CHOICES = [ + # Empty value for opting out. + (u"", u""), + (PAYMENT_GATEWAY.EXPLARA, u"Explara"), +] + 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)]) @@ -64,7 +70,7 @@ class EventForm(Form): 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)]) - + 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.") diff --git a/hacknight/models/__init__.py b/hacknight/models/__init__.py index daebcb6..0618bd2 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.ticket import * diff --git a/hacknight/models/event.py b/hacknight/models/event.py index 37093ff..154a6b5 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. diff --git a/hacknight/views/profile.py b/hacknight/views/profile.py index fbf821a..b264855 100644 --- a/hacknight/views/profile.py +++ b/hacknight/views/profile.py @@ -46,3 +46,4 @@ def profile_edit(profile): return render_redirect(profile.url_for(), code=303) return render_form(form=form, title=u"Edit profile", submit=u"Save", cancel_url=profile.url_for(), ajax=True) + From afda6823b20e4ee8d245fb2eb0a2d610616891da Mon Sep 17 00:00:00 2001 From: "@kracekumar" Date: Tue, 24 Dec 2013 12:34:54 +0530 Subject: [PATCH 02/11] Payment gateway model --- .../38bc5fa8b6d6_added_payment_model.py | 49 +++++++++++++++++++ hacknight/models/__init__.py | 2 +- hacknight/models/participant.py | 1 + 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/38bc5fa8b6d6_added_payment_model.py diff --git a/alembic/versions/38bc5fa8b6d6_added_payment_model.py b/alembic/versions/38bc5fa8b6d6_added_payment_model.py new file mode 100644 index 0000000..4af7ceb --- /dev/null +++ b/alembic/versions/38bc5fa8b6d6_added_payment_model.py @@ -0,0 +1,49 @@ +"""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 +from coaster.sqlalchemy import JsonDict +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('server_response', JsonDict(), 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'event', sa.Column('purchased_ticket', sa.Boolean(), nullable=False, server_default=sa.text(u"'f'"))) + op.alter_column(u'event', 'purchased_ticket', server_default=None) + ### 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_table('payment_gateway_log') + ### end Alembic commands ### diff --git a/hacknight/models/__init__.py b/hacknight/models/__init__.py index 0618bd2..8527aab 100644 --- a/hacknight/models/__init__.py +++ b/hacknight/models/__init__.py @@ -12,4 +12,4 @@ from hacknight.models.project import * from hacknight.models.participant import * from hacknight.models.sponsor import * -from hacknight.models.ticket import * +from hacknight.models.log import * diff --git a/hacknight/models/participant.py b/hacknight/models/participant.py index e01bb33..06864b2 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) From 41a44917dc52d5f019d77d5afbd4a8079b61c9a4 Mon Sep 17 00:00:00 2001 From: "@kracekumar" Date: Thu, 26 Dec 2013 16:12:40 +0530 Subject: [PATCH 03/11] Added payment gateway model --- .../38bc5fa8b6d6_added_payment_model.py | 8 +- hacknight/forms/event.py | 25 ++++++ hacknight/forms/participant.py | 23 +++++- hacknight/models/event.py | 9 ++ hacknight/models/participant.py | 1 + hacknight/templates/event.html | 25 ++++-- hacknight/views/event.py | 82 ++++++++++++++++++- 7 files changed, 158 insertions(+), 15 deletions(-) diff --git a/alembic/versions/38bc5fa8b6d6_added_payment_model.py b/alembic/versions/38bc5fa8b6d6_added_payment_model.py index 4af7ceb..39b4d00 100644 --- a/alembic/versions/38bc5fa8b6d6_added_payment_model.py +++ b/alembic/versions/38bc5fa8b6d6_added_payment_model.py @@ -23,7 +23,9 @@ def upgrade(): 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('server_response', JsonDict(), nullable=True), + 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'], ), @@ -35,8 +37,8 @@ def upgrade(): 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'event', sa.Column('purchased_ticket', sa.Boolean(), nullable=False, server_default=sa.text(u"'f'"))) - op.alter_column(u'event', 'purchased_ticket', server_default=None) + 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) ### end Alembic commands ### diff --git a/hacknight/forms/event.py b/hacknight/forms/event.py index c1f89e0..cbfe1d5 100644 --- a/hacknight/forms/event.py +++ b/hacknight/forms/event.py @@ -37,6 +37,16 @@ (PAYMENT_GATEWAY.EXPLARA, u"Explara"), ] +CURRENCY_CHOICES = [ + #INR, USD, GBP, EUR, PHP, ZAR + (u"INR", u"INR"), + (u"USD", u"USD"), + (u"GBP", u"GBP"), + (u"EUR", u"EUR"), + (u"PHP", u"PHP"), + (u"ZAR", u"ZAR"), +] + 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)]) @@ -70,6 +80,9 @@ class EventForm(Form): 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 gateway service name", description="Name of the payment gateway service like explara", 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 for the event", 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: @@ -91,6 +104,18 @@ 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 missing") + + def validate_ticket_price(self, field): + if self.payment_service.data == PAYMENT_GATEWAY.EXPLARA: + try: + float(field.data) + except ValueError: + raise wtforms.ValidationError(u"Event price must be number") + 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..4af89c6 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,22 @@ 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)]) + # 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)]) + 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/event.py b/hacknight/models/event.py index 154a6b5..ccf12f1 100644 --- a/hacknight/models/event.py +++ b/hacknight/models/event.py @@ -121,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 @@ -182,6 +185,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): @@ -209,6 +214,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('explara_purchase_ticket', profile=self.profile.name, event=self.name, _external=_external) + elif action == 'payment_redirect': + return url_for('explara_payment_redirect', profile=self.profile.name, event=self.name, _external=_external) class EventRedirect(BaseMixin, db.Model): diff --git a/hacknight/models/participant.py b/hacknight/models/participant.py index 06864b2..d7ee95d 100644 --- a/hacknight/models/participant.py +++ b/hacknight/models/participant.py @@ -47,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..f7e21a5 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,18 @@

{{ self.title() }}

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

    {{ self.title() }}

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