Designing PayTM Checkout Components

This summer, Open Event project has 3 different payment gateways integrated in its system enabling the user base a wider base of options to buy their tickets and pay the organizers and hence making the platform more user friendly. In the initial period Omise gateway was implemented and properly documented in the first phase while Alipay was subsequently implemented in the middle of the coding period. In the late phase, the focus has been shifted to Indian payment gateways and PayTM came out as prime choice considering it’s popularity and ease of integration with the existing technology stack.

This requires two different modals to be added to frontend project to felicitate the open-event-server hits being made on PayTM’s API services.

The first modal’s skeleton design is to check paytm wallets as the payment option and acquire the mobile number to be used.

// app/templates/components/modals/paytm-payment-options.hbs

<
div class="header">
{{t 'Amount to be paid:'}} {{currency-symbol currency}} {{amount}}
</
div>

<
div class="content">
<
div class="muted small text">
  {{t 'Select an option to pay'}}
</
div>
<
form class="ui form" autocomplete="off" {{action 'openOTPController' on='submit' preventDefault=true}}>
  <
div class="field">
    {{ui-radio name='payment_mode' value='paytm' onChange=(action (mut isWalletSelected))}}
    {{t 'Paytm Wallet'}}<
img src="/images/payment-logos/paytm.png" alt="paytm">
  </
div>
  {{#
if isWalletSelected}}
    <
div class="field">
      <
div class="label">
        {{t 'Please enter your Paytm registered Mobile Number to continue'}}
      </
div>
      {{input type='number' id='mobile_number' value=mobileNumber required=true}}
    </
div>
  {{/
if}}
</
form>
</
div>

<
div class="actions">
<
button type="button" class="ui black button" {{action 'close'}}>
  {{t 'Cancel'}}
</
button>
<
button {{action openOTPController}} class="ui green button" disabled={{not isWalletSelected}}>
  {{t 'Proceed'}}
</
button>
</
div>

This simple modal design implementation resulted in the skeleton design of the first modal which can be seen as following:

OTP sending modal

The second modal required a simple API hit integration which will be validating the acquired OTP. This was designed rather simply with the following snippet:

// app/templates/components/modals/paytm-otp.hbs
<
div class="header">
{{t 'Amount to be paid:'}} {{currency-symbol currency}} {{amount}}
</
div>

<
div class="content">
{{t 'Enter OTP sent to mobile number'}}
<
form class="ui form" autocomplete="off">
  <
div class="field">
    {{input type='number' id='otp' value=otp required=true}}
  </
div>
</
form>
</
div>
<
div class="actions">
<
button type="button" class="ui black button" {{action 'close'}}>
  {{t 'Cancel'}}
</
button>
<
button class="ui green button">
  {{t 'Verify'}}
</
button>
</
div>
OTP confirmation modal

These modals were controlled by adding corresponding pop-up logic in the pending.js controller focusing on the clicks on Proceed and verify buttons respectively.

// app/controllers/orders/pending.js
  openPaytmModal() {
    // Model controller for PaytmModal
    this.setProperties({
      'isPaytmModalOpen': true
    });
  },

  openOTPController() {
    // Modal controller for OTP step
    this.setProperties({
      'isPaytmModalOpen' : false,
      'isOTPModalOpen'   : true
    });
  }

This concludes the design walk through of custom PayTM checkout modals. These will be integrated with the APIs to complete the backend workflow and hence adding PayTM as another payment option!

Resources

Related Work and Code Repository

Continue Reading Designing PayTM Checkout Components

Migrating to Next generation of Open Event from Legacy

Screen Shot 2017-07-18 at 2.03.19 PM.png

This blog article will illustrate how, after significant refactors, eventyay.com was switched from open-event-legacy to the new version of open-event which has a decoupled open-event-API-server and frontend. We will discuss this switch from two aspects – the database migration and the DevOps logistic, as we moved from google cloud to a  more economically feasible platform – hetzner cloud.

Downloading a copy of the legacy database from Kubernetes pod on the google cloud platform

The first step, was to obtain a copy of the database being used in production. The database was stored inside a pod named Postgres of the Kubernetes cluster.

gcloud login
gcloud container clusters get-credentials vintage-cluster --zone us-west1-a --project eventyay

These commands initialized and authenticated the gcloud sdk with the project eventyay.

Next, to gain bash access to the postgres pod, exec of kubectl CLI was used.

kubectl exec -it postgres — /bin/bash

The database in both the new and legacy versions was postgresSql. Using the pg_dump functionality, a database can be dumped into a transferable file

kubectl exec -it postgres -- /bin/bash

However this file still resides on the kubernetes pod’s local storage  itself. The cp utility of kubectl CLI comes in handy to copy that file from pod to local storage. 

kubectl cp default/postgres:legacy-24-03-2019.pgsql ~/Downloads/

This command transfers the file to local storage. Now we have a database we can begin to refactor, for this we first need to import it into the postgres instance on the local machine, to take a peek inside the schema.

Psql -U postgres
Create database legacy_24_03_2019
Create database legacy_24_03_2019_reference
postgres legacy_24_03_2019 < ~/Downloads/legacy-24-03-2019.pgsql 

These commands dump the data inside the  legacy database into the newly created database inside the Postgres instance. The schema and architecture of the new version of server is different from the legacy database, hence DB has to go through migrations.

The last migration file shared by legacy and the next gen of server is corresponding to the migration ddaa6df27340.

However, it is important to note that the migrations branched from here onwards. Hence to downgrade the databse to the last legacy migration, we need to use the migrations folder of the legacy version of eventyay and not the original migrations directory

Assuming that the migrations from the legacy server stiored in a folder called migrations-legacy

mv migrations migrations-new
mv migrations-legacy migrations
Change the config in the local .env inside open-event-server to switch to this newly created database.
9d21de792967
Ticket holder thing
python manage.py db downgrade ddaa6df27340

Then we upgrade to the latest instance of the db

Switch back the directories
(venv) Abhinavs-MacBook-Pro:open-event-server abhinav$ mv migrations migrations-legacy
(venv) Abhinavs-MacBook-Pro:open-event-server abhinav$ mv migrations-new migrations

Then we upgrade the database 

Python manage.py db upgrade

These commands migrate the database in principal, and we have a basic system with core functionality using legacy data but the database is not at all ready to be used in production yet. It is rife with bugs. The migrations file don’t cover each change, and some are outright breaking. Some have problems like comma separated values being converted into their own schemas  without any migration written for them. These issues were tackled separately.

Most significant of these was the scripts written to convert legacy comma separated data format of custom forms  into a separate schema.

Forms = [exported custom forms table of legacy]
n=2567
for (let i in forms){
//console.log(i);
a = JSON.parse(forms[i].speaker_form);
c = forms[i].event_id;
for (key in a){
//console.log(key);
mkey = key.toLowerCase().replace(/_([a-z])/,function(m){return m.toUpperCase();}).replace(/_/,'');
if(key=='name' || key=='email' || key == 'title' || key=='track'){
console.log("insert into custom_forms values("+n+","+c+", '" + mkey +"', 'speaker', true,"+ (a[key].include==1?true:false)+","+ (a[key].require==1?true:false)+","+"'text');");
}
else {
console.log("insert into custom_forms values("+n+","+c+", '" + mkey +"', 'speaker', false,"+ (a[key].include==1?true:false)+","+ (a[key].require==1?true:false)+","+"'text');");
}

n++;
}
}

Similar script had to be written for sessions

n=11987
for (let i in forms){
//console.log(i);
a = JSON.parse(forms[i].session_form);
c = forms[i].event_id;
for (key in a){
//console.log(key);
mkey = key.toLowerCase().replace(/_([a-z])/,function(m){return m.toUpperCase();}).replace(/_/,'');
if(key == 'title' || key=='track'){
console.log("insert into custom_forms values("+n+","+c+", '" + mkey +"', 'session', true,"+ (a[key].include==1?true:false)+","+ (a[key].require==1?true:false)+","+"'text');");
}
else {
console.log("insert into custom_forms values("+n+","+c+", '" + mkey +"', 'session', false,"+ (a[key].include==1?true:false)+","+ (a[key].require==1?true:false)+","+"'text');");
}

n++;
}
}

These scripts returned the sql queries required to insert these custom forms to corresponding tables. After some more miscellaneous fixes like this one, the migrations was fully complete and ready for production.

Resources 

Continue Reading Migrating to Next generation of Open Event from Legacy

Enable Server Configuration with Okhttp and Retrofit in Open Event Attendee Application

The open event attendee is an android app which allows users to discover events happening around the world using the Open Event Platform. It consumes the APIs of the open event server to get a list of available events and can get detailed information about them.

We are using default API for eventyay app. Server configuration is something when we replace backend API with a new one and perform the same applications with the different server. As it is a fully open-source project on F-droid, so we have enabled the server configuration field for the F-droid build variant. 

  • Retrofit and okhttp for network calls
  • Create a feasible UI and set the link to preferences
  • Create interceptor for changing API URL
  • Add interceptor in okhttp client builder
  • Conclusion
  • Resources 

Let’s analyze every step in detail.

Retrofit and Okhttp for Network Call

Using Retrofit for your Android app’s networking can make your life so much easier. However, Retrofit’s design requires a single Retrofit instance for each API with a different base URL. Consequently, if your app is talking to two or more APIs (under different URLs), you’ll need to deal with at least two Retrofit instances.

Retrofit is a type-safe REST client for Android, Java, and Kotlin developed by Square. The library provides a powerful framework for authenticating and interacting with APIs and sending network requests with OkHttp.

OkHttp communicating with the server- 

Design UI and set the link to preferences with MVVM

Create a simple dialog with a checkbox with default URL and a EditText:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <CheckBox
        android:id="@+id/urlCheckBox"
        android:layout_margin="@dimen/layout_margin_large"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <com.google.android.material.textfield.TextInputLayout           style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
        android:id="@+id/urlTextInputLayout"
        android:layout_margin="@dimen/layout_margin_large"
        android:hint="@string/other_url"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/urlEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

Handle visibility if the dialog and display for only F-droid build:

preferenceScreen.findPreference<PreferenceCategory>(getString(R.string.key_server_configuration))?.isVisible = BuildConfig.FLAVOR == FDROID_BUILD_FLAVOR

Set current API to preference screen:

preferenceScreen.findPreference<Preference>(getString(R.string.key_api_url))?.title =
            settingsViewModel.getApiUrl()

Get API from View model: 

fun getApiUrl(): String {
        return preference.getString(API_URL) ?: BuildConfig.DEFAULT_BASE_URL
    }

Setup alert dialog:

if (preference?.key == getString(R.string.key_api_url)) {
            showChangeApiDialog()
        }
private fun showChangeApiDialog() {
        val layout = layoutInflater.inflate(R.layout.dialog_api_configuration, null)
        layout.urlCheckBox.text = BuildConfig.DEFAULT_BASE_URL

        val dialog = AlertDialog.Builder(requireContext())
            .setView(layout)
            .setPositiveButton(getString(R.string.change)) { _, _ ->
                val url = if (layout.urlCheckBox.isChecked) BuildConfig.DEFAULT_BASE_URL
                                else layout.urlEditText.text.toString()
                if (url === settingsViewModel.getApiUrl()) return@setPositiveButton
                settingsViewModel.changeApiUrl(url)
                view?.snackbar("API URL changed to $url")
                findNavController().popBackStack(R.id.eventsFragment, false)
            }
            .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() }
            .setCancelable(false)
            .show()
        dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false

        layout.urlCheckBox.setOnCheckedChangeListener { _, isChecked ->
            layout.urlTextInputLayout.isVisible = !isChecked
            dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = isChecked
        }

Set URL to preferences in the view model and end current session:

fun changeApiUrl(url: String) {
        preference.putString(API_URL, url)
        logout()
    }

Create Interceptor to Handle New API URL

Here default API URL is set to the retrofit already: 

Retrofit.Builder()
            .client(get())
            .baseUrl(baseUrl)
            .build()

As we discussed earlier OkHttp handles every network call for the application. So here we track the URL host from the okhttp interceptor. If the URL host is equaled to the default API URL host, then we can say that it is an API call and then we can replace same with the host getting from preferences if it is not null and set the interceptor to okhttp client builder.

Create host selection interceptor class to return interceptor with the API URL:

class HostSelectionInterceptor(private val preference: Preference) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        var original = chain.request()
        val httpUrl = preference.getString(API_URL)?.toHttpUrlOrNull()
        if (original.url.host == BuildConfig.DEFAULT_BASE_URL.toHttpUrlOrNull()?.host && httpUrl != null) {
            val newUrl =
                original.url.newBuilder()
                    .scheme(httpUrl.scheme)
                    .host(httpUrl.host)
                    .port(httpUrl.port)
                    .build()
            original = original.newBuilder()
                .url(newUrl)
                .build()
        }
        return chain.proceed(original)
    }
}

Set the interceptor to okhttp client builder:

val builder = OkHttpClient().newBuilder()
            .addInterceptor(HostSelectionInterceptor(get()))

GIF

In a Nutshell

Server configuration provides better user experience for open-source platform and developer, as they can mention their own server and test it.

Resources

OkHttp client with retrofit: https://futurestud.io/tutorials/retrofit-2-share-okhttp-client-and-converters-between-retrofit-instances

Tags

Eventyay, open-event, OkHttp, Retrofit, FOSSASIA, GSoC, Android, Kotlin

Continue Reading Enable Server Configuration with Okhttp and Retrofit in Open Event Attendee Application

CRUD operations on Config Keys in Admin Panel of SUSI.AI

SUSI.AI Admin Panel now allows the Admin to create, read, update and delete config keys present in system settings. Config keys are API keys which are used to link the application to third party services like Google Maps, Google ReCaptcha, Google Analytics, Matomo, etc. The API key is a unique identifier that is used to authenticate requests associated with the project for usage and billing purposes.

CRUD Operations

Create Config Key

To create a config key click on “Add Config Key” Button, a dialog opens up which has two field Key Name and Key Value. this.props.actions.openModal opens up the shared Dialog Modal. On clicking on “Create”, the createApiKey is called which takes in the two parameters.

handleCreate = () => {
   this.props.actions.openModal({
     modalType: 'createSystemSettings',
     type: 'Create',
     handleConfirm: this.confirmUpdate,
     keyName: this.state.keyName,
     keyValue: this.state.keyValue,
     handleClose: this.props.actions.closeModal,
   });
 };
 handleSave = () => {
   const { keyName, keyValue } = this.state;
   const { handleConfirm } = this.props;
   createApiKey({ keyName, keyValue })
     .then(() => handleConfirm())
     .catch(error => {
       console.log(error);
     });
 }; 

Read Config Key

API endpoint fetchApiKeys is called on componentDidMount and when Config Key is created, updated or deleted.

 fetchApiKeys = () => {
   fetchApiKeys()
     .then(payload => {
       let apiKeys = [];
       let i = 1;
       let keys = Object.keys(payload.keys);
       keys.forEach(j => {
         const apiKey = {
           serialNum: i,
           keyName: j,
           value: payload.keys[j],
         };
         ++i;
         apiKeys.push(apiKey);
       });
       this.setState({
         apiKeys: apiKeys,
         loading: false,
       });
     })
     .catch(error => {
       console.log(error);
     });
 };

Update Config Key

To Update a config key click on edit from the actions column, Update Config Key dialog opens up which allows you to edit the key value. On clicking on update, the createApiKey API is called.

 handleUpdate = row => {
   this.props.actions.openModal({
     modalType: 'updateSystemSettings',
     type: 'Update',
     keyName: row.keyName,
     keyValue: row.value,
     handleConfirm: this.confirmUpdate,
     handleClose: this.props.actions.closeModal,
   });
 };

Delete Config Key

To delete a config key click on delete from actions column, delete config key confirmation dialog opens up. On clicking on Delete, the deleteApiKey is called which takes in key name as parameter.

 handleDelete = row => {
   this.setState({ keyName: row.keyName });
   this.props.actions.openModal({
     modalType: 'deleteSystemSettings',
     keyName: row.keyName,
     handleConfirm: this.confirmDelete,
     handleClose: this.props.actions.closeModal,
   });
 };
 confirmDelete = () => {
   const { keyName } = this.state;
   deleteApiKey({ keyName })
     .then(this.fetchApiKeys)
     .catch(error => {
       console.log(error);
     });
   this.props.actions.closeModal();
 };

In conclusion, CRUD operations of Config Keys help admins to manage third party services. With these operations the admin can manage the API keys of various services without having to look for them in the backend.

Resources

Continue Reading CRUD operations on Config Keys in Admin Panel of SUSI.AI

Feature to generate Config File in PSLab Android application

In this blog, I will explain the feature to generate “Config File” in PSLab Android Application 

What is a Config File?

The main aim of this feature is to make PSLab board a self data logger, which would read user-defined configs from a config file stored on SD card connected to PSLab board and based on instrument, parameters and time interval stored in config file PSLab board would automatically log those values. 

Now as the first step of this feature, an option is added to PSLab Android application, where user can create a config file. User can select an instrument, parameters associated with that instrument and time interval. With this feature, user can easily generate a config file which can later be used by PSLab board for logging.

User Interface

The option to generate a config file is given in the side navigation menu on the main screen. 

(Figure 1: Generate Config file menu)

Once the user selects the “Generate Config File” option, the user will be directed to the following screen where user can create a config file with intended parameters

(Figure 2: Generate Config File UI)

As can be seen in the screenshot above the user can select instruments for which the config file needs to be created from a drop-down menu. User can specify the time interval, for which the data should be logged by the PSLab board. Based on the instrument selected by the user corresponding parameters will be shown at the bottom. User can select whichever parameters are required and click on “CREATE CONFIG FILE” button and a config file will be saved on device local storage. 

A config file for Oscilloscope with 25-sec interval and CH1, CH2 and CH3 parameters would look something like below,

(Figure 3: Sample config File )

Implementation

When a user clicks on Create Config File button, First we check whether the user has provided a time interval, if not a toast message appears to let the user know that time interval is missing. This is done using the following lines of code,

createConfigFileBtn.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
     interval = intervalEditText.getText().toString();
     if (interval.length() == 0) {
        Toast.makeText(CreateConfigActivity.this, getResources().getString(R.string.no_interval_message), Toast.LENGTH_SHORT).show();
                }

Once the user sets the time interval and selects the parameters, the following lines of code generates a string array containing params selected by the user.

ArrayList<String> selectedParamsList = new ArrayList<>();
for (int i = 0; i < paramsListContainer.getChildCount(); i ++) {
    CheckBox checkBox = (CheckBox) paramsListContainer.getChildAt(i);
    if (checkBox.isChecked()) {
       selectedParamsList.add(instrumentParamsList.get(selectedItem)[i]);
    }
}

After we have the list of selected parameters we call the following function to create the config file

private void createConfigFile(ArrayList<String> params) {
        String instrumentName = instrumentsList.get(selectedItem);
        String fileName = "pslab_config.txt";
        String basepath = Environment.getExternalStorageDirectory().getAbsolutePath();

        File baseDirectory = new File(basepath + File.separator + CSVLogger.CSV_DIRECTORY);
        if (!baseDirectory.exists()) {
            try {
                baseDirectory.mkdir();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        File configFile = new File(basepath + File.separator + CSVLogger.CSV_DIRECTORY + File.separator + fileName);
        if (!configFile.exists()) {
            try {
                configFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            FileWriter writer = new FileWriter(configFile);
            writer.write("instrument: " + instrumentName + "\n");
            writer.write("interval: " + interval + " " + intervalUnit + "\n");
            String param = String.join(",", params);
            writer.write("params: " + param);
            writer.flush();
            writer.close();
            CustomSnackBar.showSnackBar(rootView, getString(R.string.file_created_success_message), null, null, Snackbar.LENGTH_SHORT);
        } catch (IOException e) {
            e.printStackTrace();
            CustomSnackBar.showSnackBar(rootView, getString(R.string.file_created_fail_message), null, null, Snackbar.LENGTH_SHORT);
        }

    }

In the first part of this function, we check whether there exists a PSLab directory in the local storage of the device, if not the directory is created. After that, we create a file named “pslab_config.txt”. After that, we use FileWriter to write data to the file. 

In a nutshell with this feature user can create config files easily. The following GIF demonstrated this functionality.

(Figure 4: GIF of the functionality)

References

Tags: PSLab, Android, GSoC 19, Config File, data logger

Continue Reading Feature to generate Config File in PSLab Android application

Implementing Render Route & Security Checks for Attendee Tickets

This blog post explains the requirements & implementation details of a secure route over which the tickets could be served in the Open Event Project (Eventyay). Eventyay is the Open Event management solution using standardized event formats developed at FOSSASIA. Sometimes, tickets of a user can be utilized in the process of fraudulent actions. To prevent this, security is of the utmost importance.

Prior to this feature, anonymous/unauthorized users were able to access the tickets which belonged to another user with a simple link. There was no provision of any authentication check.  An additional problem with the tickets were the storage methodology where the tickets were stored in a top-level folder which was not protected. Therefore, there was a necessity to implement a flask route which could check if the user was authenticated, the ticket belonged to the authorized user/admin/organizer of the event and provide proper exceptions in other cases. 

Ticket completion page with option to download tickets

When the user places an order and it goes through successfully,the ticket is generated, stored in a protected folder and the user is redirected to the order completion page where they would be able to download their tickets. When the user clicks on the Download Tickets Button, the ticket_blueprint route is triggered.

@ticket_blueprint.route('/tickets/<string:order_identifier>')
@jwt_required
def ticket_attendee_authorized(order_identifier):
    if current_user:
        try:
            order = Order.query.filter_by(identifier=order_identifier).first()
        except NoResultFound:
            return NotFoundError({'source': ''}, 'This ticket is not associated with any order').respond()
        if current_user.can_download_tickets(order):
            key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
            file_path = '../generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
            try:
                return return_file('ticket', file_path, order_identifier)
            except FileNotFoundError:
                create_pdf_tickets_for_holder(order)
                return return_file('ticket', file_path, order_identifier)
        else:
            return ForbiddenError({'source': ''}, 'Unauthorized Access').respond()
    else:
        return ForbiddenError({'source': ''}, 'Authentication Required to access ticket').respond()

                         tickets_route – the logic pertaining to security module for attendee tickets

The function associated with the ticket downloads queries the Order model using the order identifier as a key. Then, it checks if the current authenticated user is either a staff member, the owner of the ticket or the organizer of the ticket. If it passes this check, the file path is generated and tickets are downloaded using the return_tickets function.

In the return_tickets function, we utilize the send_file function imported from Flask and wrap it with flask’s make_response function. In addition to that, we attach headers to specify that it is an attachment and add an appropriate name to it.

def return_file(file_name_prefix, file_path, identifier):
    response = make_response(send_file(file_path))
    response.headers['Content-Disposition'] = 'attachment; filename=%s-%s.pdf' % (file_name_prefix, identifier)
    return response

return_tickets function – sends the file as a make_response with appropriate headers

When it comes to exception handling, at each stage whenever a ticket is not to be found while querying or the authentication check fails, a proper exception is thrown to the user. For example, at the step where an attempt is made to return the file using file path after authentication, if the tickets are NotFound, the tickets are generated on the fly. 

 def can_download_tickets(self, order):
        permissible_users = [holder.id for holder in order.ticket_holders] + [order.user.id]
        if self.is_staff or self.has_event_access(order.event.id) or self.id in permissible_users:
            return True
        return False

can_download_tickets – check for proper ticket access 

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, SQLAlchemy, Open Event, Python, JWT

Continue Reading Implementing Render Route & Security Checks for Attendee Tickets

Creating SMTP as a Fallback Function to Ensure Emails Work

This blog post explains the solution to scenarios pertaining to failure/ unavailability of Sendgrid service when an attempt is made to send emails in Eventyay. Eventyay is the outstanding Open Event management solution using standardized event formats developed at FOSSASIA.

Currently, the Open Event Project utilizes 2 protocols namely, SendGrid and SMTP. Previously, they could be configured only be configured individually. If either protocol failed, there was no provision for a backup and the task to send the email would fail. 

Therefore, there was a necessity to develop a feature where the SMTP protocol, which is usually more reliable than 3rd party services, could act as a backup when sendgrid server is unavailable or the allotted quota has been exceeded.

def check_smtp_config(smtp_encryption):
    """
    Checks config of SMTP
    """
    config = {
                'host': get_settings()['smtp_host'],
                'username': get_settings()['smtp_username'],
                'password': get_settings()['smtp_password'],
                'encryption': smtp_encryption,
                'port': get_settings()['smtp_port'],
    }
    for field in config:
        if field is None:
            return False
    return True

  Function to check if SMTP has been properly configured

The main principle which was followed to implement this feature was to prevent sending emails when SMTP is not configured. Ergo, a function was implemented to check if the host, username, password, encryption and port was present in the model before proceeding. If this was configured properly, we move on to determining the protocol which was enabled. For this, we have 2 separate celery tasks, one for SMTP and the other for Sendgrid. 

@celery.task(name='send.email.post.sendgrid')
def send_email_task_sendgrid(payload, headers, smtp_config):
    try:
        message = Mail(from_email=From(payload['from'], payload['fromname']),
                       to_emails=payload['to'],
                       subject=payload['subject'],
                       html_content=payload["html"])
        if payload['attachments'] is not None:
            for attachment in payload['attachments']:
                with open(attachment, 'rb') as f:
                    file_data = f.read()
                    f.close()
                encoded = base64.b64encode(file_data).decode()
                attachment = Attachment()
                attachment.file_content = FileContent(encoded)
                attachment.file_type = FileType('application/pdf')
                attachment.file_name = FileName(payload['to'])
                attachment.disposition = Disposition('attachment')
                message.add_attachment(attachment)
        sendgrid_client = SendGridAPIClient(get_settings()['sendgrid_key'])
        logging.info('Sending an email regarding {} on behalf of {}'.format(payload["subject"], payload["from"]))
        sendgrid_client.send(message)
        logging.info('Email sent successfully')
    except urllib.error.HTTPError as e:
        if e.code == 429:
            logging.warning("Sendgrid quota has exceeded")
            send_email_task_smtp.delay(payload=payload, headers=None, smtp_config=smtp_config)
        elif e.code == 554:
            empty_attachments_send(sendgrid_client, message)
        else:
            logging.exception("The following error has occurred with sendgrid-{}".format(str(e)))


@celery.task(name='send.email.post.smtp')
def send_email_task_smtp(payload, smtp_config, headers=None):
    mailer_config = {
            'transport': {
                'use': 'smtp',
                'host': smtp_config['host'],
                'username': smtp_config['username'],
                'password': smtp_config['password'],
                'tls': smtp_config['encryption'],
                'port': smtp_config['port']
            }
        }

    try:
        mailer = Mailer(mailer_config)
        mailer.start()
        message = Message(author=payload['from'], to=payload['to'])
        message.subject = payload['subject']
        message.plain = strip_tags(payload['html'])
        message.rich = payload['html']
        if payload['attachments'] is not None:
            for attachment in payload['attachments']:
                message.attach(name=attachment)
        mailer.send(message)
        logging.info('Message sent via SMTP')
    except urllib.error.HTTPError as e:
        if e.code == 554:
            empty_attachments_send(mailer, message)
    mailer.stop()

Falling back to SMTP when the system has exceeded the sendgrid quota

Consider the function associated with the sendgrid task. The logic which sends the emails along with the payload is present in a try/catch block. When an exception occurs while attempting to send the email, it is caught via the requests library and checks for the HTTP code. If the code is determined as 429, this implies that there were TOO_MANY_REQUESTS going through or otherwise in sendgrid lingo, it means that you’ve exceeded your quota. In this case, we will not stop sending the email, rather, we would alternate to a backup solution of sending it via the SMTP protocol. In this way, we can ensure that emails would work without any kind of hassle.

def send_email(to, action, subject, html, attachments=None):
    """
    Sends email and records it in DB
    """
    from .tasks import send_email_task_sendgrid, send_email_task_smtp
    if not string_empty(to):
        email_service = get_settings()['email_service']
        email_from_name = get_settings()['email_from_name']
        if email_service == 'smtp':
            email_from = email_from_name + '<' + get_settings()['email_from'] + '>'
        else:
            email_from = get_settings()['email_from']
        payload = {
            'to': to,
            'from': email_from,
            'subject': subject,
            'html': html,
            'attachments': attachments
        }

        if not current_app.config['TESTING']:
            smtp_encryption = get_settings()['smtp_encryption']
            if smtp_encryption == 'tls':
                smtp_encryption = 'required'
            elif smtp_encryption == 'ssl':
                smtp_encryption = 'ssl'
            elif smtp_encryption == 'tls_optional':
                smtp_encryption = 'optional'
            else:
                smtp_encryption = 'none'

            smtp_config = {
                'host': get_settings()['smtp_host'],
                'username': get_settings()['smtp_username'],
                'password': get_settings()['smtp_password'],
                'encryption': smtp_encryption,
                'port': get_settings()['smtp_port'],
            }
            smtp_status = check_smtp_config(smtp_encryption)
            if smtp_status:
                if email_service == 'smtp':
                    send_email_task_smtp.delay(payload=payload, headers=None, smtp_config=smtp_config)
                else:
                    key = get_settings().get('sendgrid_key')
                    if key:
                        headers = {
                            "Authorization": ("Bearer " + key),
                            "Content-Type": "application/json"
                        }
                        payload['fromname'] = email_from_name
                        send_email_task_sendgrid.delay(payload=payload, headers=headers, smtp_config=smtp_config)
                    else:
                        logging.exception('SMTP & sendgrid have not been configured properly')

            else:
                logging.exception('SMTP is not configured properly. Cannot send email.')
        # record_mail(to, action, subject, html)
        mail = Mail(
            recipient=to, action=action, subject=subject,
            message=html, time=datetime.utcnow()
        )

        save_to_db(mail, 'Mail Recorded')
        record_activity('mail_event', email=to, action=action, subject=subject)
    return True

  Fallback logic – SMTP/Sendgrid

Resources:

Related work and code repo:

Tags:

Eventyay, FOSSASIA, Flask, SMTP, Open Event, Sendgrid, Python

Continue Reading Creating SMTP as a Fallback Function to Ensure Emails Work

Allowing Event Owner to Transfer an event in Eventyay

This blog post will showcase an option which can be used by an event owner to transfer his event to another user in Open Event Frontend. Till the invited user accepts the invitation, the previous owner will have all the rights but as soon as the invited user becomes the new owner of the event the previous owner will cease to have any control over the event.

It is a 2-step process just to ensure that user doesn’t transfers the event accidentally.

The user needs to go to Settings option of the event. The user will get an option to transfer event in the form of a red button along with the following text:

Transfer ownership of this event to another user. You’ll lose all the owner rights once they accept the ownership.
Option to transfer event in Open Event Frontend

When a user clicks on the option to transfer the event, a modal pops up asking the user to confirm the event name. Once user fills in correct event name the Proceed button becomes active.

Modal to confirm event name

The code snippet which triggers the action to open the modal event-transfer-modal is given below:

<button {{action 'openEventTransferModal' model.event.id
model.event.name}} class='ui red button'>
   {{t 'Transfer Ownership'}}
</button>
openEventTransferModal(id, name) {    
    this.setProperties({
       'isEventTransferModalOpen' : true,
       'confirmEventName'         : '',
       'eventId'                  : id,
       'eventName'                : name
     });
 }

The code snippet which takes care of event name confirmation to make Proceed button active:

isNameDifferent  : computed('confirmEventName',  
      'eventName', function() {   
           return this.eventName ? 
           this.confirmEventName.toLowerCase() !==
           this.eventName.toLowerCase() : true;
       })

When user confirms the event name and hits Proceed button, a new modal appears which asks users to fill in the email of the user to whom the event is to be transferred. Also, the user needs to check a checkbox to ensure that he/she agrees to the terms of event transferring.

Final confirmation to transfer the event

The code snippet which triggers the action to open the modal confirm-event-transfer-modal is given below:

<button {{action openConfirmEventTransferModal}} class="ui red button {{if isNameDifferent 'disabled'}}">   
    {{t 'Proceed'}}
</button>
openConfirmEventTransferModal() {     
     const currentInvite =
             this.model.roleInvites.createRecord({});
     let { roles } = this.model;
     for (const role of roles ? roles.toArray() : [])    
     {
         if (role.name === 'owner') {
             currentInvite.set('role', role);
         }
     }
     this.setProperties({
         'isEventTransferModalOpen'        : false,
         'isConfirmEventTransferModalOpen' : true,
         'checked'                         : false,
         currentInvite
     });
   }

When the confirm-event-transfer-modal is to be opened, a new role invite is created and passed to the modal so that when the user fills in the email of the new owner, the role invite is simply updated by a PATCH request. 

When the user fills in email ID and enters Transfer button, the transferEvent() function is called.

async transferEvent() {
     try {
       this.set('isLoading', true);
       this.currentInvite.set('roleName', 'owner');
       await this.currentInvite.save();
       this.setProperties({
         'isConfirmEventTransferModalOpen' : false,
         'checked'                         : false
       });
       this.notify.success(this.l10n.t('Owner Role Invite sent
       successfully.'));
     } catch (error) {
       this.notify.error(this.l10n.t(error.message));
     }
     this.set('isLoading', false);
   }
 }

The transferEvent() function updates the role invited created while opening of confirm-event-transfer-modal and then save() function is called upon the role invite. All the modals are then closed and role invite mail is sent to the new owner. When the new owner clicks on the link in the mail and accepts the invite, event is transferred to him/her and the previous owner is deprived of any control over the event.

Resources:

Related work and code repo:

Continue Reading Allowing Event Owner to Transfer an event in Eventyay

Migration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Open Event Organizer App (Eventyay Organizer App) is the Android app used by event organizers to create and manage events on the Eventyay platform as well as check-in and check-out attendees along with other functionalities. The app used the MVP (Model-View-Presenter) architecture and is being ported to MVVM (Model-View-ViewModel). This article will explain the procedure of migrating MVP to MVVM architecture and implementing LiveData. 

Why migrate to MVVM?

The MVVM architecture is designed to store and manage UI-related data in a lifecycle conscious way. Configuration changes such as screen rotations are handled properly by ViewModels.

Tight Coupling:

The issue of tight coupling is resolved since only the View holds the reference to ViewModel and not vice versa. A single View can hold references to multiple ViewModels.

Testability:

Since Presenters are hard bound to Views, writing unit tests becomes slightly difficult as there is a dependency of a View.

ViewModels are more unit test friendly as they can be independently tested. There is no dependency of the View.

Here, the implementation is being described with the example of About Event module in the Open Event Organizer App.

First step is the creation of a new class AboutEventViewModel which extends ViewModel.

@Binds
@IntoMap
@ViewModelKey(AboutEventViewModel.class)
public abstract ViewModel bindAboutEventViewModel(AboutEventViewModel aboutEventViewModel);

The new ViewModel has to be added to the ViewModelModule:

Constructor for the ViewModel:

@Inject
public AboutEventViewModel(EventRepository eventRepository,  CopyrightRepository copyrightRepository,
DatabaseChangeListener<Copyright> copyrightChangeListener) {
    this.eventRepository = eventRepository;
    this.copyrightRepository = copyrightRepository;
    this.copyrightChangeListener = copyrightChangeListener;

    eventId = ContextManager.getSelectedEvent().getId();
}

We are using Dagger2 for dependency injection. 

LiveData

LiveData is a lifecycle-aware data holder with the observer pattern.

When we have a LiveData object (e.g. list of attendees), we can add some LifecycleOwner (it can be Activity or Fragment) as an observer. Using this:

The Activity or Fragment will remain updated with the data changes.

Observers are only notified if they are in the STARTED or RESUMED state which is also known as the active state. This prevents memory leaks and NullPointerExceptions because inactive observers are not notified about changes.

Now, let’s discuss about the implementation of LiveData. We will create objects of SingleEventLiveData<> class.

private final SingleEventLiveData<Boolean> progress = new SingleEventLiveData<>();
private final SingleEventLiveData<String> error = new SingleEventLiveData<>();
private final SingleEventLiveData<Event> success = new SingleEventLiveData<>();
private final SingleEventLiveData<Copyright> showCopyright = new SingleEventLiveData<>();
private final SingleEventLiveData<Boolean> changeCopyrightMenuItem = new SingleEventLiveData<>();
private final SingleEventLiveData<String> showCopyrightDeleted = new SingleEventLiveData<>();

The functions to get the LiveData objects:

public LiveData<Boolean> getProgress() {
    return progress;
}

public LiveData<Event> getSuccess() {
    return success;
}

public LiveData<String> getError() {
    return error;
}

public LiveData<Copyright> getShowCopyright() {
    return showCopyright;
}

public LiveData<Boolean> getChangeCopyrightMenuItem() {
    return changeCopyrightMenuItem;
}

public LiveData<String> getShowCopyrightDeleted() {
    return showCopyrightDeleted;
}

Now, we can remove getView() methods and instead, these objects will be used to call various methods defined in the fragment.

Let’s discuss the changes required in the AboutEventFragment now.

The Fragment will have ViewModelProvider.Factory injected.

@Inject
ViewModelProvider.Factory viewModelFactory;

Declare an object of the ViewModel.

private AboutEventViewModel aboutEventViewModel;

Then, in onCreateView(), viewModelFactory will be passed to the ViewModelProviders.of() method as the factory, which is the second parameter.

aboutEventViewModel = ViewModelProviders.of(this, viewModelFactory).get(AboutEventViewModel.class);

Replace all references to the Presenter with references to the ViewModel.

Add the Fragment as an observer to the changes by adding the following in the onStart() method:

aboutEventViewModel.getProgress().observe(this, this::showProgress);
aboutEventViewModel.getSuccess().observe(this, this::showResult);
aboutEventViewModel.getError().observe(this, this::showError);
aboutEventViewModel.getShowCopyright().observe(this, this::showCopyright);
aboutEventViewModel.getChangeCopyrightMenuItem().observe(this, this::changeCopyrightMenuItem);
aboutEventViewModel.getShowCopyrightDeleted().observe(this, this::showCopyrightDeleted);

Two parameters are passed to the observe() method  –  first one is LifecycleOwner, which is our Fragment in this case. The second one is a callback along with a parameter and is used to call the required method.

With this, the implementation of MVVM and LiveData is brought to completion.

Resources:

Documentation: ViewModel, LiveData

Further reading:

Open Event Organizer App: Project repo, Play Store, F-Droid

Continue Reading Migration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Transmitting data from SD card through Arduino – Neurolab record feature

In the Neurolab-Android app, we have been using an Arduino board for development as a workaround to the actual Neurolab hardware which is currently in the manufacturing stages. Along with the Arduino, we use a Micro SD card module with an SD card attached containing a dataset file in it. This combination of the Arduino and the SD card module serves as the source of dataflow into the Android app.

Firstly, we need to get the Arduino programmed for reading the dataset file from the SD card in the SD card module. We program the Arduino using the Arduino IDE on any desktop platform.

1. Import required libraries before starting off, namely the SPI and SD libraries for serial communication and SD card related functions respectively. Let us also set up some constant values which are going to be used in various parts of the code needed to program the Arduino for our required purpose.

#include <SD.h>
#include <SPI.h>
int baudRate = 9600;
int chipSelect = 4;
String fileName = "Dataset1.csv";

The ‘chipSelect’ variable denotes the chip select pin number for the connected Micro SD card adapter (module).

The baud rate for the Arduino board has been set to 9600 as a default. The dataset stored in the SD card from which the data needs to be read has been named “Dataset1”.

2. Next, we will need to initialize the SD card to check even if an SD card is inserted or not in the SD card module. We create a function for this purpose named ‘initializeSDCard’ in the following way:

bool initializeSDCard() {
  if (!SD.begin(chipSelect)) { 
    return false;   }
  return true;
}

The function will return true if initialization was successful. An unsuccessful initialization may also be due to the fact of SD card corrupted apart from not being properly inserted into the module.

3. Now we will be opening the dataset file from the SD card, making it ready to be read from.

File openTheFileFromSDCard(String fileName) {
  File file = SD.open(fileName);  
  if (!file) {
    Serial.println("error opening: " + fileName);
    return file;
  }
  return file;
}

The function will take in the file name as an argument and open it from the SD card. If the file is not found in the SD card, it will simply print out an error message to the serial output. It returns the file object in a true or false context accordingly.

4. We are now going to read data from the dataset in the SD card line by line using a function. The dataset file instance is passed as an argument to this function. This file is read and transmitted over the serial output channel line by line with ‘\n’ as the line delimiter, skipping null lines.

void readFromSDCardToSerialOutputLineByLine(File file) {
  String line;
  while (file.available()) {
    line = file.readStringUntil('\n');
    line.trim();
    if (line != "") {
      Serial.println(line);
    }
  }
  Serial.println("Recorded");
  file.close();
}

This function will keep reading from the file untill it has nothing more i.e till the end of file.

5. Next, we update the setup function which is the function to be executed for the programming of the Arduino board. Here, we call our defined functions according to our need logical need.

void setup() {
  Serial.begin(baudRate);

  pinMode(chipSelect, OUTPUT);

  if (initializeSDCard()) {
    File file = openTheFileFromSDCard(fileName);   
    if (file) {
      readFromSDCardToSerialOutputLineByLine(file);
    }
  }
}

Here, we initialize the serial communication at 9600 bits per second (baud rate), specify the pin mode which is the chip select pin in our case.

6. We leave the loop( ) function module as it is, as we do not need any iterative routine which runs over and over while programming our Arduino.

We are good to go now. Connect the Arduino to the desktop, compile the code and upload it to the circuit board.

Once programmed, we can get the output of the data transmission on the serial monitor in the following way:

Note – In the picture, the dataset name is ‘k24bit’.

Hope this blog adds value to your software development skills.

References:

  1. https://www.hackerearth.com/blog/developers/arduino-programming-for-beginners/
  2. https://youtu.be/sS_oW81NweI
  3. https://www.arduino.cc/en/main/software 

Tags: FOSSASIA, GSOC19, Arduino, Neurolab, Programming, Hardware

Continue Reading Transmitting data from SD card through Arduino – Neurolab record feature