I have built a review app based on Google's "people viewer" template that allows managers to create and edit reviews for their direct reports.
The app contains the directory model as well as three roles: Admins, HR, EndUsers.
The app contains a user settings model that allows to create and store user settings similar to the "people skills" template.
The app contains a review model that will contain the reviews for every employee. As one employee can have several reviews, this will be a one-to-many relation, either linked to directory model or user settings model.
The reviews should be readable by managers chain of manager. For this I have created a server script, assuming that the EmployeeEmail will be additionally stored in the review. But maybe there is a better alternative?
function getDirectReportsChainForUser_(query) {
var userQuery = app.models.Directory.newQuery();
userQuery.filters.PrimaryEmail._equals = query.parameters.PrimaryEmail;
userQuery.prefetch.DirectReports._add();
userQuery.prefetch.DirectReports.DirectReports._add();
var users = userQuery.run();
if (users.length === 0) {
return [];
}
var user = users[0];
var directs = user.DirectReports;
var records = [];
for (var i = 0; i <= directs.length; i++) {
records.push(directs[i].PrimaryEmail);
}
// The following lines are based on the asumption that the EmployeeEmail
// will be stored in the review in case that there is no better alternative.
//The question that then remains is how to recursively add the DirectReports
//of the DirectReports to the array???
var reviewQuery = app.models.Reviews.newQuery();
reviewQuery.filters.EmployeeEmail._in = records;
return reviewQuery.run();
}
The manager should be able to define whether one or more of his deputies can read the reviews for his unit, too. My idea was to solve this issue through a many-to-many relation between the directory and review model, but I am not sure how to implement it?
Furthermore, once a manager or his deputy departures, it should be possible for the Admin to dissolve the connection and to reconnect the reviews to a successor. Therefore I was thinking about integrating a multiselect in the admin page. Would this be feasible?
Here I see at least two distinct questions:
is there better way to associate directory model's record and ordinary data model than just adding primary email field to the data model
Nope, at this time it is not possible to establish relations between data (SQL/Drive Tables) and directory models.
how to recursively get all direct reports for a user
App Maker's Directory Model is a wrapper on top of G Suit Admin SDK's Directory API that exposes just a small subset of its powerful features. When you add Directory Model App Maker automatically plugs in correspondent Apps Script advance service:
Since we already have configured Directory API we can unleash its full power and easily fetch all manger's subordinates with a single call (or multiple if you have a need to support paging). In order to do that we will use Users.List API method with managerId query parameter (the only one that allows us to query all subordinates down the tree). Here are reference for the minimal set of search query parameters quoted from the full search documentation (without those parameters query would not work or wouldn't work in a way we need):
managerId: The ID of a user's manager either directly or up the management chain.
domain: The domain name. Use this field to get fields from only one domain. To return all domains for a customer account, use the customer query parameter instead. Either the customer or the domain parameter must be provided.
viewType: Whether to fetch the administrator-only or domain-wide public view of the user. For more information, see Retrieve a user as a non-administrator (admin_view is default value so we need to override it with domain_view).
query: Query string for searching user fields. For more information on constructing user queries, see Search for Users.
/**
* Fetches all reviews associated with all subordinate employees (both direct
* and indirect reports).
*/
function getAllReportsEmails(managerId) {
var emails = [];
var result = AdminDirectory.Users.list({
domain: 'ENTER HERE YOUR DOMAIN (exapmle.com)',
query: 'managerId=' + managerId,
viewType: 'domain_public',
maxResults: 100
});
if (result.users) {
emails = result.users.map(function (user) {
return user.primaryEmail;
});
}
return emails;
}
/**
* Fetches all reviews associated with all subordinate employees (both direct
* and indirect reports).
*/
function getAllReportsReviewsForManager_(query) {
var userQuery = app.models.Directory.newQuery();
// For better security I would recommend to use
// Session.getActiveUser().getEmail() instead of parameter
// passed from the client.
userQuery.filters.PrimaryEmail._equals = Session.getActiveUser().getEmail();
var users = userQuery.run();
if (users.length === 0) {
return [];
}
var manager = users[0];
var managerId = manager._key;
var allReportsEmails = getAllReportsEmails(managerId);
var reviewQuery = app.models.Reviews.newQuery();
reviewQuery.filters.EmployeeEmail._in = allReportsEmails;
return reviewQuery.run();
}
Pavel, I tried to integrate the ideas you gave me into one server script that returns an array of the manager and his whole subordinate chains (direct reports + indirect reports), so that I can use it whenever needed. I turned into a recursive function to get the direct reports and indirect reports on the next lower level. Is there a way to get the whole chain?
function getSubordinatesChainForUser(query) {
var userQuery = app.models.Directory.newQuery();
userQuery.filters.PrimaryEmail._equals = Session.getActiveUser().getEmail();
userQuery.prefetch.DirectReports._add();
userQuery.prefetch.DirectReports.DirectReports._add();
var users = userQuery.run();
if (users.length === 0) {
return [];
}
var userEmails = users.map(function(manager){
var employeeEmails = manager.DirectReports.map(function(employee){
return employee.PrimaryEmail;
});
return manager.PrimaryEmail + ',' + employeeEmails;
});
return userEmails;
}
Related
I am creating an application that will help our employees manage tasks. Employees are divided into regions in the country. I would like the employees from a given region to see tasks related only to this region.
Workflow:
A task is submitted through the form. One of the fields in the form is the "name of the region".
The task falls into view for region.
Employees can take over the task from the view for their region.
I have MAIN data model named Service where embedded is form for submitting a task.
I also have data dictionary modal, where I have imported data about employee and his region.
Questions:
How to create a view for employees from certain region?
Should I do some relation?
EDIT: I have tried to do datasource on Service modal and write some query doing filters, but I don't exactly understand how the query works.
EDIT 2: This is how modals look like:
Service:
DOT:
The way to go here probably would be to have a datasource under your Service model that filters your Service records on Province via a subquery of the DOT model. This can be accomplished via a query server script in this datasource.
Example code would be (recall that 'query' is a default variable for the current model server script so the subquery needs it's own variable):
var DOTmodelquery = app.models.DOT.newQuery();
DOTmodelquery.filters.email._equals = Session.getActiveUser().getEmail();
var result = DOTmodelquery.run();
if (result === undefined || result.length === 0) {
throw new app.ManagedError('Could not retrieve user to perform this query!');
} else if (result[0].Province === null) {
throw new app.ManagedError('User does not have a Province value to perform this query!');
} else {
query.filters.Province._equals = result[0].Province;
}
return query.run();
Not sure that a relationship between Service and DOT would be the way to go here but you could possibly consider establishing a Province model and then having a relationship between Province-Service and Province-DOT and then try running a query in Service like so query.filters.Province.DOT.email._equals = Session.getActiveUser().getEmail();. Hopefully this will get you what you were hoping for.
Is there a way to get "User Google Id" from the session in App Maker. In the documentation its only mentioned how to retrieve the email of the logged in user Session.getActiveUser().getEmail() but no where it says how to get the id. I need this because the user email might sometimes changes. So I need the user id to keep track of users and related permission tasks. Or is there something I'm missing out here in how this should be implemented.
Yet an easier way to find Google Id simply using the Directory model. Although its mentioned in documentation that there is a way to get current signed in user id ( which is Google Id), its not clearly stated how - maybe documentation could be improved here. Another problem is that in many occasions the email of current active user is referred to as the id for example in deprecated method Session.getActiveUser().getUserLoginId(). Anyways this is a proper way to get the id.
var query = app.models.Directory.newQuery();
query.filters.PrimaryEmail._equals = Session.getActiveUser().getEmail();
var result = query.run();
var GoogleId = result[0]._key;
So with this GoogleId you can safely relate different models with each other and not worry that database integrity might break if an already referenced user email is changed.
Relating the different models could be done simply by creating a model that acts as a wrapper model around the Directory model and storing GoogleId in it. Then linking that model to other models where you want to track user related data because unfortunately we can not directly link The Directory Model to other models.
A team member has figured it out. This should be done using Apps Script - which works within App Maker environment using server side script.
var GoogleUser = (function (){
/**
*
* #param {string} email
*/
function getUserObjByEmail(email){
// Same as using AdminDirectory class.
var apiUrl = "https://www.googleapis.com/admin/directory/v1/users/"+email+"?fields=id";
var token = ScriptApp.getOAuthToken();
var header = {"Authorization":"Bearer " + token};
var options = {
"method": "GET",
"headers": header
};
var response = JSON.parse(UrlFetchApp.fetch(apiUrl, options));
return response;
}
/**
*
* #param {string} email - User email.
*/
function getIdByEmail(email){
return getUserObjByEmail(email)['id'];
}
var publicApi = {
getIdByEmail: getIdByEmail
};
return publicApi;
})();
Note that using var apiUrl = "https://www.googleapis.com/admin/directory/v1/users/"+email+"?fields=id"; is not going to be asynchronously called because its already happening in the server.
Is this a dup of this question?
I think this will solve your problem, even though it's a bit of a hack.
Using GoogleAppMaker how to create a data source from google contacts. There is an employee HR example app but I want to similarly manage contacts (add, modify, delete) and use select criteria.
At this time this task is not trivial in App Maker and it is pretty much generic. We can change question wording to CRUD operations with 3rd party datasources. Let's break it into smaller parts and address them separately.
Read/list contacts
This task is relatively easy. You need to use Calculated Model to proxy Apps Scripts Contacts API response. Once you create model with subset of fields from the Contact response you can create datasource for the model and bind it to List or Table widget. You can also try to find some inspiration in Calculated Model Sample.
// Server side script
function getContacts_() {
var contacts = ContactsApp.getContacts();
var records = contacts.map(function(contact) {
var record = app.models.Contact.newRecord();
record.FirstName = contact.getGivenName();
record.LastName = contact.getFamilyName();
var companies = contact.getCompanies();
if (companies.length > 0) {
var company = companies[0];
record.Organization = company.getCompanyName();
record.Title = company.getJobTitle();
}
var emails = contact.getEmails();
if (emails.length > 0) {
record.Email = emails[0].getAddress();
}
var phones = contact.getPhones();
if (phones.length > 0) {
record.Phone = phones[0].getPhoneNumber();
}
return record;
});
return records;
}
Create/Update/Delete
Since Calculated Models have some limitations, we need to turn on our imagination to create, update and delete records from their datasources. The basic strategy will be calling server side scripts for CUD operations in response to user actions on client side. To get user's input from UI we will need to utilize page's Custom Properties, one property for each Contact field:
Here are some snippets that should explain the idea
Create
// Client script
function onSubmitContactClick(submitButton) {
var props = submitButton.root.properties;
var contact = {
FirstName: props.FirstName,
LastName: props.LastName,
Organization: props.Organization,
...
};
google.script.run
.withSuccessHandler(function() {
// Most likely we'll need to navigate user back to the
// page with contacts list and reload its datasource
// to reflect recent changes, because our `CUD` operations
// are fully detached from the list datasource
app.showPage(app.pages.Contacts);
app.datasources.Contacts.load();
})
.withFailureHandler(function() {
// TODO: Handle error
})
.createContact(contact);
}
// Server script
function createContact(contactDraft) {
var contact = ContactsApp.createContact(contactDraft.FirsName,
contactDraft.LastName,
contactDraft.Email);
contact.addCompany(contactDraft.Organization, contactDraft.Title);
contact.addPhone(ContactsApp.Field.WORK_PHONE, contactDraft.Phone);
}
Update
Idea to update contact records will be very similar to the new contact creation flow, so I skip it for now.
Delete
Assuming that delete button is located inside contacts table row.
// Client script
function onDeleteContactClick(deleteButton) {
var email = deleteButton.datasource.item.Email;
google.script.run
.withSuccessHandler(function() {
// To update contacts list we can either reload the entire
// datasource or explicitly remove deleted item on the client.
// Second option will work way faster.
var contactIndex = deleteButton.parent.childIndex;
app.datasources.Contacts.items.splice(contactIndex, 1);
})
.withFailureHandler(function() {
// TODO: Handle error
})
.deleteContact(contact);
}
// Server script
function deleteContact(email) {
var contact = ContactsApp.getContact(email);
ContactsApp.deleteContact(contact);
}
Why does running this code
var userId = Meteor.userId();
var user = Users.findOne(userId, { fields: { earnings: 1 } });
Return
{ _id: 'Co5bMySeaqySgDP6h', earnings: { period: 0.6, total: 52.5 } }
Instead of returning all the fields on the user, including the earnings (custom field)
Also, is there a way to make user queries automatically return custom specified fields, so I dont have to manually specify it each time I need it?
Much appreciated
The reason that you only get the specified field (plus the id) is given in the docs:
To include only specific fields in the result documents, use 1 as the value. The _id field is still included in the result.
If instead you just call Meteor.users.findOne(userId) it will return all of the available fields. If this is called on the server, that will be the entire document, but if you use it on the client, it will only return the fields that have been published from the server, which by default is just the username and the emails and profile fields. Again, per the docs:
On the client, this will be the subset of the fields in the document that are published from the server (other fields won't be available on the client). By default the server publishes username, emails, and profile (writable by user). See Meteor.users for more on the fields used in user documents.
This means that if you have added a new field to you user docs, you need to explicitly publish it for it to be available on the client (assuming autopublish has been removed). Note that it's fine to do this using the previously discussed fields specifier as the other required details (username, profile) will not be overwritten by another publish function unless you try to publish the same top-level field again.
Meteor.publish('earnings', function() {
return Meteor.users.find(this.userId, { fields: { earnings: 1 } });
};
(Publish functions expect you to return a cursor rather than an array, so you need to use find rather than findOne even if there will only be one result).
Finally, it's easy to add your own methods to a collection to make finding stuff you want more concise.
Meteor.users.findSimple = function(selector, options) {
options = options || {};
options.fields = options.fields || {};
options.fields.earnings = 1;
\\ same thing for any other fields you want to limit this find to;
return this.find(selector, options);
};
I'm struggling to get a collection of records using L2E. Here's the model view:
http://pascalc.nougen.com/stuffs/aspnet_linq_model2.png
I have a user identifier, which is associated to 1 or many UserGroup which themselves are linked to TestCase. I would like to get all TestCases of all groups the user id X is associated to.
I also notice that I don't get all Project for users that are associated to 2 (or more).
Here's how I do so far:
QASModel.QASEntities qasEntities = new QASModel.QASEntities();
QASModel.User authenticatedUserEntity = (from u in qasEntities.Users
where u.ID.Equals(authenticatedUserId)
select u).FirstOrDefault();
// Get the authenticated user usergroups
var usergroup = authenticatedUserEntity.UserGroups.FirstOrDefault();
// Get all testcases of all user group the authenticated user is associated to
var allTestcases = usergroup.TestCases;
// Get the authenticated user projects based on it's usergroup(s)
var authenticatedUserProjects = usergroup.Projects;
authenticatedUserProjects give back only 1 project, where the user is linked to 2 projects.
And allTestcases gives back no result, although there are about 8 entries in TestCases associated to a project associated to one of the same UserGroup the user belongs to.
Thanks
I think your problem is in this line:
var usergroup = authenticatedUserEntity.UserGroups.FirstOrDefault();
Shouldn't your code get all UserGroups of that User? The above line will return only 1 UserGroup, this is, if the user belongs to more than 1 UserGroup the 2nd one won't be returned.
To correct this:
var userTestCases = new List<TestCase>();
var userProjects = new List<Project>();
foreach(UserGroup ug in authenticatedUserEntity.UserGroups)
{
userTestCases = userTestCases.Concat(ug.TestCases);
// Get the authenticated user projects based on it's usergroup(s)
userProjects = userProjects.Concat(ug.Projects);
...
}
var usergroup = authenticatedUserEntity.UserGroups.FirstOrDefault();
probably is wrong, since you want all usergroups the user is associated with.
In any case you can easily revert the query to work like this
var testcases = from tc in new QASModel.QASEntities().TestCases
where tc.UserGroup.UserId == USERID
select tc
This way you won't have to execute multiple queries to get all test cases, each FirstOrDefault or ToList or ForEach will actually execute the query.