jueves, 16 de diciembre de 2021

Filtrar arrays dentro del código

 Bastantes veces necesito conseguir todos los valores de una columna. Por ejemplo, cuando quiero actualizar las opciones de una pregunta en un formulario con los valores que aparezcan en una hoja de cálculo. 

Este tema está ya tratado en este artículo

Lo vuelvo a traer a colación para presentar  como filtrar un array dentro del código con una sola línea

Imaginaros que la lista de valores puede cambiar de tamaño y, por lo tanto, es lógico, aparecen líneas vacías, unas veces más otras menos, debajo del último valor, tal que así:

Si no quitamos las líneas vacías, el código no va a funcionar, porque le estamos pidiendo que cree opciones con el mismo nombre (en este caso vacío)

Para quitarlas, se pueden utilizar diferentes estrategias.

Una de ellas es utilizar el método filter que tienen los arrays

Quedaría así:

var lista = ss.getRangeByName('lista_valores').getValues()

var lista_filtrada = lista.filter( function (elemento){return elemento !=""})

Simplemente con la línea superior tenemos la lista filtrada sin líneas vacías.

Lo más difícil de esta línea de código es controlar los paréntesis y las llaves :)

miércoles, 24 de noviembre de 2021

Conseguir filtrar correos que no sean de Google

 Para ello debemos aislar la segunda parte de la dirección de correo:

var correo = 'ionrey@hotmail.com'
var posicion = correo.indexOf("@")+parseInt(1)
var empresa = correo.substring(posicion)
 
if (empresa !='gmail.com'){
 
 
}
En el caso del departamento, sería: 
if (empresa !='educacion.navarra.es'){
 
 
}
 
Y los dos a la vez:
if (empresa !='gmail.com'||empresa !='educacion.navarra.es'){
 
 
}
 
 

lunes, 22 de noviembre de 2021

Sumar en javascript (números y fechas)

 Con lo fácil que es sumar y, sin embargo, parece que los creadores de Javascript se complicaron la vida por un pequeño detalle.

 

En el código para unir texto con texto o texto con variables se utiliza el signo +

¡¡Vaya!! Después de elegir este símbolo para unir nos encontramos que queremos utilizarlo para sumar y ya está pillado. 

Bueno... he aquí las alternativas:

Con números: 

Se utiliza la función parseInt. Veamos dos ejemplos:

var numero = 8

var encadenamiento = numero +3

El resultado de encadenamiento es: 83

var numero = 8

var suma=parseInt(numero) + parseInt(3)

El resultado de suma es 11


Con fechas también hay que utilizar funciones para conseguir sumar, por ejemplo, días a una fecha

Veamos los dos ejemplos:

var fecha = 21/11/2021

var encadenamiento = fecha+1

El resultado de encadenamiento es: 21/11/2021 1

var fecha = 21/11/2021

var suma = fecha.setDate(fecha.getDate()+1)

El resultado de suma es 22/11/2021


viernes, 29 de octubre de 2021

Variables al vuelo mediante código

 Como publicaba en este artículo es fácil crear un código que consiga mandar un pdf con la información enviada desde un formulario.

En dicho código existen dos partes ( creación de variables y asignación de contenido a las partes del pdf) que podemos solucionar en unas pocas líneas aunque las preguntas del formulario sean incontables.

Para aclarar de lo que hablo, os explico de antemano cual el proceso:

- El usuario manda un formulario con una serie de respuestas a diferentes preguntas

- El código crea una variable por cada pregunta del formulario

- En una plantilla tenemos dichas preguntas convertidas en variables

- El código introduce en una copia de la plantilla cada respuesta en cada variable, gracias a replaceText


Me ha tocado hacer este proceso con formularios que tenían alrededor de 50 preguntas, por lo que me he encontrado en la tesitura de declarar 50 variables (una por cada pregunta del formulario) y, por lo tanto, crear otras 50 líneas más de código con replaceText.

Me sugerieron intentar hacerlo con bucles, pero no veía como se podía declarar variables de forma indirecta: 

Y aquí aparece eval() que hace que lo que pongamos entre paréntesis lo corra como código dentro del código

Para poder entenderlo os reto a crear un bucle para crear las 50 variables.

Yo intenté algo así:

// Recogo las 50 preguntas desde la hoja de cálculo con un intervalo con nombre

var todas_las_preguntas =  SpreadsheetApp.getActiveSpreadsheet().getRangeByName('variables').getValues()

// Creo el bucle para recorrerlas e intentar crear una variable por pregunta

for (fila in todas_las_preguntas){

var mivariable&fila

}

// Y me encuentro conque no hay forma de nombrar a cada variable de una forma distinta. La declaración var mivariable&fila da error y no hay forma de que no de (que yo sepa)


Y ahí viene a salvarnos eval()

for (fila in todas_las_preguntas){

var nombre = todas_variables[fila][0]

eval("var "+nombre)

}

Y, así conseguimos crear tantas variables como preguntas tiene el formulario

En mi caso queda así:

var numero
for (fila in todas_variables){
  var nombre =todas_variables[fila][0]
numero = parseInt(fila)+parseInt(1)
eval("var "+nombre+"= pestana.getRange(ultima_fila,"+numero+").getValue()")
}

Aprovecho la declaración de la variable para asignarle el valor de la ultima fila de la pestaña de respuestas del formulario y de la columna donde está la pregunta, así consigo la respuesta de cada pregunta

Eso sí, cuando voy a la parte del código de replaceText, vuelvo a utilizar el bucle para recorrer todas las preguntas y necesito utilizar eval() otra vez

for (cada in todas_variables){

var variable = todas_variables[cada][0]

body.replaceText('<<'+todas_variables[cada][0]+'>>', eval(variable))

}


Me da la sensación de que no me he explicado muy bien. Si alguien quiere aclararse más, que se ponga en contacto conmigo y le enseño la aplicación en vivo.



martes, 22 de junio de 2021

Enviar pdf por correo desde un formulario

 Este es un ejemplo con todas las partes que necesita el código para mandar un pdf a ciertos grupos.

Falta crear los intervalos con nombre necessarios. El más importante es "ultima_fila"

Además utilizamos dentro de las fórmulas: marca_temporal y grupos_correos

También, para conseguir datos, carpeta_url y plantilla_url

Además, por cada campo que queramos incluir en el pdf, utilizamos "jarduera_actividad" y "maila_curso" que toman toda la columna de cada campo. Si necesitamos más datos en el pdf, deberíamos crear los intervalos con nombre necesarios asociados a cada dato.

function enviar_pdf() {
  // variables básicas
  var herramienta = SpreadsheetApp.getActiveSpreadsheet()
  var ultima_fila = herramienta.getRangeByName('ultima_fila').setValue 
('=filter(row(marca_temporal);marca_temporal=max(marca_temporal))').getValue()
  var pestana = herramienta.getRangeByName('ultima_fila').getSheet()

  // variables de carpeta y de plantilla
  var carpeta_id = herramienta.getRangeByName('carpeta_url')
.getValue().match(/[-\w]{25,}/)

  var plantilla_id = herramienta.getRangeByName('plantilla_url')
.getValue().match(/[-\w]{25,}/)


// Variables que se rellenan en la plantilla, de columna y variable
    var columna_jarduera_actividad = herramienta.getRangeByName('jarduera_actividad')
.getColumn()
    var jarduera_actividad = pestana.getRange(ultima_filacolumna_jarduera_actividad)
.getValue()
    
    var columna_maila_cursoherramienta.getRangeByName('maila_curso').getColumn()
    var maila_curso = pestana.getRange(ultima_filacolumna_maila_curso).getValue() 
 
            
 // Conseguimos la carpeta    y creamos la copia basada en la plantilla
  var carpeta = DriveApp.getFolderById(carpeta_id
  var nuevo_documento_id = DriveApp.getFileById(plantilla_id).
 makeCopy('nuevo'carpeta).getId()
  var nuevo_documento = DocumentApp.openById(nuevo_documento_id)
  
// Entramos y cambiamos

  var body = nuevo_documento.getBody()
  
  body.replaceText('<<JARDUERA/ACTIVIDAD>>'jarduera_actividad)
 

    
  
// Cerramos
nuevo_documento.saveAndClose()


// Variables para el correo
var arduraduna = herramienta.getRangeByName('arduraduna').getValue()
var ikasketaburuaherramienta.getRangeByName('ikasketaburua').getValue()

var tituloherramienta.getRangeByName('titulo').getValue()
var mensajeherramienta.getRangeByName('mezua').getValue()
var correo

// Convertimos en pdf
var pdf =DriveApp.createFile(nuevo_documento.getAs('application/pdf')
.setName(titulo+' -- '+data_fecha+'.pdf')) 


// Pillamos todos los grupos
var array_grupos = maila_curso.split(",")
Logger.log(array_grupos)


// Mandamos a cada correo
for (grupo in array_grupos){
              correo = pestana.getRange('cada_correo')
.setValue('=iferror(vlookup("'+array_grupos[grupo].trim()+'";grupos_correos;2;false))')
.getValue()
             var opciones ={attachmentspdf ,bcc:arduraduna  ,cc:ikasketaburua}
       MailApp.sendEmail(correotitulomensajeopciones)
                           }

// Borramos el documento
  pdf.setTrashed(true)
//  DriveApp.getFileById(nuevo_documento_id ).setTrashed(true)
}

Gráficos desde el formulario: Ver solamente los de este curso escolar

Buena noticia: Los formularios crean automáticamente una serie de gráficos con las respuestas enviadas, lo que, supuestamente, nos ahorra mucho trabajo.

Mala noticia: En dichos gráficos se reflejan TODAS las respuestas enviadas, sean de prueba o de cualquier fecha, por lo que esta opción de gráficos, con el tiempo, suele perder mucho.

Situación: Hay aplicaciones, por ejemplo las de satisfacción, que se envían todos los cursos escolares y, que por lo tanto pierden enseguida la posibilidad de utilizar dichos gráficos, ya que las respuestas son de diferentes cursos escolares.

Objetivo: Conseguir los gráficos, al menos, del último curso escolar.

Ventaja de los formularios: En los formularios se pueden borrar las respuestas enviadas sin que se borren a la vez de la aplicación (hoja de cálculo) asociada.

Truco: Nos inventamos un código para borrar las respuestas del formulario más antiguas de una fecha dada y, así, conseguimos que los gráficos puedan ser reflejo del último curso escolar. Perdemos los gráficos anteriores, pero no los datos (siguen en la hoja de cálculo)


El código para conseguir esto es el siguiente:

 

function borrar_segun_fecha() {
  var herramienta = SpreadsheetApp.getActiveSpreadsheet()
  var formulario_url =herramienta.getFormUrl()
  var formulario = FormApp.openByUrl(formulario_url)
  var respuestas = formulario.getResponses()
  // En la hoja de cálculo creamos un intervalo con nombre
  // para que metamos una fecha concreta,  a partir de la cual
  // borramos las respuestas anteriores a ella.
      const fecha = herramienta.getRangeByName('fecha_para_borrar').getValue()
  for (cada in respuestas){
                   if  (respuestas[cada].getTimestamp()< fecha){
                          var id = respuestas[cada].getId()
                            formulario.deleteResponse(id)
                                                               }
    
                         }
}

 

En la hoja de cálculo, tenemos, como hemos comentando, un intervalo para meter

 la fecha y una imagen de una goma de borrar que activa el código superior.

martes, 15 de junio de 2021

Incluir nuevos alumnos de Educa

 Tengo una lista de alumnos que mantengo de curso escolar en curso escolar, pero necesito añadir los alumnos nuevos, utilizando la exportación desde Educa.

Podría, simplemente, añadir los alumnos de primero. No me vale, ya que los repetidores aparecerían dos veces. Además, siempre hay incorporaciones nuevas en otros cursos, sobretodo en primero de bachiller.

La solución que he encontrado es hacer una exportación total de Educa y, con el código conseguir añadir a una lista estable, que se mantiene de otros años, sólamente los que no están en ella.

Este es el código:

function nuevos_alumnos() {
  // Hay que buscar y si no aparece añadir
  var herramienta = SpreadsheetApp.getActiveSpreadsheet()
  var nueva_lista = herramienta.getRangeByName('nueva_lista').getValues()
  var antigua_lista = herramienta.getRangeByName('antigua_lista').getValues()
  var pestana_lista = herramienta.getRangeByName('nuevos').getSheet()
  var salto = false

  for (nuevo in nueva_lista){
             salto = false
     if (nueva_lista[nuevo][4] !=""){
      
                           for (viejo in antigua_lista){
if (antigua_lista[viejo][0]== nueva_lista[nuevo][4])
{salto=true}
}
                                               
                           if (salto == false){
  pestana_lista.appendRow([nueva_lista[nuevo][3], 
nueva_lista[nuevo][4],nueva_lista[nuevo][1],nueva_lista[nuevo][0]])}                                                                     
                                              }
                             }

}

jueves, 10 de junio de 2021

Evitar que se manden notificaciones de correo

 Mi caso es una aplicación que utilizo para cambiar la propiedad de una carpeta, y de todos los archivos y subcarpetas que incluye, de forma automática. 

Gracias al esquema que comenté en el artículo "Recorrer carpetas y documentos en Drive" es bastante sencillo de hacer: Simplemente debes añadir por cada archivo o subcarpeta la línea: archivo.setOwner(email_del_nuevo_propietario)

Pero tiene una consecuencia importante, muy molesta: por cada archivo o subcarpeta manda un correo de cambio de propiedad. En según que casos, pueden llegar miles de correos a la vez a la cuenta del nuevo propietario.

Por lo que hay que buscar en el código algún método que "desmarque" las notificaciones. Resulta que no lo hay en la API driveApp y tenemos que recurrir a los servicios avanzados de Google para activar la API llamada drive. Una vez activada el código cambiaría de esta forma:

//archivo.setOwner(nuevo)
var archivo_id = archivo.getId() 
Drive.Permissions.insert({
'role''owner',
  'type':'user',
  'value':nuevo},
archivo_id,
{'sendNotificationEmails':'false'})

En vez de una sola línea, necesitamos seis. Dentro de ellas, debemos definir en role la palabra owner (para propietario), en el value metemos el correo del nuevo propietario y, para acabar, metemos el id del archivo o subcarpeta en  la variable archivo_id. Para acabar, aún siendo lo más importante,  definimos el valor 'sendNotificationEmails' como 'false'


Más información en Drive API de Google

Hay que tener en cuenta que editor no es editor sino writer, pero lector si es reader

lunes, 7 de junio de 2021

Conseguir la lista de intervalos con nombre

 Esta función que os presento sirve para crear una lista de los enlaces a todos los intervalos con nombre que hemos creado en una hoja de cálculo

function lista_intervalos_con_nombre(){
  // Conseguimos todos los intervalos con nombre de la hoja de cálculo
var herramienta = SpreadsheetApp.getActiveSpreadsheet()
var rangos = herramienta.getNamedRanges()
// Apuntamos a la pestaña donde vamos a crear la lista, donde previamente hemos creado
// el intervalo con nombre -intervalos- 
var pestana_intervalos = herramienta.getRangeByName('intervalos').getSheet()
 
        // Limpiamos la pestaña
      pestana_intervalos.appendRow(['xxxx'])
      var ultima_fila = pestana_intervalos.getLastRow()
      pestana_intervalos.deleteRows(2ultima_fila-1)
      
      // Metemos fórmula para ordenarlos
      pestana_intervalos.getRange('e2').setValue('=sort(A2:d;2;true;4;true)')
      
var rango
var enlace

for (cada_uno in rangos){
                     rango = rangos[cada_uno].getRange()
         enlace = 'https://docs.google.com/spreadsheets/d/'+herramienta.getId()+ 
'/edit#gid='+rango.getSheet().getSheetId()+ 
'&range='+rango.getA1Notation()

  //rango.setBackground('#eec9e3')
                     pestana_intervalos.appendRow([ 
'=hyperlink("'+enlace+'";"'+rangos[cada_uno].getName()+'")',
  rango.getSheet().getName(), 
rango.getA1Notation(),
  rango.getColumn()
])
                      }
}

jueves, 3 de junio de 2021

Limpiar la denonimación de los intervalos con nombre importados de otra hoja de cálculo

Al hilo de los intervalos con nombre , cuando se copia una pestaña en otra hoja
de cálculo, resulta que los intervalos con nombre de dicha pestaña también se 
copian pero el nombre de dicho intervalo cambia, Google le pone un prenombre 
con la denominación de la pestaña, tal que así: mi_rango pasa a llamarse 
mipestaña!mi_rango.
A parte de quedar feo, da problemas en el código, ya que, por ejemplo, no puedes
ir a la pestaña donde está dicho rango.
 
Por lo tanto, he creado una función que se ocupa de limpiar los nombres de los
intervalos, quitándoles dicho prenombre y volviendo al nombre original.
 
function limpiar_intervalos_con_nombre (){
  // Conseguimos todos los intervalos con nombre de la hoja de cálculo
var herramienta = SpreadsheetApp.getActiveSpreadsheet()
var rangos = herramienta.getNamedRanges()

// Preparamos las variables necesarias: punto desde donde borrar el nombre, 
nuevo nombre con la segunda parte, rango a aplicar que es el mismo que tenía
var partir_por
var nuevo_nombre
var cual_es_el_rango

// Hacemos un bucle por intervalos con nombre
for (cada in rangos){
  // Comprobamos que tenga en el nombre un ! para pillar los intervalos importados
if (rangos[cada].getName().indexOf('!') !=-1){
  // Conseguimos el punto de corte, que es uno más siempre
 partir_por = rangos[cada].getName().indexOf('!')+parseInt(1)
 // Partimos el nombre viejo desde ese punto en adelante
  nuevo_nombre = rangos[cada].getName().substring(partir_por)
 // Guardamos el rango que tenía el intervalo con nombre
  cual_es_el_rango = rangos[cada].getRange()
//Borramos el intervalo con nombre
rangos[cada].remove()
// Lo volvemos a crear con nuevo nombre y el mismo rango
herramienta.setNamedRange(nuevo_nombre,cual_es_el_rango)
}
}

}

Rangos con nombre (intervalos con nombre) "GetRangeByName"

 Como ya explique en un artículo antiguo (Referencias a pestañas y/o celdas en código) es muy interesante utilizar los intervalos con nombre creándolos en la hoja de cálculo y referenciándolos en el código con .getRangeByName()

Como comenté en dicho artículo, la ventaja más evidente es que, aunque el usuario cambie el nombre de la pestaña o el lugar de una celda, el código sigue apuntando a donde le hemos dicho que apunte.

Para dejar más claro el problema: 

Si utilizo en el código 

var pestana = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('configuración')

corro el peligro que deje de funcionar si alguien cambia el nombre de pestaña, un peligro que se escapa de nuestras manos

Peor aún, si utilizo en el código esta referencia:

var resultado = pestana.getRange('B2').getValue()

Dejará de funcionar en cuanto añadamos alguna columna o fila antes de la B o de la fila 2

Para evitar todo esto, la solución es crear intervalos con nombre en la hoja de cálculo  y referenciarlos tal que así:

var pestana= SpreadsheetApp.getActiveSpreadsheet().getRangeByName('configuración')

var resultado = pestana.getRangeByName('el_de_la_celda_B2')

 

Pero... Nos hemos encontrado con una buena noticia y una mala en relación a estos rangos con nombre:

  • La buena: Cuando copiamos una pestaña en otra hoja de cálculo ("Copiar en" de la pestaña), los intervalos con nombre también se copian.. pero con un nombre mezcla de la pestaña y el nombre antiguo.
  • La mala: El código de referencia a estos rangos con nombre a veces no funciona, no encuentra dicho intervalo con nombre. La cuestión es que a veces funciona y a veces no. Y la única solución que he encontrado es borrar dicho intervalo y volverlo a crear.

jueves, 27 de mayo de 2021

Saltar errores y continuar con el código

Estoy haciendo código para conseguir quitar todos los permisos a un correo concreto en todos los archivos y subcarpetas de una carpeta dada. 

Para conseguir quitar permisos el código necesita una dirección de correo  tal que así: 

archivo.removeEditor(email)

Metiendo esta orden en un bucle que recorra todos los archivos nos permite conseguir nuestro objetivo.

Pero, a veces, resulta que no encuentra dicho correo y el código da error y NO SIGUE ADELANTE, se para.

Un truco para saltar por encima es utilizar este tratamiento de errores de Javascript:

try{archivo.removeEditor(email)} 

catch(e){}


Gracias a envolver nuestro código en try{} y añadir después catch(e){} logramos que el error no nos pare.

Entre las llaves de la segunda parte catch(e){}, podemos poner código que solamente corra cuando se da el error.



jueves, 4 de marzo de 2021

Recorrer carpetas y documentos en DRIVE

Este artículo tiene como objetivo mostrar el código básico para trabajar con Drive, es decir, para recorrer todos los archivos y subcarpetas de drive.

 

Ultimamente he estado trabajando con los permisos que damos a carpetas y documentos drive. 

Nuestra recomendaciones son:

  • No utilizar permisos en carpetas (la herencia es difícil de controlar)
  • No utilizar permisos globales, salvo que haya una razón importante
  • Utilizar google groups para gestionar los permisos

 

Debido a estas recomendaciones, ha habido centros que se han encontrado con la tarea de cambiar un montón de permisos anteriores, como por ejemplo: infinidad de carpetas, subcarpetas y documentos que tienen permiso de "cualquiera de educación con el enlace". Otro ejemplo es tener infinidad de permisos en documentos obsoletos a los que queremos hacer desaparecer, sin tener que meter horas yendo documento por documento y persona por persona quitando estos permisos.

Gracias al código, todas estas necesidades se pueden hacer de forma automática, simplemente dando la dirección de una carpeta y el código se ocupa de limpiar de permisos todo lo que contenga dicha carpeta.

He hecho diferentes funciones, según el objetivo a conseguir, pero en todas se ha repetido el proceso básico de ir recorriendo todos los documentos y subcarpetas incluidas en una carpeta dada. Por eso, me parece interesante hacer una especie de código básico que controle este proceso y, que con diferentes variantes, nos permita hacer infinidad de cosas con drive.

 

Lo vamos a hacer en tres funciones entrelazadas: función inicial, función para archivos y función para subcarpetas. La unión es la siguiente:

Empezamos con la función inicial y de ahí llamamos a la función de los archivos. Desde archivos llamamos a la función de subcarpetas. En esta última, en la función subcarpetas volvemos a llamar a la función archivos (que vuelve a llamar a subcarpetas, que vuelve a llamar a archivos..... hasta que no hay más)

Este recorrido es fundamental para poder recorrer todos los archivos y subcarpetas que haya dentro de una carpeta.

 

Dentro de cada función estas son las líneas básicas:

 

Inicio:

 // Conseguimos la carpeta principal

 var carpeta_global =DriveApp.getFolderById(id_general)

// Llamamos a la función archivos

archivos(carpeta_global)

Archivos:

 // Conseguimos todos los archivos de la carpeta

  var archivos_global = carpeta_global.getFiles() 

// Los recorremos todos con un bucle

  while (archivos_global.hasNext()){

// Apuntamos a cada archivo (dentro del bucle)

var archivo= archivos_global.next();

// Con cada archivo podemos hacer lo que necesitemos, por ejemplo, 

archivo. setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.NONE) //quitamos todos los permisos globales

}

// Una vez terminado con todos los archivos de esta carpeta llamamos a la función  de subcarpetas, fuera del bucle

   subcarpeta(carpeta_global)

Subcarpetas: 

// Conseguimos todas las subcarpetas que haya dentro

  var sub_carpetas = carpeta_global.getFolders()
// Hacemos el bucle para recorrerlas
     while (sub_carpetas.hasNext()) {

// Apuntamos a cada una de ellas
       var subcarpeta = sub_carpetas.next()

// Con cada subcarpeta podemos hacer muchas cosas, por ejemplo,

subcarpeta.removeEditor(correo) // Eliminamos un editor concreto

 // y dentro del bucle llamamos a la función archivos, PERO, pasando cada subcarpeta, en vez de la carpeta_global

archivos(subcarpeta)

Crear PDF horizontal

El código que vamos a mostrar aquí no ha sido creado por nosotros, sino que lo hemos encontrado en Internet. Objetivo: Publicar una pesta...