Our custom application built on Belladati may require certain roles and groups and it may require user friendly management of new users and their groups. We will use user groups to control access to reports and datasets. But at first we need to be able to create new users so we can invite them to our application.

1. User creation

Users are stored in Belladati's internal tables, but we will still need a dataset to add data import forms and reports. So at first a dataset needs to be created, it doesn't need any attributes at the moment.

Once created we can add new import form to create a new user. The form will need fields e-mail, first name and surname at least. These fields won't be bound to any attribute, as there is none, but will be bound to a variable instead. The user creation then happens in After submit formula of the form:

def j = new JSONObject()
j.put("assignToDomainWhenExists", true)
j.put("sendNotification", true)
j.put("username", @email)
j.put("email",@email)
j.put("firstName",@firstname)
j.put("lastName",@surname)
createUser(j)

As you can see, it's quite simple. Method "createUser" has JSONObject as input with all possible user properties. 2 properties that are set to true in our case are:

  • assignToDomainWhenExists - When working in multidomain environment, we can be creating new user which already exists in the application, just in a different domain. Setting this to true will add user to our domain. If it was false, we would get an error that this user already exists.
  • sendNotification - We want to notify the new user that they can now access our application

We can prepare similar form for editing user, but since it doesn't work with dataset data but user, we need to fill in the form with existing values manually using Default value formula:

if (!isSubmit) {
if (@userId != NaN && @userId as String != 'NaN') {
  def u = getUserById(@userId as Integer)
  if (u != null) {
  def r = [email:u.email]
  if (@firstname == null || @firstname as String == 'NaN' || @firstname == '') {
    r.firstname = u.name
  }
  if (@surname == null || @surname as String == 'NaN' || @surname == '') {
    r.surname = u.surname
  }
  return r
}
}
}
return []

After submit formula will be quite similar

def u = getUserById(@userId as Integer)

if (u != null) {
def j = new JSONObject()
j.put("username",@email)
j.put("email",@email)  
j.put("firstName",@firstname)
j.put("lastName",@surname)

def r = updateUser(u.id, j)
  if (r.status == 412) {
    throw new Exception('User already exists!')
  }
}

It's just instead of createUser we need to call updateUser and in case username is changed to already existing username, this method returns Response with status 412, so we need to react to that.

While we're at it, we can define form that users will use to edit their own profile. Of course, we can keep the default one, but in this case we want to show less fields and we want different layout. So let's add another form with fields first name, surname (we don't want users to change their usernames) and Default values formula:

if (!isSubmit) {
  def u = getCurrentUser()
  def r = [:]
  if (@firstname == null || @firstname as String == 'NaN' || @firstname == '') {
    r.firstname = u.name
  }
  if (@surname == null || @surname as String == 'NaN' || @surname == '') {
    r.surname = u.surname
  }
  return r
}
return []

 and After submit formula:

def u = getCurrentUser()

def j = new JSONObject()
j.put("firstName",@firstname)
j.put("lastName",@surname)

def r = updateUser(u.id, j)

This form then needs to be inserted into a view, we need to create an action button that opens this view in a popup and then we can link this button to replace default edit user profile form via workspace parameter editUserProfileCustomLink (e.g. like /bi/report/detail.report.layout.rvd.actionbutton:openpopup/sbwL4UDAOA?t:ac=213&vr=213-ijampbQWiz )

2. User list

Now we can create new users, but we can't display them yet. Well, how about a simple table. But wait! Tables can only work with datasets and show data in datasets... hmm, what are we going to do? Custom views is the answer. We will render a list of users ourselves.

The custom view configuration can be similar to:

def domain = currentDomain()
def pageSize = 10
def page = @page as Integer
if (page == NaN || page == null) {
  page = 0
}
def f = @filter
if (f == NaN) {
  f = null
}
def pd = PartData.build(page*pageSize as Integer,pageSize)
def users = listUsersInGroups(f, [null,'Company admins','Company users'], pd)
def conf = [total:users.total, pageSize:pageSize, currentPage:page]  
conf.list = []
users.list.each { 
  def u = [id:it.id]
  u.name = it.name+' '+it.surname
  u.firstLogin = it.firstLogin
  u.username = it.username
  u.cols = [it.username, toString(it.lastLogin,null), it.firstLogin ? translate(['':'Activated','ja':'有効済み']) : translate(['':'Not activated','ja':'有効化されていません'])]
  conf.list.add(u)
}
conf.header = [
translate(['':'Name','ja':'名称']),
translate(['':'Username','ja':'メールアドレス']),
translate(['':'Last login','ja':'最終ログイン']),
translate(['':'Status','ja':'ステータス'])]
conf.button = [
  label : translate(['':'Add user','ja':'ユーザーを作成する']),
  url : 'viewreport:213-I2QEOEM4l1'
]
conf.filter = true
conf.filterString = f
conf.rowLink = translate('':'/en/bi/reports/userdetail','ja':'/ja/bi/reports/userdetail')
conf.searchLabel = translate(['':'Search','ja':'検索'])
return conf

What's worth mentioning -

  • This will be a pageable list
  • Filterable
  • Limited to users without user group or in groups "Company admins" or "Company users".
  • Displayed columns will be Name, Username, Last login, Status
  • There will be a button to add new user
  • Clicking the row will open detail screen of the user

And this will be the Javascript rendering the table:

var renderUserList = function($, $container, conf, reloadFunc) {

  var list = conf.list
  
  $container.addClass('kwTable')
  
  var searchLabel = conf.searchLabel || 'Search'
  
  
  if (conf.filter) {
    var $df = $('<div/>').addClass('filter')
   	var $fInp = $('<input/>').attr('type','text')
    if (conf.filterString) {
      $fInp.val(conf.filterString)
    }
    $df.append($fInp)
    $df.append($('<input/>').attr('value',searchLabel).attr('type','submit').css({'margin-left':'5px'}).addClass('btn primary').click(function(e) {
      reloadFunc({'vr:filter':$fInp.val()})
    }))
    $container.append($df)
  }
  
  if (conf.button) {
    $container.append($('<button/>').addClass('btn').addClass('primary').text(conf.button.label).click(function(e) {
      openBdPopup(conf.button.url, conf.button.label)
    }))
  }
  
  var $table = $('<table/>').addClass('staticTable')
  $container.append($table)
  
  if (conf.header) {
    var $th = $('<tr/>')
    $table.append($('<thead/>').append($th))
    conf.header.forEach(function(h) {
      $th.append($('<th/>').text(h))
    })
    if (conf.actions) {
      $th.append($('<th/>'))
    }
  }
  
  list.forEach(function(u) {
    var $tr = $('<tr/>')
    $table.append($tr)
    var $td = $('<td/>').text(u.name)
    if (!u.firstLogin) {
      $td.append($('<span class="icon-exclamation"></span>'))
    }
    $tr.append($td)
    if (u.cols) {
      u.cols.forEach(function(c) {
        $tr.append($('<td/>').text(c))
      })
    }    
    if (conf.rowLink) {
      $tr.click(function(e) {
        window.location.href = conf.rowLink+'?userId='+u.id
      })
    }
    if (u.actions) {
      var $ac = $('<td/>').css({'text-align':'right'})
      $tr.append($ac)
      u.actions.forEach(function(a) {
        $ac.append($('<a/>').addClass(a.icon).click(function() {
          require('bd/zone').findById('popupZone').updateFromURL(a.link)
        }))
      })
    }
  })
  
  if (conf.total > list.length) {
    // We need pagination
    var $ul = $('<ul/>')
    $container.append($('<div/>').addClass('pagination').append($ul))
    for (var i = 0; i < conf.total/conf.pageSize; i++) {
      $ul.append($('<li/>').addClass(i == conf.currentPage ? 'disabled' : '').append($('<a/>').text(i+1)))
    }
    $ul.find('li').click(function(e) {
      var p = parseInt($(this).text())-1
      if (conf.filter) {
        reloadFunc({'vr:page':p,'vr:filter':$fInp.val()})
      } else {
      	reloadFunc({'vr:page':p})
      }
    })
  }
  
}

3. User detail

User detail is another report. Since users are not in dataset, we cannot utilize report views, but we have to write our own custom views and fetch data using user functions.

4. Delete user

User can be deleted via custom endpoint and action button that calls this endpoint. The code of the endpoint can be:

def username = request.getParameter('username')
def u = null
if (username != null) {
  u = getUserByUsername(username)
} else {
  u = getUserById(request.getParameter('userId') as Integer)
}
if (u != null) {
  deleteData('SOME_DATASET_WHERE_USER_IS_PRESENT',isEqualFilter('L_USER_ID', u.id as String))
  deleteUser(u)
}
return Response.ok().build()


  • No labels