Upgrading Open Event to Use Sendgrid API v3

Sendgrid recently upgraded their web API to send emails, and support for previous versions was deprecated. As a result, Open Event Server’s mail sending tasks were rendered unsuccessful, because the requests they were sending to Sendgrid were not being processed. On top of that, it was also found out later that the existing Sendgrid API key on the development server was expired. This had to be fixed at the earliest because emails are a core part of Open Event functionality.

The existing way for emails to be sent via Sendgrid used to hit the endpoint “https://api.sendgrid.com/api/mail.send.json” to send emails. Also, the payload structure was as follows:

payload = {
    'to': to,
    'from': email_from,
    'subject': subject,
    'html': html
}

Also, a header  “Authorization”: “Bearer ” accompanied the above payload. However, Sendgrid changed the payload structure to be of the following format:

{

“personalizations”: [

{“to”: [

{“email”: “example@example.com“}

]

}

],

“from”: {

“email”: “example@example.com

},

“subject”: “Hello, World!”,

“content”: [

{

“type”: “text/plain”,

“value”: “Heya!”

}

]

}

Furthermore, the endpoint was changed to be “https://api.sendgrid.com/v3/mail/send”. To incorporate all these changes with the minimum number of modified lines in the codebase, it was required for that the structure change itself happens at a fairly low level. This was because there are lots of features in the server that perform a wide variety of email actions. Thus, it was clear that changing all of them will not be the most efficient thing to do. So the perfect place to implement the API changes was the function send_email() in mail.py, because all other higher-level email functions are built on top of this function. But this was not the only change, because this function itself used another function, called send_email_task() in tasks.py, specifically for sending email via Sendgrid. So, in conclusion, the header modifications were made in send_email() and payload structure as well as endpoint modifications were made within send_email_task(). This brought the server codebase back on track to send emails successfully. Finally, the key for development server was also renewed and added to its settings in the Heroku Postgres database.

Screenshots:

Screen Shot 2018-08-21 at 3.40.12 PM.png

Screen Shot 2018-08-21 at 3.40.32 PM.png

Resources

Continue ReadingUpgrading Open Event to Use Sendgrid API v3

Deploying loklak search on Heroku & Using config to store and load external URLs

It is really important to have a separate configuration for all the hardcoded URLs having multiple instances across the project. It would help in testing and comparing projects like loklak with a separate configuration for hardcoded URLs. We would also be discussing deployment of Angular based loklak on heroku through this blog post.

Creating shared URL config

The idea here is to export a const Object containing all the collective hardcoded URLs used inside the project. We would try to store similar/collective URLs inside a same key e.g. all the github URLs must go inside key: ‘github’.

export const defaultUrlConfig = {
	fossasia: {
		root: 'https://fossasia.org',
		blog: 'https://blog.fossasia.org'
	},
	loklak: {
		apiServer: 'https://api.loklak.org',
		apiBase: 'loklak.org',
		blog: 'http://blog.loklak.net',
		dev: 'https://dev.loklak.org',
		apps: 'https://apps.loklak.org'
	},
	github: {
		loklak: 'https://github.com/loklak',
		fossasia: 'https://github.com/fossasia'
	},
	phimpme: {
		root: 'https://phimp.me'
	},
	susper: {
		root: 'https://susper.com'
	},
	susiChat: {
		root: 'https://chat.susi.ai'
	},
	pslab: {
		root: 'https://pslab.fossasia.org'
	},
	eventyay: {
		root: 'https://eventyay.com'
	}
};

 

Storing URLs with a key instead of storing it inside a simple Array, makes it easier to add a new URL at required place and easy to modify the existing ones. Now, this configuration can easily be called inside any Component file.

Using config inside the component

Now, the work is really simple. We just need to import the configuration file inside the required Component and store/use directly the respective URLs.

First step would be to import the configuration file as follows:

import { defaultUrlConfig } from ‘../shared/url-config’;

 

Note: Respective path for url-config file for different components might differ.

Now, we would need to store the defaultUrlConfig inside a variable.

public configUrl = defaultUrlConfig;

 

At this point, we have all the configuration URLs which could be extracted from configUrl.

Displaying URLs in HTML

We would use Angular’s interpolation binding syntax to display the URLs from configUrl in HTML. Let’s say we want to display FOSSASIA’s github url in HTML, then we would simply need to do:

{{ configUrl.github.fossasia }}

 

This could be used as an example to to replace all the hardcoded URLs inside the project.

Deploying loklak search on Heroku

Note: I am skipping the initial steps of creating a project on heroku. It is very easy to setup a project on heroku, for initial steps please follow up here.

First step in this direction would be to add a server.js file in root directory and add the following express server code in it:

const express = require(‘express’);
const path = require(‘path’);
const app = express();
app.use(express.static(__dirname + ‘/dist/<name-of-app>’));
app.get(‘/*’, function(req,res) {
res.sendFile(path.join(__dirname+‘/dist/<name-of-app>/index.html’));
});
app.listen(process.env.PORT || 8080);

 

Second step would be to add the following commands inside package.json at respective attributes.

“postinstall”: “ng build –aot -prod”
“start”: “node server.js”

 

Testing

Click on the respective URL link inside the project UI to test the configuration for hardcoded URLs. To check the deployment on heroku, please open the following URLs:

Development branch: loklak-search-dev.herokuapp.com

Master branch: loklak-search.herokuapp.com

Resources

Continue ReadingDeploying loklak search on Heroku & Using config to store and load external URLs

Using Transitions API with Email Validation in Open Event Organizer Android App

Transitions in Material Design apps provide visual continuity. As the user navigates the app, views in the app change state. Motion and transformation reinforce the idea that interfaces are tangible, connecting common elements from one view to the next.

In the Open Event Organizer Android App, we need a transition from the Get Started screen such that, if the user email is registered, we transition to the Login Screen otherwise, we Transition to the Sign Up Screen. And the transition should be such that the email field is continuously visible. One more condition is that, if the email field is even varied by one character, we need to transition back to the Get Started Screen.

To implement this, we need to use Shared Elements from the Android Transitions API.

What are shared elements?

A shared element transition determines how views that are present in two fragments transition between them. For example, an image that is displayed on an ImageView on both Fragment A and Fragment B transitions from A to B when B becomes visible.

Fade transition is used to fade a view and ChangeBounds transition is used to move a view without changing its size.

To make a transition, we use the setupTransitionAnimations() function like this:

(Note that we have created our own Fade and ChangeBounds transitions and not using XML)

   public void setupTransitionAnimation(Fragment fragment) {
       Fade exitFade = new Fade();
       exitFade.setDuration(100);
       this.setExitTransition(exitFade);
       fragment.setReturnTransition(exitFade);

       ChangeBounds changeBoundsTransition = new ChangeBounds();
       fragment.setSharedElementEnterTransition(changeBoundsTransition);

       Fade enterFade = new Fade();
       enterFade.setStartDelay(300);
       enterFade.setDuration(300);
       fragment.setEnterTransition(enterFade);
       this.setReenterTransition(enterFade);
   }

Now, in order to detect, if the email field is touched and even changed by one character, we use the TextWatcher like this:

       binding.emailLayout.getEditText().addTextChangedListener(new TextWatcher() {

           @Override
           public void beforeTextChanged(CharSequence s, int start, int count, int after) {
               //do nothing
           }

           @Override
           public void onTextChanged(CharSequence s, int start, int before, int count) {
               if (start != 0) {
                   sharedViewModel.setEmail(s.toString());
                   getFragmentManager().popBackStack();
               }
           }

           @Override
           public void afterTextChanged(Editable s) {
               //do nothing

           }

       }     

So basically, the moment the text is changed, we add the changed email to the SharedViewModel so that it can be used in the other fragments, and then to start the transition, we pop the back stack using getFragmentManager().popBackStack();

This is what the result looks like:

Resources

  • Google – Android developer blog post:

https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html

Continue ReadingUsing Transitions API with Email Validation in Open Event Organizer Android App

Option to Rename an Image in Phimpme Android Application

In the Phimpme Android application, users can perform various operations on images such as editing an image, sharing an image, moving the image to another folder, printing a pdf version of the image and many more. However, another important functionality that has been implemented is the option to rename an image. So in this blog post, I will be discussing how we achieved the functionality to rename an image.

Step 1

First, we need to add an option in the overflow menu to rename the image being viewed. The option to rename an image has been added by implementing the following lines of code in the  menu_view_pager.xml file.

<item
  android:id=“@+id/rename_photo”
  app:showAsAction=“never”
  android:title=“@string/Rename”/>

Step 2

Now after the user chooses the option to rename any image from the overflow menu, an alert dialog with an edittext would be displayed to the user with the old name and provide the user to add a new name for the photo. The dialog box with edittext has been implemented with the following lines of XML code.

<android.support.v7.widget.CardView
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  app:cardCornerRadius=“2dp”
  android:id=“@+id/description_dialog_card”>
  <ScrollView
      android:layout_width=“match_parent”
      android:layout_height=“match_parent”>
      <LinearLayout
          android:layout_width=“match_parent”
          android:layout_height=“wrap_content”
          android:orientation=“vertical”>
          <TextView
              android:id=“@+id/description_dialog_title”
              android:layout_width=“match_parent”
              android:textColor=“@color/md_dark_primary_text”
              android:layout_height=“wrap_content”
              android:background=“@color/md_dark_appbar”
              android:padding=“24dp”
              android:text=“@string/type_description”
              android:textSize=“18sp”
              android:textStyle=“bold” />
          <LinearLayout
              android:id=“@+id/rl_description_dialog”
              android:layout_width=“match_parent”
              android:layout_height=“wrap_content”
              android:orientation=“horizontal”
              android:padding=“15dp”>
              <EditText
                  android:id=“@+id/description_edittxt”
                  android:layout_width=“fill_parent”
                  android:layout_height=“wrap_content”
                  android:padding=“15dp”
                  android:hint=“@string/description_hint”
                  android:textColorHint=“@color/grey”
                  android:layout_margin=“20dp”
                  android:gravity=“top|left”
                  android:inputType=“textCapSentences|textMultiLine”
                  android:maxLength=“2000”
                  android:maxLines=“4”
                  android:selectAllOnFocus=“true”/>
              <ImageButton
                  android:layout_width=“@dimen/mic_image”
                  android:layout_height=“@dimen/mic_image”
                  android:layout_alignRight=“@+id/description_edittxt”
                  app2:srcCompat=“@drawable/ic_mic_black”
                  android:layout_gravity=“center”
                  android:background=“@color/transparent”
                  android:paddingEnd=“10dp”
                  android:paddingTop=“12dp”
                  android:id=“@+id/voice_input”/>
          </LinearLayout>
      </LinearLayout>
  </ScrollView>
</android.support.v7.widget.CardView>

The screenshot displaying the dialog with edittext is provided below.

Step 3

Now after retrieving the new name entered by the user for the photo we would extract the extension of the old name which can be .jpg, .png etc. Thereafter we’d need to create a new File object passing in the new path of the image(the path folder remains the same only the image name gets changed to a new one) as the constructor parameter. Now using the renameTo method of the File class the old image file can be renamed. However, with this rename operation the image reference would not be automatically updated in the MediaStore database. So we’d need to delete the old file path from the Android system MediaStore database which keeps a URI reference to all the media files present on the device. At last, we’d need to invoke the MediaScanner class to scan all the media files so that the new file path of the renamed image is scanned and is picked up by the MediaStore database. This can be done with the help of an action intent to initiate the media scan action. The code changes implemented to perform the above-mentioned operation is given below.

public void onClick(View dialog) {
      if (editTextNewName.length() != 0) {
          int index = file.getPath().lastIndexOf(“/”);
          String path = file.getPath().substring(0, index);
          File newname = new File(path + “/” + editTextNewName.getText().toString() + “.” +
                  imageextension);
          if(file.renameTo(newname)){
              ContentResolver resolver = getApplicationContext().getContentResolver();
              resolver.delete(
                      MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Images.Media.DATA +
                              “=?”, new String[] { file.getAbsolutePath() });
              Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
              intent.setData(Uri.fromFile(newname));
              getApplicationContext().sendBroadcast(intent);
          }
          if(!allPhotoMode){
              int a = getAlbum().getCurrentMediaIndex();
              getAlbum().getMedia(a).setPath(newname.getPath());
          }else {
              listAll.get(current_image_pos).setPath(newname.getPath());
          }
          renameDialog.dismiss();
          SnackBarHandler.showWithBottomMargin(parentView, getString(R.string.rename_succes), navigationView
                  .getHeight());
      } else {
          SnackBarHandler.showWithBottomMargin(parentView, getString(R.string.insert_something),
                  navigationView.getHeight());
          editTextNewName.requestFocus();
      }
  }
});

This is how we have implemented the functionality to rename an image in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1.Android Developer documentation-https://developer.android.com/reference/java/io/File

2.Github-Phimpme Android Repository – https://github.com/fossasia/phimpme-android/

3.Renaming a file in java – http://stacktips.com/tutorials/java/how-to-delete-and-rename-a-file-in-java

 

Continue ReadingOption to Rename an Image in Phimpme Android Application

Add check-in restrictions to Open Event Organizer App

The Open Event Organizer Android App has the ability to scan and check-in attendees holding different ticket types for an event. But often there are cases when the attendees holding a particular ticket type need to be check-in restricted. It can be because of reasons such as facilitating entry of premium ticket holders before general ticket holders, or not allowing general ticket holders in a VIP queue.

To facilitate this, we have a field called ‘is-checkin-restricted’ for the entity Ticket. So when it is set to true, any check ins for the holder of that particular ticket type will be restricted. Let’s look at how this was implemented in the Orga App.

This is what we want to achieve:

Even though we needed it to be present in the settings screen, we needed it to be dynamic in nature as the types of tickets are themselves dynamic. This meant that we couldn’t achieve this using the plain old preference themes. We must create a whole new fragment for it and try to make it as similar to a preference theme as possible.

We need the following to create a dynamic tickets fragment:

  1. The fragment itself, which should implement the interfaces:  Progressive, Erroneous  to show progress and error.
  2. An Adapter and a ViewHolder
  3. A ViewModel

The fragment CheckinRestriction is similar to the TicketsFragment for the most part except for the part where we need to restrict check in. In the fragment we are providing a checkbox at the top to restrict check-in for all ticket types. So we need to setup click listeners not just for the checkbox, but for the whole view as well, like this:

binding.restrictAll.setOnClickListener(v -> {
       restrictAll(!binding.restrictAllCheckbox.isChecked());
   });
binding.restrictAllCheckbox.setOnClickListener(v -> {
       //checkbox already checked
       restrictAll(binding.restrictAllCheckbox.isChecked());
   });

The restrictAll() method restricts check-in for all ticket types by updating the view and updating the tickets using the ViewModel:

private void restrictAll(boolean toRestrict) {
   binding.restrictAllCheckbox.setChecked(toRestrict);
   ticketSettingsViewModel.updateAllTickets(toRestrict);
   ticketsAdapter.notifyDataSetChanged();
}

It’s also important to note here how we are handling the clicks in the ViewHolder for each ticket item:

public void bind(Ticket ticket) {
   binding.setTicket(ticket);
   View.OnClickListener listener = v -> {
       ticket.isCheckinRestricted = ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted;
       binding.ticketCheckbox.setChecked(ticket.isCheckinRestricted);
       updateTicketAction.push(ticket);
       binding.executePendingBindings();
   };
   itemView.setOnClickListener(listener);
   binding.ticketCheckbox.setOnClickListener(listener);
}

A method that is run each time in order to check if all the tickets are restricted and then accordingly tick the ‘restrict-all’ box.

private void checkRestrictAll() {
   if (ticketSettingsViewModel.getTickets() == null) {
       return;
   }
    boolean restrictAll = true;
    for (Ticket ticket : ticketSettingsViewModel.getTickets().getValue()) {
       if (ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted) {
           restrictAll = false;
           break;
       }
   }
   binding.restrictAllCheckbox.setChecked(restrictAll);
}

This is all of the code we need apart from the boilerplate code in order to successfully build a check-in-restrictions fragment.

Read more of the code here

Resources:

Continue ReadingAdd check-in restrictions to Open Event Organizer App

Implementing bottom navigation view in Organizer App

The state of the app before merging the pull request: #1009 was a simple RecyclerView with StickyHeaders from FastAdapter library.

We needed the app to look more like EventBrite so that we could help the users smoothly transition from EventBrite to Open Event Organizer Android App.

Eventbrite was using Tabs with View Pagers, which is a lot tedious because of its bugginess. View Pagers are buggy and would further require unnecessary architectural changes and additional classes for the Event-list module. This issue was raised in the last GSoC as well, but developers settled with using a single list view.

Using View Pagers requires managing presenters for each of the fragments being used.

So my mentor wanted us to use BottomNavigationView instead. In fact, this was the best solution we could come up with, and we’ll see why:

Step 1: Add BottomNavigationView to the layout:

<android.support.design.widget.BottomNavigationView
           android:id=“@+id/bottom_navigation”
           android:layout_width=“match_parent”
           android:layout_height=“wrap_content”
           android:layout_gravity=“bottom”
           android:background=“@android:color/white”
           app:itemIconTint=“@drawable/bottom_nav_selector”
           app:itemTextColor=“@drawable/bottom_nav_selector”
           app:menu=“@menu/event_list_bottom”
           app:layout_insetEdge=“bottom”/>

The above code for the layout is added to the end of fragment_event_list.xml and is to just add the Bottom Navigation layout for the xml. The attribute   app:menu takes the reference to the menu resource layout for the menu items to be used, which we’ll setup next:

Step 2: Declaring menu resource file for the BottomNavigationView:

The menu resource file is used to specify what items we need in the BottomNavigationView.

<?xml version=“1.0” encoding=“utf-8”?>
<menu xmlns:android=“http://schemas.android.com/apk/res/android”>
   <item android:id=“@+id/action_live”
       android:title=“@string/event_state_live”
       android:icon=“@drawable/ic_time”/>
   <item android:id=“@+id/action_upcoming”
       android:title=“@string/event_state_upcoming”
       android:icon=“@drawable/ic_clock”/>
   <item android:id=“@+id/action_past”
       android:title=“@string/event_state_past”
       android:icon=“@drawable/ic_info”/>
</menu>

Step 3: Implement Filterable in Adapter:

Filterable is an interface which is generally used with an adapter when there are many different values possible for the adapter.

class EventsListAdapter extends RecyclerView.Adapter<EventsListAdapter.EventRecyclerViewHolder> implements StickyRecyclerHeadersAdapter<HeaderViewHolder>, Filterable {

Filterable requires overriding the method: getFilter() which as the name suggests, provide the filter logic to be used.

@Override
public Filter getFilter() {
   return new Filter() {
       @Override
       protected FilterResults performFiltering(CharSequence constraint) {
           selectedEvents.clear();
           for (Event event : events) {
               try {
                   String category = DateService.getEventStatus(event);
                   if (constraint.toString().equalsIgnoreCase(category))
                       selectedEvents.add(event);
               } catch (ParseException e) {
                   Timber.e(e);
               }
           }
           return null;
       }
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
           notifyDataSetChanged();
        }
    };
}

Step 4: Setting up the BottomViewNavigation:

We have already set up the binding so we can reference the bottomNavigationView directly and setup the OnNavigationItemSelectedListener as follows:

binding.bottomNavigation.setOnNavigationItemSelectedListener(
           item -> {
               switch (item.getItemId()) {
                   case R.id.action_live:
                       eventListAdapter.getFilter().filter(“live”);
                       return true;
                   case R.id.action_upcoming:
                       eventListAdapter.getFilter().filter(“upcoming”);
                       return true;
                   case R.id.action_past:
                       eventListAdapter.getFilter().filter(“past”);
                       return true;
                   default:
                      return false;
               }
           });

As can be seen above, we can directly filter according to the string parameters we are passing and the logic is being handled in the getFilter() method.

This is the result of the making the above changes:

The best benefits of using  BottomNavigationView  was that we didn’t even had to touch the presenter code to handle event list, or create any new fragments. This was one of the cleanest possible solutions, but now we have a pager implemented in the app, which seems to give a little bit more visual appeal.

References:

Open Event Organizer App – Pull Request #1009
https://github.com/fossasia/open-event-orga-app/pull/1009

Continue ReadingImplementing bottom navigation view in Organizer App

Implementing Checkout Times for Attendees on Open Event Server

As of this writing, Open Event Server did not have the functionality to add, manipulate and delete checkout times of attendees. Event organizers should have access to log and update attendee checkout times. So it was decided to implement this functionality in the server. This boiled down to having an additional attribute checkout_times in the ticket holder model of the server.

So the first step was to add a string column named checkout_times in the ticket holder database model, since this was going to be a place for comma-separated values (CSV) of attendee checkout times. An additional boolean attribute named is_checked_out was also added to convey whether an attendee has checked out or not. After the addition of these attributes in the model, we saved the file and performed the required database migration:

To create the migration file for the above changes:

$ python manage.py db migrate

To upgrade the database instance:

$ python manage.py db upgrade

Once the migration was done, the API schema file was modified accordingly:

class AttendeeSchemaPublic(SoftDeletionSchema):
    """
    Api schema for Ticket Holder Model
    """
    
    checkout_times = fields.Str(allow_none=True)  # ←
    is_checked_out = fields.Boolean()  # ←
    

After the schema change, the attendees API file had to have code to incorporate these new fields. The way it works is that when we receive an update request on the server, we add the current time in the checkout times CSV to indicate a checkout time, so the checkout times field is essentially read-only:

from datetime import datetime
...
class AttendeeDetail(ResourceDetail):
    def before_update_object(self, obj, data, kwargs):
        
        if 'is_checked_out' in data and data['is_checked_out']:
        ...
        else:
            if obj.checkout_times and data['checkout_times'] not in \
obj.checkout_times.split(","):
                data['checkout_times'] = '{},{},{}'.format(
                    obj.checkout_times,
                    data['checkout_times'],
                    datetime.utcnow())

 

This completes the implementation of checkout times, so now organizers can process attendee checkouts on the server with ease.

Resources

Continue ReadingImplementing Checkout Times for Attendees on Open Event Server

Option to delete albums from storage in Phimpme Android Application

In the Phimpme Android application, users can perform various operations on the albums available such as creating a zip file of the album, hiding an album and many more. However, one another useful functionality that has been added to the Phimpme Android application is the option to delete unwanted albums permanently from the storage. So in this post, I will be discussing how we implemented the functionality to delete albums.

Step 1

First, we need to provide an option in the overflow menu to delete album/albums when the user long-selects album/albums or the user is viewing photos inside a particular album. The option in the overflow menu can be added by integrating following lines of code in the menu_albums.xml file(this file contains the XML code for overflow menu options provided for albums).

 <item
android:id=”@+id/delete_action”
android:title=”@string/delete”
app:showAsAction=”ifRoom” />

Step 2

Now when the user opts to delete some selected albums, we need to retrieve user’s choice of albums to delete and store it in an ArrayList<Album> to keep track of the albums and for further use. We would here also keep track of all the albums displayed currently to the user through another ArrayList<Album> named displayAlbums. Thereafter deleteSelectedAlbums method of class HandlingAlbums will be invoked passing in the activity context as the parameter. Now in this method we’d iterate through the ArrayList<Album>(ArrayList used to store user’s choice of albums to delete) and in turn invoke another method deleteAlbum with each iteration, passing in the album and context as the parameters. The deleteAlbum method would return a boolean value that will indicate whether or not the particular album was successfully deleted. If the boolean value returned is true, indicating that the particular album and its contents have been permanently deleted from the device storage, then we would delete that particular album object from the displayAlbums list and the AlbumsAdapter would be notified to display the remaining set of albums by passing in the modified displayAlbums list. The code snippet used to implement the method deleteSelectedAlbums and deleteAlbum is provided below.

public boolean deleteSelectedAlbums(Context context) {
boolean success = true;

for (Album selectedAlbum : selectedAlbums) {
  int index = dispAlbums.indexOf(selectedAlbum);
  if(deleteAlbum(selectedAlbum, context))
    dispAlbums.remove(index);
  else success = false;
}
return success;
}
public boolean deleteAlbum(Album album, Context context) {
return ContentHelper.deleteFilesInFolder(context, new File(album.getPath()));
}

Step 3

In the previous step, we were invoking a function deleteAlbum which would return a boolean variable denoting whether the album was deleted permanently or not. Therefore in this step, I’d be discussing the working of the deleteAlbum method. So the deleteAlbum method was being invoked passing in the album to delete and the context as the parameters. This method, in turn invokes the deleteFilesInFolder method of the ContentHelper class passing in the context and a File object created from the album path(to be deleted). This method returns a boolean variable indicating the success or failure of the operation and this variable is in turn returned by the deleteAlbum method. The code representing the function invoke has been provided above. The code snippet representing the deleteFilesInFolder method is provided below.

public static boolean deleteFilesInFolder(Context context, @NonNull final File folder) {
 boolean totalSuccess = true;

 String[] children = folder.list();
 if (children != null) {
    for (String child : children) {
       File file = new File(folder, child);
       if (!file.isDirectory()) {
          boolean success = deleteFile(context, file);
          if (!success) {
             Log.w(TAG, “Failed to delete file” + child);
             totalSuccess = false;
          }
       }
    }
 }
 return totalSuccess;
}

Now inside the deleteFilesInFolder method first all the contents of the file are stored in a String[] array child. Thereafter by iterating through the child array, every time a method deleteFile is invoked passing in an item from the child array(an image path) and the context as parameters, which returns a boolean variable indicating if the particular image content is deleted or not.

Now depending on which Android version the device is running, there are two methods used to delete the image file permanently from the storage.

If the device is running on version LOLLIPOP and above, the Android Storage Access Framework has been used to delete the image file, and if the device is running version KITKAT, the MediaStore content provider is used to delete the file from device’s storage. The code snippet implementing the deleteFile method is provided below.

public static boolean deleteFile(Context context, @NonNull final File file) {
 boolean success = file.delete();

 // Try with Storage Access Framework.
 if (!success && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    DocumentFile document = getDocumentFile(context, file, false, false);
    success = document != null && document.delete();
 }

 // Try the Kitkat workaround.
 if (!success && Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
    ContentResolver resolver = context.getContentResolver();

    try {
       Uri uri = null;//MediaStoreUtil.getUriFromFile(file.getAbsolutePath());
       if (uri != null) {
          resolver.delete(uri, null, null);
       }
       success = !file.exists();
    }
    catch (Exception e) {
       Log.e(TAG, “Error when deleting file “ + file.getAbsolutePath(), e);
       return false;
    }
 }

 if(success) scanFile(context, new String[]{ file.getPath() });
 return success;
}

This is how we achieved the functionality to delete album/albums from the storage in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1.Android Developer Guide – https://developer.android.com/guide/topics/providers/document-provider#delete

2.Github-Phimpme Android Repository – https://github.com/fossasia/phimpme-android/

3.MediaStore class – https://developer.android.com/reference/android/provider/MediaStore

4.Deleting files via Android media content provider – https://stackoverflow.com/questions/10925196/deleting-files-via-a-contentresolver-as-opposed-to-deleting-them-via-file-del

Continue ReadingOption to delete albums from storage in Phimpme Android Application
Read more about the article Adding Option to Choose Room for SUSI Smart Speaker in iOS App
SAMSUNG CAMERA PICTURES

Adding Option to Choose Room for SUSI Smart Speaker in iOS App

SUSI Smart Speaker is an open source smart speaker that supports many features. The user can use Android or iOS to connect their device with SUSI Smart Speaker. During initial installation, it is asking from use to enter the Room name. Room name is basically the location of your SUSI Smart Speaker in the home. You may have multiple SUSI Smart Speaker in different rooms, so the purpose of adding the room is to differentiate between them. You can find useful instructions for the initial connection between the iOS device and SUSI Smart Speaker here. It this post, we will see how the adding rooms feature implemented for SUSI iOS.

When the user enters into the Device Activity screen, we check if the iOS device is connected to SUSI.AI Wi-Fi hotspot or not. If the device is connected to SUSI Smart Speaker, it shows the Wi-Fi displayed SSID in Device Activity Screen. On clicking the displayed Wi-Fi cell, a popup is open with a Room Location Text field. The user can enter Room location and by clicking the Next button, proceed further with the setup.

In the popup, there is also an option for choosing rooms, where the list of most common room names is displayed and the user can choose room name from the list.

Presenting Room Picker View Controller –

func presentRoomsPicker() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let roomVC = storyboard.instantiateViewController(withIdentifier: "RoomPickerVC") as? RoomPickerController {
roomVC.deviceActivityVC = self
let roomNVC = AppNavigationController(rootViewController: roomVC)
self.present(roomNVC, animated: true)
}
}

Room Picker View Controller is UITableViewController that display the rooms names in table cells. Some of the most common rooms names displayed are:

let rooms: [String] = ["Bedroom", "Kitchen", "Family Room", "Entryway", "Living Room", "Front Yard", "Guest Room", "Dining Room", "Computer Room", "Downstairs", "Front Porch", "Garage", "Hallway", "Driveway"]

 

Presenting Room Cell –

We are using cellForRowAt method to present the cell.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "roomsCell", for: indexPath)
cell.textLabel?.text = rooms[indexPath.row]
cell.imageView?.image = ControllerConstants.Images.roomsIcon
return cell
}

 

Selecting the room from cell –

When the user clicks on the cell, first willSelectRowAt method use to display the right icon in the accessory view that shows which cell is selected.

if let oldIndex = tableView.indexPathForSelectedRow {
tableView.cellForRow(at: oldIndex)?.accessoryType = .none
}
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark

We are storing the selected room in the following variable and selecting it by using didSelectRowAt method of UITableView.

var selectedRoom: String?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedRoom = rooms[indexPath.row]
}

In Room Picker Screen, the user has two option, Cancel and Done. If the user clicks the Cancel, we dismiss the Room Picker screen and display the popup with the empty room location text field and with Choose Room option. If the user clicks the Done button, we store the picked room in UserDefaults shared instance and dismiss Room Picker screen with a different popup which has already filled room location in the text field and without choose room option in the popup as in the image below. By clicking the next, the user proceeds with the further setup.

Resources –

  1. Apple’s Documentations on UserDefaults.
  2. Initial Setups for Connecting SUSI Smart Speaker with iPhone/iPad
  3. Apple’s Documentations on tableView(_:cellForRowAt:)
  4. Apple’s Documentations on tableView(_:willSelectRowAt:)
  5. Apple’s Documentations on tableView(_:didSelectRowAt:)
Continue ReadingAdding Option to Choose Room for SUSI Smart Speaker in iOS App

Adding Tickets Relationship with Discount Codes in Open Event Server

Recently (as of this writing), it was discovered that the relationship between discount codes and tickets was not implemented yet in Open Event Server. It turns out that the server has two types of discount codes – discount codes for entire events and discount codes for individual tickets of a specific event. More information on how discount code themselves are implemented in the server can be found in this blog post from 2017 – Discount Codes in Open Event Server.

So, for implementing the relationship of discount codes with tickets, it was decided to be present only for discount codes that have the DiscountCodeSchemaTicket schema, since those are the discount codes that are used for individual tickets. As a first step, the `tickets` attribute of the discount code model was removed, as it was redundant. The already implemented used_for attribute did the same job, and with better validation. At the same time, discount code was added as an attribute.

In the ticket model file:

discount_code_id = db.Column(db.Integer, db.ForeignKey('discount_codes.id', ondelete='CASCADE'))
discount_code = db.relationship('DiscountCode', backref="tickets")

Also, in the __init__ constructor:

def __init__(self, ..., discount_code_id=None, ...):
    ...
    ...
    self.discount_code_id = discount_code_id

After that, we added a discount_code_id field in the ticket schema file:

discount_code_id = fields.Integer(allow_none=True)

In this file, we also removed the redundant tickets field.

Now, we migrated the Open Event Server database via the following commands:

$ python manage.py db migrate

then

$ python manage.py db upgrade

Next, in the discount code schema file, we added the tickets relationship. Note that this is a one-to-many relationship. One discount code (for tickets) can be mapped to many tickets. Here is the code for that relationship, in the discount code schema file, under the DiscountCodeSchemaTicket class:

tickets = Relationship(attribute='tickets',
self_view='v1.discount_code_tickets',
self_view_kwargs={'id': '<id>'},
related_view='v1.ticket_list',
related_view_kwargs={'discount_code_id': '<id>'},
schema='TicketSchemaPublic',
many=True,
type_='ticket')

For this, we, of course, imported the TicketSchemaPublic in this file first. After that, we created a DiscountCodeTicketRelationship class in the discount codes API file:

class DiscountCodeTicketRelationship(ResourceRelationship):
    """
    DiscountCode Ticket Relationship
    """
    decorators = (jwt_required,)
    methods = ['GET', 'PATCH']
    schema = DiscountCodeSchemaTicket
    data_layer = {'session': db.session, 'model': DiscountCode}

The next step was to add the query code to fetch the tickets related to a particular discount code from the database. For this, we added the following snippet to the query() method of the TicketList class in the tickets API file:

if view_kwargs.get('discount_code_id'):
    discount_code = safe_query(self, DiscountCode, 'id', view_kwargs['discount_code_id'], 'discount_code_id')
    # discount_code - ticket :: one-to-many relationship
    query_ = self.session.query(Ticket).filter_by(discount_code_id=discount_code.id)

The only thing that remains now is adding the API routes for this relationship. We do that in the project’s __init__.py file:

api.route(TicketList, 'ticket_list', '/events/<int:event_id>/tickets',
'/events/<event_identifier>/tickets', '/ticket-tags/<int:ticket_tag_id>/tickets',
'/access-codes/<int:access_code_id>/tickets', '/orders/<order_identifier>/tickets',
'/discount-codes/<int:discount_code_id>/tickets')

api.route(DiscountCodeTicketRelationship, 'discount_code_tickets',
'/discount-codes/<int:id>/relationships/tickets')

 

Many routes already map to TicketList, we added one for that comes from discount codes API. Now we can use Postman to check this relationship, and it indeed works as expected, as seen below!

Screen Shot 2018-08-21 at 1.54.22 PM

Here’s the end:

Screen Shot 2018-08-21 at 1.54.35 PM.png

References:

Continue ReadingAdding Tickets Relationship with Discount Codes in Open Event Server