Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unique coupon code in org and assignee email in item #295

Merged
merged 14 commits into from
Aug 1, 2019
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ instance/testing.py
baseframe-packed.js
baseframe-packed.css
boxoffice/static/js/node_modules/
boxoffice/static/node_modules/
boxoffice/static/js/npm-debug.log
.vscode
.coverage
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
all:
cd boxoffice/static; make
19 changes: 13 additions & 6 deletions boxoffice/assets/js/views/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const Order = {

formValidator.setMessage('required', 'Please fill out the %s field');
formValidator.setMessage('valid_email', 'Please enter a valid email');

formValidator.registerCallback('validate_phone', function(phone) {
//Remove all punctations (except +) and letters
phone = phone.replace(/[^0-9+]/g,'');
Expand Down Expand Up @@ -156,11 +156,12 @@ export const Order = {
contentType: 'application/json',
data: JSON.stringify({
attendee: attendeeDetails,
line_item_id: line_item_id
line_item_id: line_item_id,
csrf_token: $('meta[name=csrf-token]').attr('content')
}),
timeout: 5000,
timeout: 30000,
retries: 5,
retryInterval: 5000,
retryInterval: 30000,
success: function(data) {
order.ticketComponent.set(line_item + '.assigningTicket', false);
order.ticketComponent.set(line_item + '.toAssign', false);
Expand All @@ -169,8 +170,14 @@ export const Order = {
},
error: function(response) {
var ajaxLoad = this;
var onServerError = function() {
order.ticketComponent.set(line_item + '.errorMsg', 'Server error');
var onServerError = function () {
console.log(response)
if (response.responseJSON !== undefined && response.responseJSON.error_description !== undefined) {
var errorMsg = response.responseJSON.error_description;
} else {
var errorMsg = "Server error.";
}
order.ticketComponent.set(line_item + '.errorMsg', errorMsg);
order.ticketComponent.set(line_item + '.assigningTicket', false);
};
var onNetworkError = function() {
Expand Down
1 change: 1 addition & 0 deletions boxoffice/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .order_refund import *
from .discount import *
from .category import *
from .participant import *
31 changes: 25 additions & 6 deletions boxoffice/forms/discount.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# -*- coding: utf-8 -*-

from baseframe import __
import baseframe.forms as forms
from baseframe.forms.sqlalchemy import QuerySelectMultipleField, QuerySelectField
from coaster.utils import getbool
from baseframe import __
from baseframe.forms.sqlalchemy import (
AvailableAttr,
QuerySelectField,
QuerySelectMultipleField,
)
from baseframe.forms.validators import StopValidation
from baseframe.forms.sqlalchemy import AvailableAttr
from ..models import DISCOUNT_TYPE, CURRENCY, Item, ItemCollection, db, DiscountCoupon
from coaster.utils import getbool

from ..models import (
CURRENCY,
DISCOUNT_TYPE,
DiscountCoupon,
DiscountPolicy,
Item,
ItemCollection,
db,
)

__all__ = ['DiscountPolicyForm', 'PriceBasedDiscountPolicyForm', 'DiscountPriceForm', 'DiscountCouponForm', 'AutomaticDiscountPolicyForm', 'CouponBasedDiscountPolicyForm']

Expand Down Expand Up @@ -84,7 +96,14 @@ def set_queries(self):


def validate_unique_discount_coupon_code(form, field):
if DiscountCoupon.query.filter(DiscountCoupon.discount_policy == form.edit_parent, DiscountCoupon.code == field.data).notempty():
if (
DiscountCoupon.query.join(DiscountPolicy)
.filter(
DiscountPolicy.organization == form.edit_parent.organization,
DiscountCoupon.code == field.data
)
.notempty()
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I get the difference between these two queries. Explain it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously it was searching for other instances of the same code being used in the same discount policy. I changed it to look inside the same organization until we figure out how to do it within the same item collection. Right now there is no relationship between a discount policy and an item collection, and discount coupons are created within an organization, and not an item collection. Hence this.

raise StopValidation(__("This discount coupon code already exists. Please enter a different coupon code"))


Expand Down
45 changes: 45 additions & 0 deletions boxoffice/forms/participant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-

from baseframe import __
import baseframe.forms as forms

from boxoffice.models import Assignee, Item, LineItem

__all__ = ['AssigneeForm']


class AssigneeForm(forms.Form):
email = forms.EmailField(
__("Email"),
validators=[forms.validators.DataRequired(), forms.validators.Length(max=254)],
)
fullname = forms.StringField(
__("Full Name"), validators=[forms.validators.DataRequired()]
)
phone = forms.StringField(
__("Phone number"), validators=[forms.validators.DataRequired()]
)
city = forms.StringField(__("City"), validators=[forms.validators.DataRequired()])
company = forms.StringField(
__("Company"), validators=[forms.validators.DataRequired()]
)
jobtitle = forms.StringField(
__("Job Title"), validators=[forms.validators.DataRequired()]
)
subscribe = forms.BooleanField(
__("Sign up to be notified about HasGeek events"),
validators=[forms.validators.DataRequired()],
)

def validate_email(self, field):
existing_assignees = (
Assignee.query.join(LineItem)
.filter(LineItem.item == self.edit_parent.item)
.filter(Assignee.email == field.data)
)
if self.edit_obj is not None:
existing_assignees = existing_assignees.filter(
Assignee.id != self.edit_obj.id
)
if existing_assignees.count() > 0:
raise forms.ValidationError(__("Email address has been already used"))
14 changes: 12 additions & 2 deletions boxoffice/models/discount_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from werkzeug import cached_property
from itsdangerous import Signer, BadSignature
from sqlalchemy import event, DDL
from sqlalchemy.orm.exc import MultipleResultsFound
from baseframe import __
from coaster.utils import LabeledEnum, uuid1mc, buid
from coaster.sqlalchemy import cached
Expand Down Expand Up @@ -179,8 +180,17 @@ def is_valid_access_coupon(cls, item, code_list):
if policy and not DiscountCoupon.is_signed_code_usable(policy, code):
break
else:
policy = cls.query.join(DiscountCoupon).filter(
DiscountCoupon.code == code, DiscountCoupon.used_count < DiscountCoupon.usage_limit).one_or_none()
try:
policy = (
cls.query.join(DiscountCoupon)
.filter(
DiscountCoupon.code == code,
DiscountCoupon.used_count < DiscountCoupon.usage_limit,
)
.one_or_none()
)
except MultipleResultsFound as e:
iambibhas marked this conversation as resolved.
Show resolved Hide resolved
policy = None
if bool(policy) and policy in item.discount_policies:
return True
return False
Expand Down
8 changes: 8 additions & 0 deletions boxoffice/static/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
all: assets

assets:
npm install
npm run build

buildonly:
npm run build
116 changes: 81 additions & 35 deletions boxoffice/views/participant.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,95 @@
# -*- coding: utf-8 -*-

from flask import request, jsonify, make_response
from .. import app
from ..models import db
from ..models import LineItem, Assignee
from ..models import Order
from coaster.views import load_models
from flask import jsonify, make_response, request

from boxoffice.mailclient import send_ticket_assignment_mail
from utils import xhr_only

from coaster.views import load_models, render_with

from .. import app
from ..forms import AssigneeForm
from ..models import Assignee, LineItem, Order, db


@app.route('/participant/<access_token>/assign', methods=['GET', 'OPTIONS', 'POST'])
@xhr_only
@load_models(
(Order, {'access_token': 'access_token'}, 'order')
)
@render_with(json=True)
@load_models((Order, {'access_token': 'access_token'}, 'order'))
def assign(order):
"""
Assign a line_item to a participant
"""
assignee_dict = request.json.get('attendee')
if not request.json or not assignee_dict or not assignee_dict.get('email') or not assignee_dict.get('fullname'):
return make_response(jsonify(status='error', error='missing_attendee_details', error_description="Attendee details are missing"), 400)
line_item = LineItem.query.get(request.json.get('line_item_id'))
if line_item.is_cancelled:
return make_response(jsonify(status='error', error='cancelled_ticket', error_description="Ticket has been cancelled"), 400)

item_assignee_details = line_item.item.assignee_details
assignee_details = {}
if item_assignee_details:
for key in item_assignee_details.keys():
assignee_details[key] = assignee_dict.get(key)
if line_item.current_assignee and assignee_dict['email'] == line_item.current_assignee.email:
# updating details of the current assignee
line_item.current_assignee.fullname = assignee_dict['fullname']
line_item.current_assignee.phone = assignee_dict['phone']
line_item.current_assignee.details = assignee_details
db.session.commit()
if line_item is None:
return {
'status': 'error',
'error': 'missing_line_item',
'error_description': u"Invalid line item",
}
iambibhas marked this conversation as resolved.
Show resolved Hide resolved
elif line_item.is_cancelled:
return (
{
'status': 'error',
'error': 'cancelled_ticket',
'error_description': u"Ticket has been cancelled",
},
400,
)

if line_item.current_assignee is not None:
assignee_form = AssigneeForm.from_json(
request.json.get('attendee'),
obj=line_item.current_assignee,
parent=line_item,
csrf_token=request.json.get('csrf_token'),
)
else:
iambibhas marked this conversation as resolved.
Show resolved Hide resolved
assignee_form = AssigneeForm.from_json(
request.json.get('attendee'),
parent=line_item,
csrf_token=request.json.get('csrf_token'),
)

if assignee_form.validate_on_submit():
item_assignee_details = line_item.item.assignee_details
assignee_details = {}
if item_assignee_details:
for key in item_assignee_details.keys():
assignee_details[key] = assignee_form.data.get(key)
if (
line_item.current_assignee
and assignee_form.data['email'] == line_item.current_assignee.email
):
# updating details of the current assignee
line_item.current_assignee.fullname = assignee_form.data['fullname']
line_item.current_assignee.phone = assignee_form.data['phone']
line_item.current_assignee.details = assignee_details
db.session.commit()
else:
if line_item.current_assignee:
# Archive current assignee
line_item.current_assignee.current = None
new_assignee = Assignee(
current=True,
email=assignee_form.data.get('email'),
fullname=assignee_form.data.get('fullname'),
phone=assignee_form.data.get('phone'),
details=assignee_details,
line_item=line_item,
)
db.session.add(new_assignee)
db.session.commit()
send_ticket_assignment_mail.queue(line_item.id)
return {'status': 'ok', 'result': {'message': 'Ticket assigned'}}
else:
if line_item.current_assignee:
# Archive current assignee
line_item.current_assignee.current = None
new_assignee = Assignee(current=True, email=assignee_dict.get('email'), fullname=assignee_dict.get('fullname'),
phone=assignee_dict.get('phone'), details=assignee_details, line_item=line_item)
db.session.add(new_assignee)
db.session.commit()
send_ticket_assignment_mail.queue(line_item.id)
return make_response(jsonify(status='ok', result={'message': 'Ticket assigned'}), 201)
return (
{
'status': 'error',
'error': 'invalid_assignee_details',
'error_description': ", ".join(
[str(err.pop()) for err in assignee_form.errors.values()]
iambibhas marked this conversation as resolved.
Show resolved Hide resolved
),
},
400,
)