Microsoft graph
Vamos a por la quinta y última parte de este tutorial.
La cuarta parte la podemos encontrar en este enlace Microsoft graph - Eventos calendarios Outlook IV
Como comenté en un post anterior me gusta ir un poco más allá del "esto funciona asà y adiós", siempre prefiero mostrar un ejemplo con una aplicación práctica, ya sea esta más o menos útil.
En el último post vimos ya resultados "reales" en una herramienta con la que podÃamos interactuar y manipular los eventos. Aunque en lo visto hasta ahora sólo los modificábamos, ahora vamos a utilizar una de las funciones que añadimos en el último post, se trata de crear eventos.
Nuevos eventos
Pensando en Business central y sus operaciones, lo primero que se me viene a la mente que esté delimitada por una fecha y hora de inicio y de fin son las órdenes de fabricación asà que podemos añadir la funcionalidad de crear eventos en el calendario elegido a partir de estas órdenes.
Tenemos varias formas de hacer esto, pero para el ejemplo vamos a añadir a nuestra página en la que hemos estado probando los calendarios un page part con las lÃneas de las órdenes de producción. El resultado final es un poco ...
Tenemos varias formas de hacer esto, pero para el ejemplo vamos a añadir a nuestra página en la que hemos estado probando los calendarios un page part con las lÃneas de las órdenes de producción. El resultado final es un poco ...
... pero disponemos de un sólo vistazo de todo lo visto hasta ahora.
Preparando el ejemplo para este post el primer problema que me encontré era:
Vamos a tener una página padre, la de los calendarios, que va a tener una página hija (un part con las lÃneas de las órdenes de producción). a esta página le podemos añadir por ejemplo una función y una variable global con la que la página padre informa a la hija del calendario, pero ... yo quiero que al añadir un evento al calendario elegido desde la página hija, el calendario se actualice mostrando ya ese nuevo evento ... el calendario está en la página padre ... ¿cómo hago esto?
Es muy posible que existan muchas más formas, pero de primeras se presentó un problema algo complicado de resolver.
Recordé haber viso algo similar en un post de Erik Hougaard, asà que lo localicé y efectivamente, era exactamente lo que necesitaba.
El post al que me refiero se puede ver aquà (debajo de la imagen aparece el link al video en youtube donde explica cómo hacerlo).
Asà pues Erik hizo el 99% del trabajo y yo le di una aplicación práctica a lo visto sobre un caso real: cómo informa la página hija a su padre de que ha creado un evento para que el padre actualice sus datos.
Recordé haber viso algo similar en un post de Erik Hougaard, asà que lo localicé y efectivamente, era exactamente lo que necesitaba.
El post al que me refiero se puede ver aquà (debajo de la imagen aparece el link al video en youtube donde explica cómo hacerlo).
Asà pues Erik hizo el 99% del trabajo y yo le di una aplicación práctica a lo visto sobre un caso real: cómo informa la página hija a su padre de que ha creado un evento para que el padre actualice sus datos.
Será necesario crear un nuevo addin, empezamos por el archivo al, es indiferente su nombre
controladdin childparentpagecomm
{
MaximumHeight = 1;
MaximumWidth = 1;
MinimumHeight = 1;
MinimumWidth = 1;
RequestedHeight = 1;
RequestedWidth = 1;
Scripts = './Objects/Addin/childparentpagecomm.js';
event ReloadCalendar(prodlineid: text);
procedure EventCreated(prodlineid: text);
}
A continuación aplicamos lo visto en el video de Erik, creamos un fichero llamado childparentpagecomm.js
function EventCreated(prodlineid)
{
try
{
for(var i = 0; i < window.parent.frames.length; i++){
if(window.frameElement != window.parent.frames[i].frameElement)
if (window.parent.frames[i].frameElement.contentWindow.EventCreated != null)
window.parent.frames[i].frameElement.contentWindow.SendReload(prodlineid);
}
}
catch(ex)
{
console.log(ex);
}
}
function SendReload(prodlineid)
{
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ReloadCalendar',[prodlineid]);
}
Y ya tendrÃamos comunicación desde la página hija a la padre. Este addin es tan simple como útil.
Volvemos a nuestra página "padre", la que mostraba el calendario y añadimos primero las lÃneas de las órdenes de producción lanzadas, yo lo he puesto a continuación del repeater con los calendarios
group(ProdOrders)
{
Caption = 'Órdenes de producción';
part(ProdOrderLines; "Released Prod. Order Lines")
{
}
}
y debajo del addin del calendario añado el control que nos permite la comunicación desde la página hija a la padre
usercontrol(childparentpagecomm; childparentpagecomm)
{
trigger ReloadCalendar(prodlineid: Text)
var
ProdOrderLine: Record "Prod. Order Line";
ProdOrderLineRecID: RecordId;
Body: Label '%1 %2 %3';
Error001: Label 'El evento no pudo ser creado en el calendario %1';
begin
Evaluate(ProdOrderLineRecID, prodlineid);
if ProdOrderLine.Get(ProdOrderLineRecID) then
if GraphConnectorMngt.NewEvent(Rec.Id, ProdOrderLine.Description, StrSubstNo(Body, ProdOrderLine.Description, ProdOrderLine.Quantity, ProdOrderLine."Unit of Measure Code"), ProdOrderLine."Starting Date-Time", ProdOrderLine."Ending Date-Time", false) then
DrawCalendar(ProdOrderLine."Starting Date")
else
Error(Error001, Rec.Name);
end;
}
y en evento OnAfterGetCurrRecord añado una lÃnea para informar a la página hija que está trabajando con calendarios
CurrPage.ProdOrderLines.Page.SetCalendar();
esto es útil para que en la página del estándar no muestre la acción de añadir evento al calendario.
y aquà vemos la página completa
page 60803 OutlookCalendar
{
ApplicationArea = All;
Caption = 'Eventos calendario Outlook';
PageType = List;
SourceTable = Calendars;
UsageCategory = Lists;
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
layout
{
area(content)
{
group(Calendars)
{
Caption = 'Calendarios';
repeater(General)
{
field(Id; Rec.Id)
{
Visible = false;
}
field(Name; Rec.Name)
{ }
field(IsDefault; Rec.IsDefault)
{ }
}
}
group(ProdOrders)
{
Caption = 'Órdenes de producción';
part(ProdOrderLines; "Released Prod. Order Lines")
{
}
}
group(Events)
{
Caption = 'Eventos';
usercontrol(calendar; calendar)
{
trigger OnStartCalendar()
begin
DrawCalendar();
end;
trigger EventModified(eventTxt: Text; startDateTime: DateTime; endDateTime: DateTime; eventId: Text; allDay: Boolean)
var
Error001: Label 'No pudo ser modificado';
begin
if not GraphConnectorMngt.ModifyEvent(eventId, startDateTime, endDateTime, allDay) then begin
Message(Error001);
DrawCalendar();
end;
end;
}
}
usercontrol(childparentpagecomm; childparentpagecomm)
{
trigger ReloadCalendar(prodlineid: Text)
var
ProdOrderLine: Record "Prod. Order Line";
ProdOrderLineRecID: RecordId;
Body: Label '%1 %2 %3';
Error001: Label 'El evento no pudo ser creado en el calendario %1';
begin
Evaluate(ProdOrderLineRecID, prodlineid);
if ProdOrderLine.Get(ProdOrderLineRecID) then
if GraphConnectorMngt.NewEvent(Rec.Id, ProdOrderLine.Description, StrSubstNo(Body, ProdOrderLine.Description, ProdOrderLine.Quantity, ProdOrderLine."Unit of Measure Code"), ProdOrderLine."Starting Date-Time", ProdOrderLine."Ending Date-Time", false) then
DrawCalendar(ProdOrderLine."Starting Date")
else
Error(Error001, Rec.Name);
end;
}
}
}
trigger OnOpenPage()
begin
Rec.SetRange(UserGuid, UserSecurityId());
GraphConnectorMngt.GetAllCalendars();
CurrPage.Update(false);
end;
trigger OnAfterGetCurrRecord()
begin
CurrPage.ProdOrderLines.Page.SetCalendar();
DrawCalendar();
end;
var
GraphConnectorMngt: Codeunit GraphConnectorMngt;
EventsJArray: JsonArray;
local procedure DrawCalendar()
begin
DrawCalendar(Today);
end;
local procedure DrawCalendar(startingdate: date)
begin
DrawMyCalendar(startingdate);
end;
local procedure DrawMyCalendar(startingdate: date)
begin
EventsJArray := GraphConnectorMngt.GetCalendarEvents(Rec.Id);
CurrPage.calendar.SetCalendarData(EventsJArray, FORMAT(startingdate, 0, 9));
end;
}
Para que la página hija se comunique con la padre debemos añadir un par de cosillas, asà que creamos una extensión de la página Released Prod. Order Lines
pageextension 60800 RelProdOrdLines extends "Released Prod. Order Lines"
{
layout
{
addlast(content)
{
usercontrol(childparentpagecomm; childparentpagecomm)
{
ApplicationArea = All;
}
}
}
actions
{
addlast(processing)
{
action(add2calendar)
{
ApplicationArea = All;
Caption = 'Añadir como evento a calendario Outlook';
Image = CalendarWorkcenter;
Scope = Repeater;
Visible = OutlookCalendar;
trigger OnAction()
begin
if OutlookCalendar then
CurrPage.childparentpagecomm.EventCreated(Format(Rec.RecordId));
end;
}
}
}
var
OutlookCalendar: Boolean;
procedure SetCalendar()
begin
OutlookCalendar := true;
end;
}
Veamos qué tenemos aquÃ:
Al final de la página añadimos el control que nos permite comunicar la página hija con su padre. Es un control que tiene el tamaño mÃnimo y que además no deseamos mostrar visualmente ya que su trabajo consiste en "trabajar de incógnito", fuera de las cámaras 😂, si observamos en la página el control con el inspector de página, aparece con una altura de 17 pixéles; casi inapreciable. En el video a Erik le ocurre lo mismo:
Añadimos una acción que puede ser llamada desde la propia lÃnea pulsando en los 3 puntos que aparecerán en el campo producto Scope = Repeater y que sólo será visible si estamos trabajando con calendarios. Esto lo conocerá porque se llamó anteriormente a la función SetCalendar desde la página padre.
Por último envÃa al addin el RecordId que será devuelto a la página padre y esta se encargará de crear el evento, cargar de nuevo el calendario y posicionarse en la fecha del evento creado para mostrarlo rápidamente.
Veamos todo esto en funcionamiento:
Añadimos un evento al calendario fabricación, vemos como el calendario se recarga y posiciona en diciembre 2023
Observamos el resultado en Outlook
Creamos otro evento en calendario Calendario. Una vez se carga, se posiciona en enero 2025
Observamos el resultado en Outlook
Vemos como la página de lÃneas de órdenes de producción lanzadas no dispone de la acción a no ser que sea llamada desde nuestra página de eventos.
Y aquà terminamos con el ejemplo práctico del tutorial.
Los calendarios de Outlook pueden ser compartidos y simplemente con disponer de acceso a un calendario en concreto, poder conocer la disponibilidad de stock de un producto.
También tenemos la posibilidad de crear un evento de tipo reunión con una lista de convocados, ... existen muchas posibilidades y usos prácticos para lo que acabamos de ver.
También tenemos la posibilidad de crear un evento de tipo reunión con una lista de convocados, ... existen muchas posibilidades y usos prácticos para lo que acabamos de ver.
Esta última parte se merecerÃa un post propio, pero lo incluiremos en este por no alargar la serie de post del tutorial.
Vamos a hablar de...
Vamos a hablar de...
Token de refresco o de actualización
Si habéis tenido ocasión de usar el código del ejemplo, veréis que pasada una hora (o 3600 segundos) desde la primera identificación del usuario u obtención del token, nos solicita de nuevo identificarnos.
La operación es muy simple y se limita a un click, escoger la cuenta con la que loguearnos y al estar cacheada, será sólo eso ... un click. Pero ¿Es realmente necesario o podemos hacer algo para evitar ese paso?
La operación es muy simple y se limita a un click, escoger la cuenta con la que loguearnos y al estar cacheada, será sólo eso ... un click. Pero ¿Es realmente necesario o podemos hacer algo para evitar ese paso?
La respuesta es si, podemos hacer algo para evitarlo y es utilizar el token de refresco.
Vemos qué es un token de refresco o actualización: "Cuando los tokens de acceso caducan o pierden su validez, pero la aplicación sigue necesitando acceder a un recurso protegido, se enfrenta al problema de obtener un nuevo token de acceso sin obligar al usuario a conceder de nuevo el permiso. Para resolver este problema, OAuth 2.0 introdujo un artefacto llamado token de actualización. Un token de actualización permite a una aplicación obtener un nuevo token de acceso sin preguntar al usuario"
Si ponéis un punto de interrupción en la la función GetAccessToken de la codeunit GraphConnectorMngt, veremos que recibimos el token de refresco vacÃo.
Esto es porque la obtención de este token implica que incluyamos un nuevo permiso en la aplicación registrada de Azure, como vimos en el post I, en este caso debemos añadir el permiso llamado offline_access.
En registro de aplicaciones Azure, seleccionamos Permisos de API en el menú lateral izquierdo y pulsamos sobre agregar permiso
Seleccionamos Microsoft Graph y elegimos permisos delegados para posteriormente localizar el permiso comentado: offline_access
y lo agregamos a nuestra aplicación.
Ahora en Business central debemos añadirlo al ámbito de llamada en la página de configuración que creamos para configurar el conector.
Si recordamos, lo dejamos configurado con https://graph.microsoft.com/.default
Sólo debemos añadir el nombre del nuevo permiso separado por un espacio quedando de la siguiente manera: https://graph.microsoft.com/.default offline_access
Ahora sólo tenemos que volver a acceder a la página de eventos para disparar de nuevo la autenticación del usuario.
Al haber cambiado el ámbito de acceso a la aplicación, graph nos solicita de nuevo autorizar la aplicación
Ahora en Business central debemos añadirlo al ámbito de llamada en la página de configuración que creamos para configurar el conector.
Si recordamos, lo dejamos configurado con https://graph.microsoft.com/.default
Sólo debemos añadir el nombre del nuevo permiso separado por un espacio quedando de la siguiente manera: https://graph.microsoft.com/.default offline_access
Ahora sólo tenemos que volver a acceder a la página de eventos para disparar de nuevo la autenticación del usuario.
Al haber cambiado el ámbito de acceso a la aplicación, graph nos solicita de nuevo autorizar la aplicación
Nos indica que anteriormente ya se otorgaron una serie de permisos. Exacto.
Vale. Ya tengo el token de refresco ¿y ahora qué?
Ahora deberemos trabajar con él, esto es, debemos modificar la codeunit que controla todo esto.
Primero debemos añadir la función que obtiene el nuevo token a partir del token de refresco. Esta llamada devolverá tanto un nuevo token, como un nuevo token de refresco, guardamos ambos para disponer de un token utilizable y un token de refresco más reciente
Primero debemos añadir la función que obtiene el nuevo token a partir del token de refresco. Esta llamada devolverá tanto un nuevo token, como un nuevo token de refresco, guardamos ambos para disponer de un token utilizable y un token de refresco más reciente
procedure RefreshAccessToken(): Boolean
var
DotNetUriBuilder: Codeunit Uri;
Success: Boolean;
Client: HttpClient;
Content: HttpContent;
ContentHeaders: HttpHeaders;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
JAccessToken: JsonObject;
JToken: JsonToken;
OStream: OutStream;
ContentText: Text;
Property: Text;
RefreshToken: Text;
ResponseText: Text;
Secret: Text;
begin
CheckConfig();
IsolatedStorage.Get('Secret', Secret);
RefreshToken := RefreshTokenToText();
if RefreshToken = '' then
exit;
GraphSetup."Authorization Time" := CurrentDateTime();
ContentText := 'grant_type=refresh_token' +
'&refresh_token=' + DotNetUriBuilder.EscapeDataString(RefreshToken) +
'&redirect_uri=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Redirect URL") +
'&client_id=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Client ID") +
'&client_secret=' + DotNetUriBuilder.EscapeDataString(Secret);
Content.WriteFrom(ContentText);
Content.GetHeaders(ContentHeaders);
ContentHeaders.Remove('Content-Type');
ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded');
Request.Method := 'POST';
Request.SetRequestUri(GraphSetup."Token URL");
Request.Content(Content);
if Client.Send(Request, Response) then
if Response.IsSuccessStatusCode() then
if Response.Content.ReadAs(ResponseText) then
Success := JAccessToken.ReadFrom(ResponseText);
if Success then begin
foreach Property in JAccessToken.Keys() do begin
JAccessToken.Get(Property, JToken);
case Property of
'token_type',
'scope':
;
'expires_in':
GraphSetup."Expires In" := JToken.AsValue().AsInteger();
'ext_expires_in':
GraphSetup."RefTkn Expires In" := JToken.AsValue().AsInteger();
'access_token':
begin
GraphSetup."Access Token".CreateOutStream(OStream, TextEncoding::UTF8);
OStream.WriteText(JToken.AsValue().AsText());
end;
'refresh_token':
begin
GraphSetup."Refresh Token".CreateOutStream(OStream, TextEncoding::UTF8);
OStream.WriteText(JToken.AsValue().AsText());
end;
end;
end;
GraphSetup.Modify();
Commit();
end;
exit(Success);
end;
También necesitamos una función similar a la ya existente para convertir el token de refresco en texto, recordemos que está almacenado como un campo de tipo blob.
procedure RefreshTokenToText(): Text
var
IStream: InStream;
Buffer: TextBuilder;
Line: Text;
begin
GraphSetup.CalcFields("Refresh Token");
if GraphSetup."Refresh Token".HasValue then begin
GraphSetup."Refresh Token".CreateInStream(IStream, TextEncoding::UTF8);
while not IStream.EOS do begin
IStream.ReadText(Line, 1024);
Buffer.Append(Line);
end;
end;
exit(Buffer.ToText())
end;
Y por último hacemos un pequeño ajuste en nuestra función GetOrUpdateToken para que tenga en cuenta todo lo anterior resultando en esto:
procedure GetOrUpdateToken() Token: Text
var
ElapsedSecs: Integer;
AuthCode: Text;
NeedToken: Boolean;
begin
CheckConfig();
if (GraphSetup."Authorization Time" <> 0DT) then begin
ElapsedSecs := Round((CurrentDateTime() - GraphSetup."Authorization Time") / 1000, 1, '>');
if ElapsedSecs >= GraphSetup."Expires In" then
if not RefreshAccessToken() then
NeedToken := true;
end else
NeedToken := true;
if NeedToken then begin
AuthCode := GetAuthorizationCode();
GetAccessToken(AuthCode);
end;
Token := GraphSetup.AccessTokenToText();
end;
Sólo debemos añadir la lÃnea
if not RefreshAccessToken() then
Si el token ha expirado, intenta conseguir uno nuevo, y si aun asà no lo consigue, pide al usuario que se loguee de nuevo.
La duración de nuestro token de refresco será de 90 dÃas desde la última obtención de un token de acceso como podemos ver en la documentación de Microsoft.
Pero como ya hemos dicho, cada vez que caduca el token, se obtiene tanto un nuevo token como uno de refresco por lo que para que caduque un token de refresco deben pasar 90 dÃas sin que el usuario utilice la página que hemos creado para gestionar eventos o cualquier otra que utilice ese token.
Conclusiones
La moraleja o idea principal del tutorial no es disponer de un calendario y poder mover eventos, son sólo herramientas o artificios para mostrar lo que podemos llegar a hacer o el cómo aplicar una idea a un hecho. El principal propósito del tutorial es mostrar la potencia y las posibilidades que ofrece graph.
Hemos visto sólo cómo manejar eventos, pero disponemos de multitud de posibilidades:
Hemos visto sólo cómo manejar eventos, pero disponemos de multitud de posibilidades:
- Outlook
- Teams
- OneDrive
- ...
Aquà podemos ver documentación de las herramientas disponibles.
En este otro enlace podemos ver todos los recursos a los que podemos tener acceso (id desplegando el menú lateral izquierdo).
Recordad que cada recurso tiene sus permisos correspondientes, en la documentación de cada recurso nos indica qué permiso es necesario, por ejemplo, bloc de notas de OneNote
Recordad que cada recurso tiene sus permisos correspondientes, en la documentación de cada recurso nos indica qué permiso es necesario, por ejemplo, bloc de notas de OneNote
En aplicaciones registradas de portal azure debemos añadir los permisos necesarios en la aplicación en la que necesitemos estos.
Podemos utilizar lo desarrollado hasta ahora y crearnos la gestión de las entidades que necesitemos: notas, ficheros, etc.
Y con esto concluimos este tutorial que espero que os pueda servir de ayuda o que os aporte nuevas ideas. Comentad cualquier duda o sugerencia.
El código fuente de todo lo visto está disponible en github.
Publicar un comentario