Jump to content

Messages for individuals


gbligh

Recommended Posts

I've been building a template that allows teachers to create a 'workspace' like area just for their class. I wondered if there was a way in Frog Code to have a notice board/message wall style widget that allows you to only send messages to certain kids in that group. In Google Classroom, you can choose to send a message to the whole class or just certain kids.

@Graham Quince @pconkie

Link to comment
Share on other sites

On 06/04/2022 at 19:48, gbligh said:

I've been building a template that allows teachers to create a 'workspace' like area just for their class. I wondered if there was a way in Frog Code to have a notice board/message wall style widget that allows you to only send messages to certain kids in that group. In Google Classroom, you can choose to send a message to the whole class or just certain kids.

@Graham Quince @pconkie

Frog Messages?

Link to comment
Share on other sites

This is a good excuse for a FrogCode Tutorial to create a group messaging widget

To summarise, George is asking for a widget which a teacher could use to send a message to everyone in a group or just to some members of the group.  We need the following:

  1. A method of selecting a group, which then lists the members
  2. A form / input to add the message
  3. The message API to be able to send the message.

 


Step 1 - Getting group members

After creating your widget within FrogCode Editor we need to load in the group search component.  While this is not documented for use anywhere, it is available in the Random Name Selector - Basic widget tutorial, so we can copy it from there.

            steal.import('frogui/modules/selectusers/selectusers!frog-component').then(function() { 
                var groupName = self.element.find("div.group-select input.groupName"),
                    groupSelect = self.element.find("div.group-select");

                groupSelect.frogui_modules_selectusers({
                    show_label: true,
                    label: 'Search for a group',
                    show_permission: false,
                    show_submit: true,
                    submit_label: 'Search for a group', // This button sends the selected group to the next function
                    allow_convert: false,
                    search_users: false,
                    max_selections: 1,
                    profiles: ['admin','staff','parent','governor','student','external admin','external staff'] // Determine which profile types are searchable with the group selector
                });
            });

We add this to the widget.live function so it becomes:

        'widget.live': function(el, ev, data) {
            this.element.html(
                this.view('main.ejs')
            );
            steal.import('frogui/modules/selectusers/selectusers!frog-component').then(function() {
                var groupName = self.element.find("div.group-select input.groupName"),
                    groupSelect = self.element.find("div.group-select");

                groupSelect.frogui_modules_selectusers({
                    show_label: true,
                    label: 'Search for a group',
                    show_permission: false,
                    show_submit: true,
                    submit_label: 'Search for a group',
                    allow_convert: false,
                    search_users: false,
                    max_selections: 1,
                    profiles: ['admin','staff','parent','governor','student','external admin','external staff'] // Determine which profile types are searchable with the group selector
                });
            });
        },

And this code needs to be added to the main.ejs page

<div class="group-select"><input type="text" class="groupName input-block-level" placeholder="Type name of group or profile" autocomplete="off"/></div>

 

Next, once a group is selected, we need a function to be triggered:

 'selectusers.submitted': function() {
   var self = this;
   var groupSelect = self.element.find("div.group-select");
    groupSelect.trigger('selectusers.getSelected', function(data) {
      console.log(data)
    });

},

On testing this is what we get so far

first-test.PNG

When a group is selected and the blue button pressed, this is returned in the console log

first-console.PNG

In the groups object, there is only the 0 entry listed.  And in that entry, we get the group's name and it's UUID.  Which means we can now use the FDP API to list all the members of that group.

From FrogDrive > Applications, open the FDP API Reference app, if it's not visible you'll find it in Package Manager > Ready to Install.

fdp-app.PNG

Once opened, you'll see details on all the Frog Developer Platform APIs.  These APIs do not change, meaning they are safe for you to use, without fearing that our developers will update them.

There's a whole section for groups.  

group-api.PNG

Using the FDP API template, we can search for our group's members in FDP 1, like this:

         'selectusers.submitted': function() {
             var self = this;
             var groupSelect = self.element.find("div.group-select");
             var table = self.element.find('.class_members');
                        
             table.find('tr:gt(0)').remove();           
           
             groupSelect.trigger('selectusers.getSelected', function(data) {
                 FrogOS.fdp({
                     url: 'group/get',
                     path: '/api/fdp/1/',
                     data: {
                         uuid: data.groups[0].uuid
                     }
                 }).done(function(response) {
                     // do something with the response data
                     var members = response;
                     console.log(members);
                 }).fail(function(e) {
                     // Report Error
                     console.log('failed');
                 });
             });
         },

Notes:

  • The documentation states that this API is in the 1 directory, so you need to alter the template's path.
  • The documentation also mentions it is a POST API, this is a mistake, it is a GET type.
  • In a few steps, we'll be adding the class members to a table, so we set it as a variable at the top.
  • By finding the table here, we can also empty it of previous searches using the remove function, otherwise each new class search will add to the bottom of the table.

Testing that, gives us this response:

members.PNG

So now we have all the members of the class.  We just need to list them, which is simple enough.  we will create a table in main.ejs and a second ejs file called row.  Then iterate through each member and create a row for them using self.view to do so.  We also need to add row to the require section at the top of the widget.js file.

require.PNG

main1.PNG

Row.ejs

<tr title="<%= username %>">
    <td><%= name %></td>
    <td><input type="checkbox" id="member_<%= uuid %>" name="member_<%= uuid %>" data-uuid="<%= uuid %>" class="checkbox member_<%= uuid %>" checked></td>
</tr>

Notes:

  • Within the EJS files, I can use <%= %> to add additional javascript. In the next part, we'll pass through the name and uuid as part of the view append.
  • Frog imports Bootstrap CSS, so we can take advantage of Bootstrap's table classes to set out some standard styling.  But we'll use our own class name for the javascript and potentially additional CSS.
  • Each row will already have the checkbox selected, as I assume it is likely to be a case of de-selecting a few students.  Later we could add a Select all option.
         'selectusers.submitted': function() {
             var self = this;
             var groupSelect = self.element.find("div.group-select");
             var table = self.element.find('.class_members');
                        
             table.find('tr:gt(0)').remove();
             
             groupSelect.trigger('selectusers.getSelected', function(data) {
                 FrogOS.fdp({
                     url: 'group/get',
                     path: '/api/fdp/1/',
                     data: {
                         uuid: data.groups[0].uuid
                     }
                 }).done(function(response) {
                     // do something with the response data
                     var members = response.response;

                     members.sort((a, b) => (a.displayname > b.displayname) ? 1 : -1);

                     $.each(members, function(index,member) {
                         table.append(
                             self.view('row.ejs', {
                                 name: member.displayname,
                                 username: member.username,
                                 uuid: member.uuid
                             })
                         )
                         
                     });
                 }).fail(function(e) {
                     // Report Error
                     console.log('failed');
                 });
             });
         },

Notes:

  • We know from the first console log's data for the API call that our members are listed in the response.response section, so we can set members to be that, instead of the whole response.
  • A simple sort function, sorts all the members alphabetically by display name
  • $.each allows us to create a new function for each individual in members, where each individual is its own object, called (in this case) 'member' - this contains all the individual data from the returned API
  • In self.view we attach the file 'row.ejs', then in the object area, we have the names for the code used in row.ejs.  

All of the above, gives us:

step1-result.PNG

Which completes Step 1!


Step 2 - (Re)Creating the Message Input form

Looking at the Messaging application, we need the following fields:messaging.PNG

  • Title
  • Message
  • Recipients (we've got that covered)
  • (Link is optional)

In Main.ejs, let's set the following:

<div class="packaged-widget-placeholder--container">
    <h3 class="packaged-widget-placeholder--title">
        <%= app._('widget.placeholder.title') %>
    </h3>
   <div class="row-fluid">
       <div class="message_form span6">
           <label for="title"><%= app._('widget.placeholder.message_title') %>:</label>
           <input type="text" id="title" name="title" class="message_title my_input">
           <label for="msg"><%= app._('widget.placeholder.message') %></label>    
           <textarea id="msg" name="msg" class="message my_input" rows="4" cols="50"></textarea>
           <label for="link"><%= app._('widget.placeholder.link') %>:</label>
           <input type="text" id="link" name="link" class="message_link my_input">
           <br>
           <div class="btn btn-success disabled"><%= app._('widget.placeholder.send') %></div>
       </div>
       <div class="group_list span6">
           <div class="group-select message_group_select"><input type="text" class="groupName input-block-level" placeholder="Type name of group or profile" autocomplete="off"/></div>
           <table class="table table-bordered table-hover table-striped class_members">
               <tr>
                   <th>Name</th>
                   <th>Include?</th>
               </tr>
           </table>
       </div>
    </div>
</div>

Notes:

  • Even though most web pages would use name or id to get the entries, because Frog loads all pages on top of each other, id can cause conflicts as it would be possible to use the same id twice.  Classes however are part of the widget and you can have two DIVs with the same class in different widgets.
  • We're borrowing some more bootstrap here, with the DIVs with classes "row-fluid" and "span6".  This are used in the page layouts to organise everything without us having to style it all ourselves.
  • The breakout code such as <%= app._('widget.placeholder.message_title') %> uses the lang.manifest.json file to populate the text on the page.  This is good practice, should another language be encoded into the platform.  It also means there is only one place to find all the text and you can reuse the text in more than one place, so far, my lang.manifest.json looks like this:
    {
        "en-GB": {
            "widget.title": "Group Messaging",
            "widget.placeholder.title": "Group Messaging",
            "preference.button_label": "Search for a group",
            "preference.button_submit_label": "List members",
            "widget.placeholder.message_title": "Message Title",
            "widget.placeholder.message": "Message",
            "widget.placeholder.link": "Web link (optional)",
            "widget.placeholder.send": "Send",
            "widget.placeholder.table_header_name": "Name",
            "widget.placeholder.table_header_include": "Include?"
        }
    }

     

layout.PNG

Note:

  • To set the search button next to the search text box, I add CSS of position:relative; to the class "message_group_select"

It also collapses down nicely on mobiles:

layout_mobile.PNG

The Send button is greyed out, until all the fields are completed.  To change this, we'll use a change event, then check through the input elements and table checkboxes to see if the button can have the disabled class removed from it:

        '.my_input change': function(ev) {
            var title = this.element.find('.message_title').val();
            var message = this.element.find('.message').val();
            var table_rows = this.element.find('.class_members');

            if (title !== '' && message !== '' && table_rows.find('input:checked').length > 0) {
                this.element.find('.send').removeClass('disabled');
            }
        },

Notes:

  • If all the table rows are unchecked, then the value of the length would be 0, if there is at least one row checked, the button can be enabled.

And now that that works, we can send a message.


Step 3 - Using the Message API

Unfortunately, there is no FDP API for the messaging application, so instead we have to "borrow" the official one. 

  1. Open up your developer console and the messaging application.  Send a new message to several individuals, and include a link (we'll avoid site links, just stick with a URL).
  2. In the Developer console's Network tab, you'll see an API labeled '?method=messages.create'  
  3. When you click into that API, you'll see the params we need to pass through:
    send_API.PNG

 

Create a new function, based on the click on the Send button.  And inside that, copy the Internal API POST template:

       
        '.send:not(.disabled) click': function() {
            var self = this;
            var title = self.element.find('.message_title');
            var message = self.element.find('.message');
            var link = self.element.find('.message_link');
            var table_rows = self.element.find('.class_members').find('.checkbox:checked');
            var send = self.element.find('.send');
            
            var sendTo = [];
            $.each(table_rows, function(index, member) {
                    sendTo.push({
                        sendTo: 'groupOnly',
                        uuid: member.dataset.uuid
                    })
            });       
            var data = {
                title: title.val(),
                message: message.val(),
                sendTo: sendTo                
            };

            if (link.val() !== '') {
                data = {
                    title: title,
                    message: message,
                    sendTo: sendTo,
                    link: {
                        label: self.prefs.linkLabel.value,
                        type: 'url',
                        url: link
                    }
                };
            };

            Frog.Model.api('messages.create', 
                data,
            {
                type: 'POST'
            }).done(function(response) {
                // do something with the response data

                send.addClass('disabled');
                title.val('');
                message.val('');
                link.val('');
                
            }).fail(function(e) {
                // Report Error

            });
            
        },

Notes:

  • The functions inside FrogCode are declared slightly differently to how you may see them in online tutorials.  The opening line here look for the button with the class of 'send' but not with the class of 'disabled' to be clicked. 
  • Self = this is a useful way of not having to bind the outer code to this function.
  • Because George asked for specific individuals to be messaged, we need to populate the Sendto variable using the input boxes.  By including in the input the data-uuid attribute, we can look at just the checked inputs.
  • The parameters need to list the link value if we're sending a link, but if not, we have to not list it.  It is much simpler to overwrite a data object than it is to add an extra field unfortunately, so we set the data object fields, then replace the whole thing if a link is present.  
  • self.prefs.linkLabel.value looks for this widget preference:
            prefs: {
                linkLabel: {
                    type: 'text',
                    label: 'Link label for message',
                    defaultValue: 'Link'
                }
            },

    Which I added into the prefs section at the top of widget.js

And that's it!  Obviously we could / should add a growl to demonstrate the message is sent and we could add an uncheck all button etc...


 

Summary

I'll tidy this widget up a little before releasing it, but hopefully you can see the process.

One bit of feedback I would like from you, (either here or privately in email), is this sort of tutorial useful to you?  It took me quite a bit of time (as an amateur developer) to understand how to use Frog's APIs and what was possible to do with them when I did.  I'm keen to know if having the steps broken down like this helps you build your own widgets.

row.PNG

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...