Implementing Notification Action Buttons in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create access codes for his or her event.  Access codes can be used to password protect hidden tickets reserved for sponsors, members of the press and media. Notifications are an important part of the project. We show each registered user notifications based on their activity. This blog post goes over the implementation of the notification action buttons in the notification panel.

Notification Action Model

The model for Notification action is very simple. It has the following variables:

  1. Subject: The subject of the notification. E.g. ‘event’, ‘order’ etc.
  2. actionType: The action that can be taken by the user for that notification. E.g: ‘view’, ‘submit’.
  3. subjectId: The id of the subject. In case of an event, it will store the event id. Similarly for other cases.
  4. Link: The link to be applied to the button.

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';
import { belongsTo } from 'ember-data/relationships';

export default ModelBase.extend({
  subject    : attr('string'),
  actionType : attr('string'),
  subjectId  : attr('number'),
  link       : attr('string'),

  notification: belongsTo('notification')
});

Action Button Title

We make use of ember computed property to determine the action button title. The title of the button depends on the subject and the actionType defined in the notification-action model. The actionType can be one of ‘download’, ‘submit’ and ‘view’. If the action type is ‘download’ and the subject is ‘invoice’, then the button title will be “Download Invoice”. Similarly, for other cases, we do the same.

buttonTitle: computed('subject', 'actionType', function() {
    let action;
    const actionType = this.get('actionType');
    switch (actionType) {
      case 'download':
        action = 'Download';
        break;

      case 'submit':
        action = 'Submit';
        break;

      default:
        action = 'View';
    }

    let buttonSubject;
    const subject = this.get('subject');
    switch (subject) {
      case 'event-export':
        buttonSubject = ' Event';
        break;

      case 'event':
        buttonSubject = ' Event';
        break;

      case 'invoice':
        buttonSubject = ' Invoice';
        break;

      case 'order':
        buttonSubject = ' Order';
        break;

      case 'tickets-pdf':
        buttonSubject = ' Tickets';
        break;

      case 'event-role':
        buttonSubject = ' Invitation Link';
        break;

      case 'session':
        buttonSubject = ' Session';
        break;

      case 'call-for-speakers':
        if (this.get('actionType') === 'submit') {
          buttonSubject = ' Proposal';
        } else {
          buttonSubject = ' Call for Speakers';
        }
        break;

      default:
        // Nothing here.
    }

    return action + buttonSubject;
  })

Action Button Route

The route that the button will lead to depends on the subject of the action. If the link is provided in the notification action, we simply set it on the button otherwise we use the subject to derive the route name. For e.g., if the subject is an event, then the route will be “events.view”.

/**
   * The route name to which the action button will direct the user to.
   */
  buttonRoute: computed('subject', function() {
    const subject = this.get('subject');
    let routeName;
    switch (subject) {
      case 'event-export':
        routeName = 'events.view';
        break;

      case 'event':
        routeName = 'events.view';
        break;

      case 'invoice':
        routeName = 'orders.view';
        break;

      case 'order':
        routeName = 'orders.view';
        break;

      default:
      // Nothing here.
    }
    return routeName;
  })

Template

We simply check if the link exists or not. If it does then we simply use it otherwise we use the computed button route name.

{{#if action.link}}
     {{#link-to action.link tagName='button' class='ui blue button'}}
         {{t action.buttonTitle}}
         {{/link-to}}
{{else}}
    {{#link-to action.buttonRoute action.subjectId tagName='button' class='ui blue button'}}
         {{t action.buttonTitle}}
         {{/link-to}}
{{/if}}

References

Continue Reading

Onsite Attendee in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. The Event organizers may add orders on behalf of others and accept payments onsite. This blog post goes over the implementation of the onsite attendee feature in the Open Event Server.

Route

Normally we expect the payload for a POST request of order to contain already created attendees also. In this case we want to create the attendees internally inside the server. Hence we need some way to differentiate between the two types of orders. The most basic and easy to implement option is to use a query parameter to specify if the attendees are onsite or not. We use ?onsite=true in order to specify that the attendees are onsite and hence should be created internally.

In the POST request, we check if the query parameters contains the onsite param as true or not. If it is true then we create the attendees using a helper function. The helper function will be discussed in detail later in the article.

# Create on site attendees.
if request.args.get('onsite', False):
    create_onsite_attendees_for_order(data)
elif data.get('on_site_tickets'):
    del data['on_site_tickets']
require_relationship(['ticket_holders'], data)

 

OnsiteTicketSchema

In order to create attendees on the server, we need the information about each ticket bought and it’s quantity. This data is expected in the format declared in the OnsiteTicketSchema.

class OnSiteTicketSchema(SoftDeletionSchema):
    class Meta:
        type_ = 'on-site-ticket'
        inflect = dasherize

    id = fields.Str(load_only=True, required=True)
    quantity = fields.Str(load_only=True, required=True)

Creating onsite Attendees

Following are the few points which we need to focus on when creating onsite attendees:

  1. Validate if the ticket’s data is provided or not. We raise an error if the ticket data is not provided.
  2. Verify if the ticket is sold out or not. We raise an error if the ticket is sold out.
  3. In case an error is raised in any step then we delete the already created attendees. This is a very important point to keep in mind.

if not on_site_tickets:
        raise UnprocessableEntity({'pointer': 'data/attributes/on_site_tickets'}, 'on_site_tickets info missing')

ticket_sold_count = get_count(db.session.query(TicketHolder.id).
                                      filter_by(ticket_id=int(ticket.id), deleted_at=None))

        # Check if the ticket is already sold out or not.
        if ticket_sold_count + quantity > ticket.quantity:
            # delete the already created attendees.
            for holder in data['ticket_holders']:
                ticket_holder = db.session.query(TicketHolder).filter(id == int(holder)).one()
                db.session.delete(ticket_holder)
                try:
                    db.session.commit()
                except Exception as e:
                    logging.error('DB Exception! %s' % e)
                    db.session.rollback()

            raise ConflictException(
                {'pointer': '/data/attributes/on_site_tickets'},
                "Ticket with id: {} already sold out. You can buy at most {} tickets".format(ticket_id,
                                                                                             ticket.quantity -
                                                                                             ticket_sold_count)
            )

The complete method can be checked here.

References

 

 

Continue Reading

Attendee Form Builder in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. When buying a ticket we ask for certain information from the buyer. Ideally the event organizer should get to choose what information they want to ask from the buyer. This blog post goes over the implementation of the attendee form builder in the Open Event Frontend.

Information to Collect

The event organizer can choose what details to ask from the order buyer. In order to specify the choices, we present a table with the entries of allowed fields that the organizer can ask for. Moreover there is an option to mark the field as required and hence making it compulsory for the order buyer to add that information in order to buy the tickets.

Route

The route is mainly responsible for fetching the required custom forms.

async model() {
    let filterOptions = [{
      name : 'form',
      op   : 'eq',
      val  : 'attendee'
    }];

    let data = {
      event: this.modelFor('events.view')
    };
    data.customForms = await data.event.query('customForms', {
      filter       : filterOptions,
      sort         : 'id',
      'page[size]' : 50
    });

    return data;
  }

If they don’t exist then we create them in afterModel hook. We check if the size of the list of custom forms sent from the server is 3 or not. If it is 3 then we create the additional custom forms for the builder. Upon creating an event, the server automatically creates 3 custom forms for the builder. These 3 forms are firstName, lastName and email.

afterModel(data) {
    /**
     * Create the additional custom forms if only the compulsory forms exist.
     */
    if (data.customForms.length === 3) {
      let customForms = A();
      for (const customForm of data.customForms ? data.customForms.toArray() : []) {
        customForms.pushObject(customForm);
      }

      const createdCustomForms = this.getCustomAttendeeForm(data.event);

      for (const customForm of createdCustomForms ? createdCustomForms : []) {
        customForms.pushObject(customForm);
      }

      data.customForms = customForms;
    }
  }

Complete source code for reference can be found here.

Component Template

The component template for the form builder is supposed to show the forms and other options to the user in a presentable manner. Due to pre-existing components for handling custom forms, the template is extremely simple. We just loop over the list of custom forms and present the event organizer with a table comprising of the forms. Apart from the forms the organizer can specify the order expiry time. Lastly we present a save button in order to save the changes.

<form class="ui form {{if isLoading 'loading'}}"  {{action 'submit' data on='submit'}} autocomplete="off">
  <h3 class="ui dividing header">
    <i class="checkmark box icon"></i>
    <div class="content">
      {{t 'Information to Collect'}}
    </div>
  </h3>
  <div class="ui two column stackable grid">
    <div class="column">
      <table class="ui selectable celled table">
        <thead>
          <tr>
            {{#if device.isMobile}}
              <th class="center aligned">
                {{t 'Options'}}
              </th>
            {{else}}
              <th class="right aligned">
                {{t 'Option'}}
              </th>
              <th class="center aligned">
                {{t 'Include'}}
              </th>
              <th class="center aligned">
                {{t 'Require'}}
              </th>
            {{/if}}
          </tr>
        </thead>
        <tbody>
          {{#each data.customForms as |field|}}
            <tr class="{{if field.isIncluded 'positive'}}">
              <td class="{{if device.isMobile 'center' 'right'}} aligned">
                <label class="{{if field.isFixed 'required'}}">
                  {{field.name}}
                </label>
              </td>
              <td class="center aligned">
                {{ui-checkbox class='slider'
                              checked=field.isIncluded
                              disabled=field.isFixed
                              onChange=(action (mut field.isIncluded))
                              label=(if device.isMobile (t 'Include'))}}
              </td>
              <td class="center aligned">
                {{ui-checkbox class='slider'
                              checked=field.isRequired
                              disabled=field.isFixed
                              onChange=(action (mut field.isRequired))
                              label=(if device.isMobile (t 'Require'))}}
              </td>
            </tr>
          {{/each}}
        </tbody>
      </table>
    </div>
  </div>
  <h3 class="ui dividing header">
    <i class="options box icon"></i>
    <div class="content">
      {{t 'Registration Options'}}
    </div>
  </h3>
  <div class="field">
    <label>{{t 'REGISTRATION TIME LIMIT'}}</label>
    <div class="{{unless device.isMobile 'two wide'}} field">
      {{input type='number' id='orderExpiryTime' value=data.event.orderExpiryTime min="1" max="60" step="1"}}
    </div>
  </div>
  <div class="ui hidden divider"></div>
  <button type="submit" class="ui teal submit button" name="submit">{{t 'Save'}}</button>
</form>

 

Component

The component is responsible for saving the form. It also provides runtime validations to ensure that the entries entered in the fields of the form are valid.

import Component from '@ember/component';
import FormMixin from 'open-event-frontend/mixins/form';

export default Component.extend(FormMixin, {
  getValidationRules() {
    return {
      inline : true,
      delay  : false,
      on     : 'blur',
      fields : {
        orderExpiryTime: {
          identifier : 'orderExpiryTime',
          rules      : [
            {
              type   : 'integer[1..60]',
              prompt : this.get('l10n').t('Please enter a valid registration time limit between 1 to 60 minutes.')
            }
          ]
        }
      }
    };
  },
  actions: {
    submit(data) {
      this.onValid(() => {
        this.save(data);
      });
    }
  }
});

References

 

Continue Reading

Redirecting to Previous Route in Ember

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. In order to make the user experience smooth, we redirect the user to their previous route when they successfully login into their account. This blog explains how we have achieved this functionality in the project.

Insight

We have two different cases to handle in order to solve this problem:

  1. The user was in route A and wanted to move to route B. Here route A doesn’t require authorization and route B requires authorization. In this case, we would like to direct the user to the login route and once they are done, redirect them back to route B.
  2. The user was in route A and directly entered the login route using the login button. In this case we want to direct them back to the route A after successful login.

We use Ember-simple-auth in order to manage authentication in the project. Not only does it make it easy to manage authentication, it also handles the case 1 for us out of the box. So now the simplified problem is to redirect the user back to the previous route if they entered the login route directly using the web address or the login button.

Approach

If we can somehow store the previous route visited by a user, then we can easily redirect them back once they are logged in.

We will add a custom property in the session service called previousRouteName which will store the URL of the previous route visited by the user. We will make use of the willTransition hook in the application.js file. This hook is called everytime the user transitions from one route to another which makes it suitable for us to update the previousRouteName.

actions: {
    willTransition(transition) {
      transition.then(() => {
        let params = this._mergeParams(transition.params);
        let url;

        // generate doesn't like empty params.
        if (isEmpty(params)) {
          url = transition.router.generate(transition.targetName);
        } else {
          url = transition.router.generate(transition.targetName, params);
        }
        // Do not save the url of the transition to login route.
        if (!url.includes('login')) {
          this.set('session.previousRouteName', url);
        }
      });
    }
  }

_mergeParams is a helper function which makes use of merge function of the Lodash library.

/**
   * Merge all params into one param.
   * @param params
   * @return {*}
   * @private
   */
  _mergeParams(params) {
    return merge({}, ...values(params));
  },

Now we’re done with saving the URL of the previous route. All that remains is to trigger the redirect once the user has successfully logged in. We will use the sessionAuthenticated hook which is triggered everytime the user logs in.

sessionAuthenticated() {
    if (this.get('session.previousRouteName')) {
      this.transitionTo(this.get('session.previousRouteName'));
    } else {
      this._super(...arguments);
    }
  },

If the previous route variable is set, we redirect to it otherwise we can the super method and let Ember-simple-auth handle case 1 mentioned earlier for us.

References

Continue Reading

My Tickets in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create tickets for his or her event. Other uses can buy these tickets in order to attend the event. The My tickets section lists all the tickets that have been bought by a user. This blog post explains how it has been implemented in the project.

Route

The My-Tickets list route has three responsibilities:

  1. Showing appropriate title according to the current tab.
  2. Setting the filter options according to the tab.
  3. Fetching the data from the store according to the filter options.

The title of the route is decided by the following snippet:

titleToken() {
    switch (this.get('params.ticket_status')) {
      case 'upcoming':
        return this.get('l10n').t('Upcoming');
      case 'past':
        return this.get('l10n').t('Past');
      case 'saved':
        return this.get('l10n').t('Saved');
    }
  },

The second and the third requirement is satisfied inside the model hook. We define the filterOptions according to the current tab and then make the request to fetch the data accordingly. The following code snippet is responsible for this:

model(params) {
    this.set('params', params);
    let filterOptions = [{
      name : 'completed-at',
      op   : 'ne',
      val  : null
    }];
    if (params.ticket_status === 'upcoming') {
      filterOptions.push(
        {
          name : 'event',
          op   : 'has',
          val  : {
            name : 'starts-at',
            op   : 'ge',
            val  : moment().toISOString()
          }
        });
    } else if (params.ticket_status === 'past') {
      filterOptions.push(
        {
          name : 'event',
          op   : 'has',
          val  : {
            name : 'ends-at',
            op   : 'lt',
            val  : moment().toISOString()
          }
        }
      );
    }

    return this.get('authManager.currentUser').query('orders', {
      include : 'event',
      filter  : filterOptions
    });
  }

Template

The template of the My tickets list is extremely simple. We simply loop over all the orders and use the order-card component to display each of them. The order-card component is discussed in detail later. If there are no orders under the user, we show the appropriate message.

<div class="row">
  <div class="sixteen wide column">
    {{#if model}}
      {{#each model as |order|}}
        {{#order-card order=order}}
        {{/order-card}}
        <div class="ui hidden divider"></div>
      {{/each}}
    {{else}}
      <div class="ui disabled header">{{t 'No tickets found'}}</div>
    {{/if}}
  </div>
</div>

Order-card Component

The order card component is responsible for handling a single order and showing its details in as a card. In order to decide whether the order is a paid order or not, we have defined a computed property inside the order-card.js file.

import Component from '@ember/component';
import { computed } from '@ember/object';
import { isEqual } from '@ember/utils';

export default Component.extend({
  isFreeOrder: computed('order', function() {
    const amount = this.get('order.amount');
    return amount === null || isEqual(amount, '0');
  })
});

The template for the component contains the event logo aligned to the left in the card. We show the event details such as the name, location and start date on the right. Below the event details we show the order details such as the order amount, currency, the identifier and the date and time on which the order was completed. Below is the full code for reference:

<div class="event wide ui grid row">
  {{#unless device.isMobile}}
    <div class="ui card three wide computer six wide tablet column">
      <a class="image" href="#">
        {{widgets/safe-image src=(if order.event.originalImageUrl order.event.originalImageUrl order.event.originalImageUrl)}}
      </a>
    </div>
  {{/unless}}
  <div class="ui card thirteen wide computer ten wide tablet sixteen wide mobile column">
    <a class="main content" href="#">
      {{#smart-overflow class='header'}}
        {{order.event.name}}
      {{/smart-overflow}}
      <div class="meta">
        <span class="date">
          {{moment-format order.event.startsAt 'ddd, DD MMMM YYYY, h:mm A'}}
        </span>
      </div>
      {{#smart-overflow class='description'}}
        {{order.event.shortLocationName}}
      {{/smart-overflow}}
    </a>
    <div class="extra content small text">
      <span>
        <span>
          {{#if isFreeOrder}}
            {{t 'Free'}}
          {{else}}
            {{order.event.paymentCurrency}}{{order.amount}}
          {{/if}}
          {{t 'order'}}
        </span>
        <span>#{{order.identifier}}</span>
        <span>{{t 'on'}} {{moment-format order.completedAt 'MMMM DD, YYYY h:mm A'}}</span>
      </span>
    </div>
  </div>
</div>

References

Continue Reading

Speaker details in the Open Event Orga App

The Open Event Organiser Android App is currently released in the Alpha phase on the Google Play Store here. This blog post explains how the speaker details feature has been implemented in the app.

Model

The model for Speaker is pretty straightforward. It includes the personal details of the speaker such as name, biography, country, social media profiles, designation etc. Apart from these details, every instance of speaker is associated with a single event. A speaker will also have multiple instances of sessions. Full implementation of the speaker’s model can be found here.

Network Call

We use Retrofit in order to make the network call and Jackson Factory to deserialize the data received from the call into an instance of the speaker model. The following endpoint provides us with the required information:

https://open-event-api-dev.herokuapp.com/speakers/{speaker_id}

Repository

In any typical android application using both network calls and data persistence, there is a need of a repository class to handle them. Speaker Repository handles the network call to the API in order to fetch the speaker details. It then saves the data returned by the api into the database asynchronously. It also ensures that we send the latest data that we have stored in the database to the view model. Given below is the full implementation for reference:

@Override
    public Observable<Speaker> getSpeaker(long speakerId, boolean reload) {
        Observable<Speaker> diskObservable = Observable.defer(() ->
            repository
                .getItems(Speaker.class, Speaker_Table.id.eq(speakerId)).take(1)
        );

        Observable<Speaker> networkObservable = Observable.defer(() ->
            speakerApi.getSpeaker(speakerId)
                .doOnNext(speaker -> repository
                    .save(Speaker.class, speaker)
                    .subscribe()));

        return repository
            .observableOf(Speaker.class)
            .reload(reload)
            .withDiskObservable(diskObservable)
            .withNetworkObservable(networkObservable)
            .build();
    }

ViewModel

The View Model is responsible for fetching the necessary details from the repository and displaying it in the view. It handles all the view binding logic. The most important method in the SpeakerDetailsViewModel is the getSpeakers method. It accepts a speaker id from the fragment, queries the repository for the details of the speaker and returns it back to the fragment in the form of a LiveData. Below is the full implementation of the getSpeakers method:

protected LiveData<Speaker> getSpeaker(long speakerId, boolean reload) {
        if (speakerLiveData.getValue() != null && !reload)
            return speakerLiveData;

        compositeDisposable.add(speakerRepository.getSpeaker(speakerId, reload)
            .compose(dispose(compositeDisposable))
            .doOnSubscribe(disposable -> progress.setValue(true))
            .doFinally(() -> progress.setValue(false))
            .doOnNext(speaker -> speakerLiveData.setValue(speaker))
            .flatMap(speaker -> sessionRepository.getSessionsUnderSpeaker(speakerId, reload))
            .toList()
            .subscribe(sessionList -> sessionLiveData.setValue(sessionList),
                throwable -> error.setValue(ErrorUtils.getMessage(throwable))));

        return speakerLiveData;
    }

We add the disposable to a composite disposable and dispose it in the onCleared method of the View Model. The full implementation of the View Model can be found here.

Fragment

The SpeakerDetailsFragment acts as the view and is responsible for everything the user sees on the screen. It accepts the id of the speaker whose details are to be displayed in the constructor. When an instance of the fragment is created it sets up it’s view model and inflates it’s layout using the Data binding framework.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
   context = getContext();
   binding = DataBindingUtil.inflate(inflater, R.layout.speaker_details_fragment, container, false);
   speakerDetailsViewModel = ViewModelProviders.of(this, viewModelFactory).get(SpeakerDetailsViewModel.class);
   speakerId = getArguments().getLong(SPEAKER_ID);

   AppCompatActivity activity = ((AppCompatActivity) getActivity());
   activity.setSupportActionBar(binding.toolbar);

   ActionBar actionBar = activity.getSupportActionBar();
   if (actionBar != null) {
       actionBar.setHomeButtonEnabled(true);
       actionBar.setDisplayHomeAsUpEnabled(true);
   }

   return binding.getRoot();
}

In the onStart method of the fragment we load the data by calling the getSpeaker method in the view model. Then we set up the RecyclerView for the sessions associated with the speaker. Lastly we also set up the refresh listener which can be used by the user to refresh the data.

@Override
public void onStart() {
   super.onStart();

   speakerDetailsViewModel.getProgress().observe(this,this::showProgress);
   speakerDetailsViewModel.getError().observe(this, this::showError);
   loadSpeaker(false);

   setupRecyclerView();
   setupRefreshListener();
}

Once the data is returned we simply set it on the layout by calling setSpeaker on the binding.

@Override
public void showResult(Speaker item) {
   binding.setSpeaker(item);
}

The full implementation of the SpeakerDetailsFragment can be found here.

Sessions Adapter

The SessionsAdapter is responsible for handling the RecyclerView of sessions associated with the speaker. The most important method in the adapter is the setSessions method. It accepts a list of sessions and shows it as the contents of the recycler view. It uses the DiffUtil.calculateDiff method to create a DiffResult which will be used by the adapter to figure out where to insert items.

protected void setSessions(final List<Session> newSessions) {
        if (sessions == null) {
            sessions = newSessions;
            notifyItemRangeInserted(0, newSessions.size());
        } else {
            DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                @Override
                public int getOldListSize() {
                    return sessions.size();
                }

                @Override
                public int getNewListSize() {
                    return newSessions.size();
                }

                @Override
                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                    return sessions.get(oldItemPosition).getId()
                        .equals(newSessions.get(newItemPosition).getId());
                }

                @Override
                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                    return sessions.get(oldItemPosition).equals(newSessions.get(newItemPosition));
                }
            });
            sessions = newSessions;
            result.dispatchUpdatesTo(this);
        }
    }

The full implementation of the Adapter can be found here.

References

Continue Reading

Paypal Integration in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. This blog post explains how Paypal has been integrated in the Open Event Server in order to accept payments for tickets.

The integration of Paypal in the server involved the following steps:

  1. An endpoint to accept the Paypal token from the client applications.
  2. Using the token to get the approved payment details.
  3. Capturing the payment using the fetched payment details.

Endpoint for Paypal token

The server exposes an endpoint to get the Paypal token in order to accept payments.

api.route(ChargeList, ‘charge_list’, ‘/orders/<identifier>/charge’, ‘/orders/<order_identifier>/charge’)

The above endpoint accepts the Paypal token and uses that to get the payment details from Paypal and then capture the payments.

Getting Approved Payment Details

We use the Paypal Name-Value pair API in the project. First we get the credentials of the event organizer who will be accepting the payments using a call to the get_credentials helper method. It returns the data as the following dictionary:

credentials = {
  'USER': settings['paypal_live_username'],
  'PWD': settings['paypal_live_password'],
  'SIGNATURE': settings['paypal_live_signature'],
  'SERVER': 'https://api-3t.paypal.com/nvp',
  'CHECKOUT_URL': 'https://www.paypal.com/cgi-bin/webscr',
  'EMAIL': '' if not event or not event.paypal_email or event.paypal_email == "" else event.paypal_email
}

Next, we use the credentials to get the approved payment details from paypal using the following code snippet.

@staticmethod
def get_approved_payment_details(order, credentials=None):
   if not credentials:
     credentials = PayPalPaymentsManager.get_credentials(order.event)

   if not credentials:
     raise Exception('PayPal credentials have not been set correctly')

   data = {
            'USER': credentials['USER'],
            'PWD': credentials['PWD'],
            'SIGNATURE': credentials['SIGNATURE'],
            'SUBJECT': credentials['EMAIL'],
            'METHOD': 'GetExpressCheckoutDetails',
            'VERSION': PayPalPaymentsManager.api_version,
            'TOKEN': order.paypal_token
   }

   if current_app.config['TESTING']:
      return data

   response = requests.post(credentials['SERVER'], data=data)
   return json.loads(response.text)

Capturing the payments

After successfully fetching the payment details, the final step is to capture the payment. We set the amount to be charged to the amount of the order and the payer_id to be the payer id received from step 2. Then we simply make a POST request to the Paypal nvp server and capture the payments. The below method is responsible for executing this task:

@staticmethod
def capture_payment(order, payer_id, currency=None, credentials=None):
  if not credentials:
    credentials = PayPalPaymentsManager.get_credentials(order.event)

  if not credentials:
    raise Exception('PayPal credentials have not be set correctly')

  if not currency:
    currency = order.event.payment_currency

  if not currency or currency == "":
    currency = "USD"

   data = {
            'USER': credentials['USER'],
            'PWD': credentials['PWD'],
            'SIGNATURE': credentials['SIGNATURE'],
            'SUBJECT': credentials['EMAIL'],
            'METHOD': 'DoExpressCheckoutPayment',
            'VERSION': PayPalPaymentsManager.api_version,
            'TOKEN': order.paypal_token,
            'PAYERID': payer_id,
            'PAYMENTREQUEST_0_PAYMENTACTION': 'SALE',
            'PAYMENTREQUEST_0_AMT': order.amount,
            'PAYMENTREQUEST_0_CURRENCYCODE': currency,
   }

   response = requests.post(credentials['SERVER'], data=data)
   return json.loads(response.text)

References

Continue Reading

Charges Layer in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. This blog post explains how the charge layer has been implemented in the Open Event Server in order to charge the user for tickets of an event.

Schema

We currently support payments via Stripe and Paypal. As a result the schema for Charges layer consists of fields for providing the token for stripe or paypal. It also contains a read only id field.

class ChargeSchema(Schema):
    """
    ChargeSchema
    """

    class Meta:
        """
        Meta class for ChargeSchema
        """
        type_ = 'charge'
        inflect = dasherize
        self_view = 'v1.charge_list'
        self_view_kwargs = {'id': '<id>'}

    id = fields.Str(dump_only=True)
    stripe = fields.Str(allow_none=True)
    paypal = fields.Str(allow_none=True)

Resource

The ChargeList resource only supports POST requests since there is no need for other type of requests. We simply declare the schema, supported methods and the data layer. We also check for required permissions by declaring the decorators.

class ChargeList(ResourceList):
    """
    ChargeList ResourceList for ChargesLayer class
    """
    methods = ['POST', ]
    schema = ChargeSchema

    data_layer = {
        'class': ChargesLayer,
        'session': db.session
    }

    decorators = (jwt_required,)

Layer

The data layer contains a single method create_object which does all the heavy lifting of charging the user according to the payment medium and the related order. It first loads the related order from the database using the identifier.

We first check if the order contains one or more paid tickets or not. If not, then ConflictException is raised since it doesn’t make sense to charge a user without any paid ticket in the order. Next, it checks the payment mode of the order. If the payment mode is Stripe then it checks if the stripe_token is provided with the request or not. If not, an UnprocessableEntity exception is raised otherwise relevant methods are called in order to charge the user accordingly. A similar procedure is followed for payments via Paypal. Below is the full code for reference.

class ChargesLayer(BaseDataLayer):

    def create_object(self, data, view_kwargs):
        """
        create_object method for the Charges layer
        charge the user using paypal or stripe
        :param data:
        :param view_kwargs:
        :return:
        """
        order = Order.query.filter_by(id=view_kwargs['id']).first()
        if not order:
            raise ObjectNotFound({'parameter': 'id'},
                                 "Order with id: {} not found".format(view_kwargs['id']))
        elif order.status == 'cancelled' or order.status == 'expired':
            raise ConflictException({'parameter': 'id'},
                                    "You cannot charge payments on a cancelled or expired order")
        elif (not order.amount) or order.amount == 0:
            raise ConflictException({'parameter': 'id'},
                                    "You cannot charge payments on a free order")

        # charge through stripe
        if order.payment_mode == 'stripe':
            if not data.get('stripe'):
                raise UnprocessableEntity({'source': ''}, "stripe token is missing")
            success, response = TicketingManager.charge_stripe_order_payment(order, data['stripe'])
            if not success:
                raise UnprocessableEntity({'source': 'stripe_token_id'}, response)

        # charge through paypal
        elif order.payment_mode == 'paypal':
            if not data.get('paypal'):
                raise UnprocessableEntity({'source': ''}, "paypal token is missing")
            success, response = TicketingManager.charge_paypal_order_payment(order, data['paypal'])
            if not success:
                raise UnprocessableEntity({'source': 'paypal'}, response)
        return order

The charge_stripe_order_payment and charge_paypal_order_payment are helper methods defined to abstract away the complications of the procedure from the layer.

References

Continue Reading

Soft Deletion Support in Open Event Server

The Open Event Server enables organizers to manage events from concerts to conferences and meetups. It offers features for events with several tracks and venues. This blog post explains how the soft deletion support has been implemented in the project to allow the event admin to quickly restore the deleted events or users.

Idea

The idea is extremely simple. We can add a column deleted_at in all the soft deleted models. If that entry is None then we can be sure that this row wasn’t deleted or else we can conclude that the row was deleted by the user. In case we want to recover it later, we can simply set it back to None.

Approach

We had a lot of models that needed this support. Hence in order to obey the DRY principles, we created base model and schema classes which would be inherited by the models and schemas that need this support.

from marshmallow_jsonapi import fields
from marshmallow_jsonapi.flask import Schema


class SoftDeletionSchema(Schema):
    """
        Base Schema for soft deletion support. All the schemas that support soft deletion should extend this schema
    """
    deleted_at = fields.DateTime(allow_none=True)

from app.models import db


class SoftDeletionModel(db.Model):
    """
        Base model for soft deletion support. All the models which support soft deletion should extend it.
    """
    __abstract__ = True

    deleted_at = db.Column(db.DateTime(timezone=True))

Usage

We use the Flask-Rest-Json-API library in the project. We have modified it in order to support the soft deletion feature. By default all the models that support soft deletion are soft deleted. In order to specify hard deletion, an extra parameter permanent set to true needs to be passed in with the request.

DELETE

We check if the model supports soft deletion or not. If the model doesn’t support soft deletion then we permanently delete it otherwise we check if we have the parameter permanent and it is set to true. If it is set to true, then we permanently delete the row otherwise we update the deleted_at with the current time.

if 'deleted_at' not in self.schema._declared_fields or request.args.get('permanent') == 'true' or current_app.config['SOFT_DELETE'] is False:
            self._data_layer.delete_object(obj, kwargs)
else:
    data = {'deleted_at': str(datetime.now(pytz.utc))}
    self._data_layer.update_object(obj, data, kwargs)

GET

When we are returning one or more entry we offer the client the option to choose whether the soft deleted entries should be included or not. By default they aren’t. In order to get them a query parameter get_trashed should be passed along with the URL.

if request.args.get('get_trashed') == 'true':
    obj = self._data_layer.get_object(kwargs, get_trashed=True)
else:
    obj = self._data_layer.get_object(kwargs)

In case soft deleted entries are not required, then we filter the results for cases when deleted_at is None

try:
    if 'deleted_at' not in self.resource.schema._declared_fields or get_trashed or current_app.config['SOFT_DELETE'] is False:
         obj = self.session.query(self.model).filter(filter_field == filter_value).one()
    else:
         obj = self.session.query(self.model).filter(filter_field == filter_value).filter_by(deleted_at=None).one()

References

Continue Reading

Implementing Access Codes in Open Event Frontend

The Open-Event-Frontend allows the event organiser to create access codes for his or her event.  Access codes can be used to password protect hidden tickets reserved for sponsors, members of the press and media. This blog post explains how we have integrated access codes creation in the frontend utilising the various features of Ember JS and Semantic UI.

Create Access code component

We will be creating a separate component for creating access code. To create it we will use the following command:

ember g component forms/events/view/create-access-code

This will create the following files:

  1. components/forms/events/view/create-access-code.js
  2. templates/components/forms/events/view/create-access-code.hbs
  3. tests/integration/components/forms/events/view/create-access-code-test.js

Create-access-code.hbs

This file includes the handlebar syntax to design the front end of the access code component. The whole template is nested inside the Semantic UI’s form class. Some of the helpers used are as follows:

  • Ember Input Helper: It has been used extensively throughout the template in order to take input from the event organizer. For e.g.:
{{input type=‘text’ name=‘access_code’ value=data.code}}
  • Semantic Radio Button: The semantic radio button has been used in order to allow the organizer to select the state of the access-code. He/She can choose if the access-code is active or inactive.

<div class="grouped inline fields">
    <label class="required">{{t 'Status'}}</label>
    <div class="field">
      {{ui-radio current=data.isActive name='status' label='Active' value='true' onChange=(action (mut data.isActive))}}
    </div>
    <div class="field">
      {{ui-radio name='status' label='Inactive' value='false' current=data.isActive onChange=(action (mut data.isActive))}}
    </div>
  </div>
  • Date Time Picker: The organizer can set the validity of the access code as well. We have used date-picker and time-picker components which were already created in the project. They have been used in the following way:

<div class="fields">
        <div class="wide field {{if device.isMobile 'sixteen' 'five'}}">
          <label>{{t 'Valid from'}}</label>
          {{widgets/forms/date-picker id='start_date' value=data.validFromDate rangePosition='start'}}
          <div class="ui hidden divider"></div>
          {{widgets/forms/time-picker id='start_time' value=data.validFromTime rangePosition='start'}}
        </div>
        <div class="wide field {{if device.isMobile 'sixteen' 'five'}}">
          <label>{{t 'Expires on'}}</label>
          {{widgets/forms/date-picker id='end_date' value=data.validTillDate rangePosition='end'}}
          <div class="ui hidden divider"></div>
          {{widgets/forms/time-picker id='end_time' value=data.validTillTime rangePosition='end'}}
        </div>
      </div>

Create-access-code.js

We use this file as the core of the component and handle the following use cases:

  1. Validation of the input. We show warning if something is wrong.
  2. Actions used by the various elements of the templates.
  3. Providing the link for the access-code.
  4. Saving the access-code.

accessCode : '',
  accessUrl  : computed('data.code', function() {
    const params = this.get('router._router.currentState.routerJsState.params');
    this.set('data.accessUrl', location.origin + this.get('router').urlFor('public', params['events.view'].event_id, { queryParams: { access_code: this.get('data.code') } })) ;
    return this.get('data.accessUrl');
  }),
  actions: {
    toggleAllSelection(allTicketTypesChecked) {
      this.set('allTicketTypesChecked', allTicketTypesChecked);
      if (allTicketTypesChecked) {
        this.set('data.tickets', this.get('data.event.tickets').slice());
      }
    },
    updateTicketSelections(newSelection) {
      if (newSelection.length === this.get('data.event.tickets').length) {
        this.set('allTicketTypesChecked', true);
      } else {
        this.set('allTicketTypesChecked', false);
      }
    },
    submit(data) {
      this.onValid(() => {
        data.save()
          .then(() => {
            this.get('notify').success(this.get('l10n').t('Access code has been successfully created.'));
            this.get('router').transitionTo('events.view.tickets.access-codes');
          })
          .catch(() => {
            this.get('notify').error(this.get('l10n').t('An unexpected error has occurred. Access code cannot be created.'));
          });
      });
    }
  }

Create-access-code-test.js

We can specify the tests in order to test the compatibility of the component here. For now, we will just write a simple test which checks if the component is rendered or not.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Controller | events/view/tickets/access codes/create', function(hooks) {
  setupTest(hooks);


  test('it exists', function(assert) {
    let controller = this.owner.lookup('controller:events/view/tickets/access-codes/create');
    assert.ok(controller);
  });
});

Now that we are done, setting up our component, we just need to add it in our application. We can achieve that using the following:

{{forms/events/view/create-access-code data=model}}

The model passed to the component is fetched from the create-access-code.js file.

References

Continue Reading
  • 1
  • 2
Close Menu