about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--wqflask/wqflask/oauth2/client.py2
-rw-r--r--wqflask/wqflask/oauth2/groups.py73
-rw-r--r--wqflask/wqflask/oauth2/resources.py121
-rw-r--r--wqflask/wqflask/oauth2/roles.py78
-rw-r--r--wqflask/wqflask/templates/base.html49
-rw-r--r--wqflask/wqflask/templates/gsearch_gene.html11
-rw-r--r--wqflask/wqflask/templates/oauth2/create-role.html46
-rw-r--r--wqflask/wqflask/templates/oauth2/list_roles.html58
-rw-r--r--wqflask/wqflask/templates/oauth2/view-group-role.html96
-rw-r--r--wqflask/wqflask/templates/oauth2/view-resource.html121
10 files changed, 636 insertions, 19 deletions
diff --git a/wqflask/wqflask/oauth2/client.py b/wqflask/wqflask/oauth2/client.py
index 2bf3f94d..f712e54d 100644
--- a/wqflask/wqflask/oauth2/client.py
+++ b/wqflask/wqflask/oauth2/client.py
@@ -6,7 +6,7 @@ from pymonad.maybe import Just, Maybe, Nothing
 from pymonad.either import Left, Right, Either
 from authlib.integrations.requests_client import OAuth2Session
 
-SCOPE = "profile group role resource register-client"
+SCOPE = "profile group role resource register-client user"
 
 def oauth2_client():
     config = app.config
diff --git a/wqflask/wqflask/oauth2/groups.py b/wqflask/wqflask/oauth2/groups.py
index f9e9bffe..551c0640 100644
--- a/wqflask/wqflask/oauth2/groups.py
+++ b/wqflask/wqflask/oauth2/groups.py
@@ -1,3 +1,4 @@
+import uuid
 import datetime
 from functools import partial
 
@@ -131,3 +132,75 @@ def reject_join_request():
         data=request.form).either(
             handle_error("oauth2.group.list_join_requests"),
             __success__)
+
+@groups.route("/role/<uuid:group_role_id>", methods=["GET"])
+@require_oauth2
+def group_role(group_role_id: uuid.UUID):
+    """View the details of a particular role."""
+    def __render_error(**kwargs):
+        return render_template("oauth2/view-group-role.html", **kwargs)
+
+    def __gprivs_success__(role, group_privileges):
+        return render_template(
+            "oauth2/view-group-role.html", group_role=role,
+            group_privileges=tuple(
+                priv for priv in group_privileges
+                if priv not in role["role"]["privileges"]))
+
+    def __role_success__(role):
+        return oauth2_get("oauth2/group/privileges").either(
+            lambda err: __render_error__(
+                group_role=group_role,
+                group_privileges_error=process_error(err)),
+            lambda privileges: __gprivs_success__(role, privileges))
+
+    return oauth2_get(f"oauth2/group/role/{group_role_id}").either(
+        lambda err: __render_error__(group_role_error=process_error(err)),
+        __role_success__)
+
+def add_delete_privilege_to_role(
+        group_role_id: uuid.UUID, direction: str) -> Response:
+    """Add/delete a privilege to/from a role depending on `direction`."""
+    assert direction in ("ADD", "DELETE")
+    def __render__():
+        return redirect(url_for(
+            "oauth2.group.group_role", group_role_id=group_role_id))
+
+    def __error__(error):
+        err = process_error(error)
+        flash(f"{err['error']}: {err['error_description']}", "alert-danger")
+        return __render__()
+
+    def __success__(success):
+        flash(success["description"], "alert-success")
+        return __render__()
+    try:
+        form = request.form
+        privilege_id = form.get("privilege_id")
+        assert bool(privilege_id), "Privilege to add must be provided"
+        uris = {
+            "ADD": f"oauth2/group/role/{group_role_id}/privilege/add",
+            "DELETE": f"oauth2/group/role/{group_role_id}/privilege/delete"
+        }
+        return oauth2_post(
+            uris[direction],
+            data={
+                "group_role_id": group_role_id,
+                "privilege_id": privilege_id
+            }).either(__error__, __success__)
+    except AssertionError as aerr:
+        flash(aerr.args[0], "alert-danger")
+        return redirect(url_for(
+            "oauth2.group.group_role", group_role_id=group_role_id))
+
+@groups.route("/role/<uuid:group_role_id>/privilege/add", methods=["POST"])
+@require_oauth2
+def add_privilege_to_role(group_role_id: uuid.UUID):
+    """Add a privilege to a group role."""
+    return add_delete_privilege_to_role(group_role_id, "ADD")
+
+@groups.route("/role/<uuid:group_role_id>/privilege/delete", methods=["POST"])
+@require_oauth2
+def delete_privilege_from_role(group_role_id: uuid.UUID):
+    """Delete a privilege from a group role."""
+    return add_delete_privilege_to_role(group_role_id, "DELETE")
diff --git a/wqflask/wqflask/oauth2/resources.py b/wqflask/wqflask/oauth2/resources.py
index 872a29c6..8f31f7c9 100644
--- a/wqflask/wqflask/oauth2/resources.py
+++ b/wqflask/wqflask/oauth2/resources.py
@@ -1,6 +1,7 @@
 import uuid
 
-from flask import flash, request, url_for, redirect, Blueprint, render_template
+from flask import (
+    flash, request, url_for, redirect, Response, Blueprint, render_template)
 
 from .checks import require_oauth2
 from .client import oauth2_get, oauth2_post
@@ -51,18 +52,62 @@ def create_resource():
 @require_oauth2
 def view_resource(resource_id: uuid.UUID):
     """View the given resource."""
-    # Display the resource's details
-    # Provide edit/delete options
-    # Metadata edit maybe?
+    def __users_success__(
+            resource, unlinked_data, users_n_roles, this_user, group_roles,
+            users):
+        return render_template(
+            "oauth2/view-resource.html", resource=resource,
+            unlinked_data=unlinked_data, users_n_roles=users_n_roles,
+            this_user=this_user, group_roles=group_roles, users=users)
+
+    def __group_roles_success__(
+            resource, unlinked_data, users_n_roles, this_user, group_roles):
+        return oauth2_get("oauth2/user/list").either(
+            lambda err: render_template(
+                "oauth2/view-resource.html", resource=resource,
+                unlinked_data=unlinked_data, users_n_roles=users_n_roles,
+                this_user=this_user, group_roles=group_roles,
+                users_error=process_error(err)),
+            lambda users: __users_success__(
+                resource, unlinked_data, users_n_roles, this_user, group_roles,
+                users))
+
+    def __this_user_success__(resource, unlinked_data, users_n_roles, this_user):
+        return oauth2_get("oauth2/group/roles").either(
+            lambda err: render_template(
+                "oauth2/view-resources.html", resource=resource,
+                unlinked_data=unlinked_data, users_n_roles=users_n_roles,
+                this_user=this_user, group_roles_error=process_error(err)),
+            lambda groles: __group_roles_success__(
+                resource, unlinked_data, users_n_roles, this_user, groles))
+
+    def __users_n_roles_success__(resource, unlinked_data, users_n_roles):
+        return oauth2_get("oauth2/user").either(
+            lambda err: render_template(
+                "oauth2/view-resources.html",
+                this_user_error=process_error(err)),
+            lambda usr_dets: __this_user_success__(
+                resource, unlinked_data, users_n_roles, usr_dets))
+
+    def __unlinked_success__(resource, unlinked_data):
+        return oauth2_get(f"oauth2/resource/{resource_id}/user/list").either(
+            lambda err: render_template(
+                "oauth2/view-resource.html", resource=resource,
+                unlinked_data=unlinked_data,
+                users_n_roles_error=process_error(err)),
+            lambda users_n_roles: __users_n_roles_success__(
+                resource, unlinked_data, users_n_roles))
+        return render_template(
+                "oauth2/view-resource.html", resource=resource, error=None,
+                unlinked_data=unlinked)
+
     def __resource_success__(resource):
         dataset_type = resource["resource_category"]["resource_category_key"]
         return oauth2_get(f"oauth2/group/{dataset_type}/unlinked-data").either(
             lambda err: render_template(
                 "oauth2/view-resource.html", resource=resource,
                 unlinked_error=process_error(err)),
-            lambda unlinked: render_template(
-                "oauth2/view-resource.html", resource=resource, error=None,
-                unlinked_data=unlinked))
+            lambda unlinked: __unlinked_success__(resource, unlinked))
 
     return oauth2_get(f"oauth2/resource/view/{resource_id}").either(
         lambda err: render_template("oauth2/view-resource.html",
@@ -128,6 +173,68 @@ def unlink_data_from_resource():
         return redirect(url_for(
             "oauth2.resource.view_resource", resource_id=form["resource_id"]))
 
+@resources.route("<uuid:resource_id>/user/assign", methods=["POST"])
+@require_oauth2
+def assign_role(resource_id: uuid.UUID) -> Response:
+    form = request.form
+    group_role_id = form.get("group_role_id", "")
+    user_email = form.get("user_email", "")
+    try:
+        assert bool(group_role_id), "The role must be provided."
+        assert bool(user_email), "The user email must be provided."
+
+        def __assign_error__(error):
+            err = process_error(error)
+            flash(f"{err['error']}: {err['error_description']}", "alert-danger")
+            return redirect(url_for(
+                "oauth2.resource.view_resource", resource_id=resource_id))
+
+        def __assign_success__(success):
+            flash(success["description"], "alert-success")
+            return redirect(url_for(
+                "oauth2.resource.view_resource", resource_id=resource_id))
+
+        return oauth2_post(
+            f"oauth2/resource/{resource_id}/user/assign",
+            data={
+                "group_role_id": group_role_id,
+                "user_email": user_email
+            }).either(__assign_error__, __assign_success__)
+    except AssertionError as aserr:
+        flash(aserr.args[0], "alert-danger")
+        return redirect(url_for("oauth2.resources.view_resource", resource_id=resource_id))
+
+@resources.route("<uuid:resource_id>/user/unassign", methods=["POST"])
+@require_oauth2
+def unassign_role(resource_id: uuid.UUID) -> Response:
+    form = request.form
+    group_role_id = form.get("group_role_id", "")
+    user_id = form.get("user_id", "")
+    try:
+        assert bool(group_role_id), "The role must be provided."
+        assert bool(user_id), "The user id must be provided."
+
+        def __unassign_error__(error):
+            err = process_error(error)
+            flash(f"{err['error']}: {err['error_description']}", "alert-danger")
+            return redirect(url_for(
+                "oauth2.resource.view_resource", resource_id=resource_id))
+
+        def __unassign_success__(success):
+            flash(success["description"], "alert-success")
+            return redirect(url_for(
+                "oauth2.resource.view_resource", resource_id=resource_id))
+
+        return oauth2_post(
+            f"oauth2/resource/{resource_id}/user/unassign",
+            data={
+                "group_role_id": group_role_id,
+                "user_id": user_id
+            }).either(__unassign_error__, __unassign_success__)
+    except AssertionError as aserr:
+        flash(aserr.args[0], "alert-danger")
+        return redirect(url_for("oauth2.resources.view_resource", resource_id=resource_id))
+
 @resources.route("/edit/<uuid:resource_id>", methods=["GET"])
 @require_oauth2
 def edit_resource(resource_id: uuid.UUID):
diff --git a/wqflask/wqflask/oauth2/roles.py b/wqflask/wqflask/oauth2/roles.py
index 0b181264..cabfdfac 100644
--- a/wqflask/wqflask/oauth2/roles.py
+++ b/wqflask/wqflask/oauth2/roles.py
@@ -1,22 +1,38 @@
 """Handle role endpoints"""
 import uuid
 
-from flask import Blueprint, render_template
+from flask import flash, request, url_for, redirect, Blueprint, render_template
 
 from .checks import require_oauth2
 from .client import oauth2_get, oauth2_post
-from .request_utils import request_error
+from .request_utils import request_error, process_error
 
 roles = Blueprint("role", __name__)
 
 @roles.route("/user", methods=["GET"])
 @require_oauth2
 def user_roles():
-    def __success__(roles):
-        return render_template("oauth2/list_roles.html", roles=roles)
+    def  __grerror__(roles, user_privileges, error):
+        return render_template(
+            "oauth2/list_roles.html", roles=roles,
+            user_privileges=user_privileges,
+            group_roles_error=process_error(error))
+
+    def  __grsuccess__(roles, user_privileges, group_roles):
+        return render_template(
+            "oauth2/list_roles.html", roles=roles,
+            user_privileges=user_privileges, group_roles=group_roles)
+
+    def __role_success__(roles):
+        uprivs = tuple(
+            privilege["privilege_id"] for role in roles
+            for privilege in role["privileges"])
+        return oauth2_get("oauth2/group/roles").either(
+            lambda err: __grerror__(roles, uprivs, err),
+            lambda groles: __grsuccess__(roles, uprivs, groles))
 
     return oauth2_get("oauth2/user/roles").either(
-        request_error, __success__)
+        request_error, __role_success__)
 
 @roles.route("/role/<uuid:role_id>", methods=["GET"])
 @require_oauth2
@@ -26,3 +42,55 @@ def role(role_id: uuid.UUID):
 
     return oauth2_get(f"oauth2/role/view/{role_id}").either(
         request_error, __success__)
+
+@roles.route("/create", methods=["GET", "POST"])
+@require_oauth2
+def create_role():
+    """Create a new role."""
+    def __roles_error__(error):
+        return render_template(
+            "oauth2/create-role.html", roles_error=process_error(error))
+
+    def __gprivs_error__(roles, error):
+        return render_template(
+            "oauth2/create-role.html", roles=roles,
+            group_privileges_error=process_error(error))
+
+    def __success__(roles, gprivs):
+        uprivs = tuple(
+            privilege["privilege_id"] for role in roles
+            for privilege in role["privileges"])
+        return render_template(
+            "oauth2/create-role.html", roles=roles, user_privileges=uprivs,
+            group_privileges=gprivs,
+            prev_role_name=request.args.get("role_name"))
+
+    def __fetch_gprivs__(roles):
+        return oauth2_get("oauth2/group/privileges").either(
+            lambda err: __gprivs_error__(roles, err),
+            lambda gprivs: __success__(roles, gprivs))
+
+    if request.method == "GET":
+        return oauth2_get("oauth2/user/roles").either(
+            __roles_error__, __fetch_gprivs__)
+
+    form = request.form
+    role_name = form.get("role_name")
+    privileges = form.getlist("privileges[]")
+    if len(privileges) == 0:
+        flash("You must assign at least one privilege to the role",
+              "alert-danger")
+        return redirect(url_for(
+            "oauth2.role.create_role", role_name=role_name))
+    def __create_error__(error):
+        err = process_error(error)
+        flash(f"{err['error']}: {err['error_description']}",
+              "alert-danger")
+        return redirect(url_for("oauth2.role.create_role"))
+    def __create_success__(*args):
+        flash("Role created successfully.", "alert-success")
+        return redirect(url_for("oauth2.role.user_roles"))
+    return oauth2_post(
+        "oauth2/group/role/create",data={
+            "role_name": role_name, "privileges[]": privileges}).either(
+        __create_error__,__create_success__)
diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html
index 7145cd7a..4b1237f5 100644
--- a/wqflask/wqflask/templates/base.html
+++ b/wqflask/wqflask/templates/base.html
@@ -28,6 +28,10 @@
     {% endblock %}
 
     <style>
+
+        .form-rounded {
+            border-radius: 1rem;
+        }
         table.dataTable thead .sorting_asc {
             background-image: url({{ url_for("js", filename="DataTables/images/sort_asc_disabled.png") }});
         }
@@ -135,14 +139,19 @@
 
     <div class="container-fluid" style="width: 100%; min-width: 650px; position: relative; background-color: #d5d5d5; height: 84px;">
 
-        <form method="get" action="/gsearch">
+        <form method="get" action="/gsearch" id="searchform">
             <div class="row" style="width: 100%; position: absolute; bottom: 0; top: 30px;">
+
+                <!--
                 <button id="btsearch" class="btn btn-primary" style="margin-left: 20px;"><span class="glyphicon glyphicon-search"></span> Search All</button>
+
+             use enter key to submit fomr -->
                 <select style="width: 160px; margin-top: 15px; margin-left: 10px;" name="type">
                     <option value="gene">Genes / Molecules</option>
                     <option value="phenotype" {% if type=="phenotype" %}selected{% endif %}>Phenotypes</option>
                 </select>
-                <input style="width: 50%; margin-top: 15px; margin-left: 10px;" type="text" name="terms" required>
+
+                <input id="term"  class="form-rounded" style="width: 40vw; margin-top: 15px; margin-left: 10px;" type="text" name="terms" required placeholder="enter you search term here">
             </div>
         </form>
     </div>
@@ -247,6 +256,7 @@
       <!--</div>-->
     </div>
 
+
     <script src="{{ url_for('js', filename='jquery/jquery.min.js') }}" type="text/javascript"></script>
     <script src="{{ url_for('js', filename='bootstrap/js/bootstrap.min.js') }}" type="text/javascript"></script>
     <script>
@@ -259,6 +269,41 @@
               }
             })
     </script>
+
+
+    <script type="text/javascript">
+
+
+
+$(document).ready(function() {
+
+
+    const urlParams = new  URLSearchParams(window.location.search)
+
+   let term = "cytochrome NOT P450"
+
+   if (urlParams.get("search_terms_or")){
+    term =  urlParams.get("search_terms_or")
+   }
+
+   else if (urlParams.get("terms")){
+    term = urlParams.get("terms")
+   }
+
+    $("#term").val(term);
+    $("#term").keyup(function(event) {
+  if (event.keyCode === 13) {
+    event.preventDefault();
+    $('#searchform').attr('action', "/gsearch").submit();
+    }
+    });    
+
+
+});
+
+
+    </script>
+
     <script src="{{ url_for('js', filename='jquery-cookie/jquery.cookie.js') }}" type="text/javascript"></script>
     <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
     <!-- <script src="{{ url_for('js', filename='jquery-ui/jquery-ui.min.js') }}" type="text/javascript"></script> -->
diff --git a/wqflask/wqflask/templates/gsearch_gene.html b/wqflask/wqflask/templates/gsearch_gene.html
index 03e5019c..6bc92377 100644
--- a/wqflask/wqflask/templates/gsearch_gene.html
+++ b/wqflask/wqflask/templates/gsearch_gene.html
@@ -175,8 +175,15 @@
                 'orderSequence': [ "desc", "asc"],
                 'width': "30px",
                 'targets': 10,
-                'data': "mean",
-                'defaultContent': "N/A"
+                'data': null,
+                'defaultContent': "N/A",
+                'render': function(data) {
+                  if (data.mean > 100) {
+                    return Math.log2(data.mean).toFixed(3)
+                  } else {
+                    return data.mean
+                  }
+                }
               },
               {
                 'title': "<div style='text-align: right; padding-right: 10px;'>Peak</div> <div style='text-align: right;'>-logP <a href=\"{{ url_for('glossary_blueprint.glossary') }}#LRS\" target=\"_blank\" style=\"color: white;\"><sup>?</sup></a></div>",
diff --git a/wqflask/wqflask/templates/oauth2/create-role.html b/wqflask/wqflask/templates/oauth2/create-role.html
new file mode 100644
index 00000000..f2bff7b4
--- /dev/null
+++ b/wqflask/wqflask/templates/oauth2/create-role.html
@@ -0,0 +1,46 @@
+{%extends "base.html"%}
+{%from "oauth2/profile_nav.html" import profile_nav%}
+{%from "oauth2/display_error.html" import display_error%}
+{%block title%}View User{%endblock%}
+{%block content%}
+<div class="container" style="min-width: 1250px;">
+  {{profile_nav("roles", user_privileges)}}
+  <h3>Create Role</h3>
+
+  {{flash_me()}}
+
+  {%if group_privileges_error is defined%}
+  {{display_error("Group Privileges", group_privileges_error)}}
+  {%else%}
+  {%if "group:role:create-role" in user_privileges%}
+  <form method="POST" action="{{url_for('oauth2.role.create_role')}}">
+    <legend>Create Group Role</legend>
+    <div class="form-group">
+      <label for="role_name" class="form-label">Name</label>
+      <input type="text" id="role_name" name="role_name" required="required"
+	     class="form-control"
+	     {%if prev_role_name is defined and prev_role_name is not none%}
+	     value="{{prev_role_name}}"
+	     {%endif%} />
+    </div>
+    <label class="form-label">Privileges</label>
+    {%for priv in group_privileges%}
+    <div class="checkbox">
+      <label for="chk:{{priv.privilege_id}}">
+	<input type="checkbox" id="chk:{{priv.privilege_id}}"
+	       name="privileges[]" value={{priv.privilege_id}} />
+	<span style="text-transform: capitalize;">
+	  {{priv.privilege_description}}
+	</span> ({{priv.privilege_id}})
+      </label>
+    </div>
+    {%endfor%}
+
+    <input type="submit" class="btn btn-primary" value="Create" />
+  </form>
+  {%else%}
+  {{display_error("Privilege", {"error":"PrivilegeError", "error_description": "You do not have sufficient privileges to create a new role."})}}
+  {%endif%}
+  {%endif%}
+</div>
+{%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/list_roles.html b/wqflask/wqflask/templates/oauth2/list_roles.html
index 028d0a17..6feccb6f 100644
--- a/wqflask/wqflask/templates/oauth2/list_roles.html
+++ b/wqflask/wqflask/templates/oauth2/list_roles.html
@@ -1,15 +1,17 @@
 {%extends "base.html"%}
 {%from "oauth2/profile_nav.html" import profile_nav%}
+{%from "oauth2/display_error.html" import display_error%}
 {%block title%}View User{%endblock%}
 {%block content%}
 <div class="container" style="min-width: 1250px;">
-  {{profile_nav("roles")}}
+  {{profile_nav("roles", user_privileges)}}
   <h3>Roles</h3>
 
   {{flash_me()}}
 
   <div class="container-fluid">
     <div class="row">
+      <h3>Your System-Level Roles</h3>
       <ul>
 	{%for role in roles %}
 	<li>
@@ -25,6 +27,60 @@
       </ul>
     </div>
 
+    <div class="row">
+      <h3>Group-Wide Roles</h3>
+
+      {%if "group:role:create-role" in user_privileges%}
+      <a href="{{url_for('oauth2.role.create_role')}}"
+	 title="Link to create a new group role"
+	 class="btn btn-info">New Group Role</a>
+      {%endif%}
+
+      {%if group_roles_error is defined%}
+      {{display_error("Group Roles", group_role_error)}}
+      {%else%}
+      <table class="table">
+	<caption>Group Roles</caption>
+	<thead>
+	  <tr>
+	    <th>Role Name</th>
+	    <th colspan="2">Actions</th>
+	  </tr>
+	</thead>
+	<tbody>
+	  {%for grole in group_roles%}
+	  <tr>
+	    <td>{{grole.role.role_name}}</td>
+	    <td>
+	      <a href="{{url_for('oauth2.group.group_role', group_role_id=grole.group_role_id)}}"
+		 title="Link to role {{grole.role.role_name}}"
+		 class="btn btn-info">
+		View
+	      </a>
+	    </td>
+	    <td>
+	      <a href="#/edit/role"
+		 title="Edit role {{grole.role.role_name}}"
+		 class="btn btn-warning">
+		Edit
+	      </a>
+	    </td>
+	  </tr>
+	  {%else%}
+	  <tr>
+	    <td colspan="3">
+	      <span class="glyphicon glyphicon-exclamation-sign text-info">
+	      </span>
+	      &nbsp;
+	      <span class="text-info">No group roles found</span>
+	    </td>
+	  </tr>
+	  {%endfor%}
+	</tbody>
+      </table>
+      {%endif%}
+    </div>
+
   </div>
 
 </div>
diff --git a/wqflask/wqflask/templates/oauth2/view-group-role.html b/wqflask/wqflask/templates/oauth2/view-group-role.html
new file mode 100644
index 00000000..873eb0ee
--- /dev/null
+++ b/wqflask/wqflask/templates/oauth2/view-group-role.html
@@ -0,0 +1,96 @@
+{%extends "base.html"%}
+{%from "oauth2/profile_nav.html" import profile_nav%}
+{%from "oauth2/display_error.html" import display_error%}
+{%block title%}View User{%endblock%}
+{%block content%}
+<div class="container" style="min-width: 1250px;">
+  {{profile_nav("roles")}}
+  <h3>View Group Role</h3>
+
+  {{flash_me()}}
+
+  <div class="container-fluid">
+    <div class="row">
+      <h3>Role Details</h3>
+      {%if group_role_error is defined%}
+      {{display_error("Group Role", group_role_error)}}
+      {%else%}
+      <table class="table">
+	<caption>Details for '{{group_role.role.role_name}}' Role</caption>
+	<thead>
+	  <tr>
+	    <th>Privilege</th>
+	    <th>Description</th>
+	    <th>Action</th>
+	  </tr>
+	</thead>
+	<tbody>
+	  {%for privilege in group_role.role.privileges%}
+	  <tr>
+	    <td>{{privilege.privilege_id}}</td>
+	    <td>{{privilege.privilege_description}}</td>
+	    <td>
+	      <form action="{{url_for(
+			    'oauth2.group.delete_privilege_from_role',
+			    group_role_id=group_role.group_role_id)}}"
+		    method="POST">
+		<input type="hidden" name="privilege_id"
+		       value="{{privilege.privilege_id}}" />
+		<input type="submit" class="btn btn-danger"
+		       value="Remove" />
+	      </form>
+	    </td>
+	  </tr>
+	  {%endfor%}
+	</tbody>
+      </table>
+      {%endif%}
+    </div>
+
+    <div class="row">
+      <h3>Other Privileges</h3>
+      <table class="table">
+	<caption>Other Privileges not Assigned to this Role</caption>
+	<thead>
+	  <tr>
+	    <th>Privilege</th>
+	    <th>Description</th>
+	    <th>Action</th>
+	  </tr>
+	</thead>
+
+	<tbody>
+	  {%for priv in group_privileges%}
+	  <tr>
+	    <td>{{priv.privilege_id}}</td>
+	    <td>{{priv.privilege_description}}</td>
+	    <td>
+	      <form action="{{url_for(
+			    'oauth2.group.add_privilege_to_role',
+			    group_role_id=group_role.group_role_id)}}"
+		    method="POST">
+		<input type="hidden" name="privilege_id"
+		       value="{{priv.privilege_id}}" />
+		<input type="submit" class="btn btn-warning"
+		       value="Add to Role" />
+	      </form>
+	    </td>
+	  </tr>
+	  {%else%}
+	  <tr>
+	    <td colspan="3">
+	      <span class="glyphicon glyphicon-info-sign text-info">
+	      </span>
+	      &nbsp;
+	      <span class="text-info">All privileges assigned!</span>
+	    </td>
+	  </tr>
+	  {%endfor%}
+	</tbody>
+      </table>
+    </div>
+
+  </div>
+
+</div>
+{%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/view-resource.html b/wqflask/wqflask/templates/oauth2/view-resource.html
index fb44560b..14e7872b 100644
--- a/wqflask/wqflask/templates/oauth2/view-resource.html
+++ b/wqflask/wqflask/templates/oauth2/view-resource.html
@@ -12,6 +12,7 @@
   <div class="container-fluid">
 
     <div class="row">
+      <h3>Resource Details</h3>
       {%if error %}
       <span class="glyphicon glyphicon-exclamation-sign text-danger">
       </span>
@@ -41,6 +42,7 @@
     </div>
 
     <div class="row">
+      <h3>Resource Data</h3>
       <table class="table">
 	<caption>Resource Data</caption>
 	<thead>
@@ -88,6 +90,7 @@
     </div>
 
     <div class="row">
+      <h3>Unlinked Data</h3>
       <table class="table">
 	<caption>Link Data</caption>
 	<thead>
@@ -125,7 +128,7 @@
 	    </td>
 	  </tr>
 	  {%else%}
-	  <span class="glyphicon glyphicon-info-sign text-danger">
+	  <span class="glyphicon glyphicon-info-sign text-info">
 	  </span>
 	  &nbsp;
 	  <strong class="text-info">No data to link.</strong>
@@ -135,6 +138,122 @@
       </table>
     </div>
 
+    <div class="row">
+      <h3>User Roles</h3>
+      {%if users_n_roles_error is defined%}
+      {{display_error("Users and Roles", users_n_roles_error)}}
+      {%else%}
+      <table class="table">
+	<caption>User Roles</caption>
+	<thead>
+	  <tr>
+	    <th>User Email</th>
+	    <th>User Name</th>
+	    <th>User Group</th>
+	    <th colspan="2">Assigned Roles</th>
+	  </tr>
+	</thead>
+	<tbody>
+	  {%for user_row in users_n_roles%}
+	  <tr>
+	    <td rowspan="{{user_row.roles | length + 1}}">{{user_row.user.email}}</td>
+	    <td rowspan="{{user_row.roles | length + 1}}">{{user_row.user.name}}</td>
+	    <td rowspan="{{user_row.roles | length + 1}}">
+	      {{user_row.user_group.group_name}}</td>
+	    <th>Role</th>
+	    <th>Action</th>
+	  </tr>
+	  {%for grole in user_row.roles%}
+	  <tr>
+	    <td>
+	      <a href="{{url_for(
+		       'oauth2.group.group_role',
+		       group_role_id=grole.group_role_id)}}"
+		 title="Details for '{{grole.role.role_name}}' role">
+		{{grole.role.role_name}}
+	      </a>
+	    </td>
+	    <td>
+	      <form action="{{url_for('oauth2.resource.unassign_role',
+			    resource_id=resource.resource_id)}}"
+		    method="POST">
+		<input type="hidden" name="user_id"
+		       value="{{user_row.user.user_id}}" />
+		<input type="hidden" name="group_role_id"
+		       value="{{grole.group_role_id}}">
+		<input type="submit"
+		       value="Unassign"
+		       class="btn btn-danger"
+		       {%if user_row.user.user_id==this_user.user_id%}
+		       disabled="disabled"
+		       {%endif%}>
+	      </form>
+	    </td>
+	  </tr>
+	  {%endfor%}
+	  {%else%}
+	  <tr>
+	    <td colspan="5">
+	      <span class="glyphicon glyphicon-info-sign text-info">
+	      </span>
+	      &nbsp;
+	      <span class="text-info">
+		There are no users assigned any role for this resource.
+	      </span>
+	    </td>
+	  </tr>
+	  {%endfor%}
+	</tbody>
+      </table>
+      {%endif%}
+    </div>
+
+    <div class="row">
+      <h3>Assign</h3>
+      {%if group_roles_error is defined%}
+      {{display_error("Group Roles", group_roles_error)}}
+      {%elif users_error is defined%}
+      {{display_error("Users", users_error)}}
+      {%else%}
+      <form action="{{url_for(
+		    'oauth2.resource.assign_role',
+		    resource_id=resource.resource_id)}}"
+	    method="POST" autocomplete="off">
+	<input type="hidden" name="resource_id" value="{{resource_id}}" />
+	<div class="form-group">
+	  <label for="group_role_id" class="form-label">Role</label>
+	  <select class="form-control" name="group_role_id"
+		  id="group_role_id" required="required">
+	    <option value="">Select role</option>
+	    {%for grole in group_roles%}
+	    <option value="{{grole.group_role_id}}">
+	      {{grole.role.role_name}}
+	    </option>
+	    {%endfor%}
+	  </select>
+	</div>
+	<div class="form-group">
+	  <label for="user-email" class="form-label">User Email</label>
+	  <input list="users-list" name="user_email" class="form-control"
+		 {%if users | length == 0%}
+		 disabled="disabled"
+		 {%endif%}
+		 required="required" />
+	  <datalist id="users-list">
+	    {%for user in users%}
+	    <option value="{{user.email}}">{{user.email}} - {{user.name}}</option>
+	    {%endfor%}
+	  </datalist>
+	</div>
+
+	<input type="submit" class="btn btn-primary" value="Assign"
+	       {%if users | length == 0%}
+	       disabled="disabled"
+	       {%endif%} />
+      </form>
+      {%endif%}
+    </div>
+
   </div>
 
 </div>