Sign In
sep. 24
Cómo guardar contenido cuando el usuario no tiene permisos en una lista de SharePoint

He observado con mi equipo de trabajo, colegas y sharepointeros en general que una de los problemas que tenemos cuando comenzamos a usar el modelo de objetos de SharePoint es el tema de la seguridad. De hecho a mí mismo me pasaba que tenía que crear un WebPart, el cual obtendría datos de una lista o biblioteca, los renderiza, y al final presenta alguna acción que causa una actualización en dicha fuente. Por supuesto, en ambiente de desarrollo todo jala de pelos, porque usamos la cuenta del administrador. Pero tan pronto publicamos para que el cliente pruebe, ¡boom! Un SecurityException es nuestra recompensa.

   

Por supuesto, el problema en estos casos suele ser que el usuario que ha ingresado no tiene permisos para acceder a una biblioteca. El caso suele ser sencillo: darle permisos a dicho usuario, pues si no los tiene, su razón ha de tener. En otras palabras, si el usuario no tiene permiso es por la lógica de seguridad implementada para la lista o biblioteca en cuestión.

   

Sin embargo hay ocasiones en las que el cliente quiere compartir sólo cierta información. Por ejemplo, imagina una lista donde se guarda información sobre los proveedores de una empresa y la tarifa que se les paga a cada uno por unidad de servicio. Por otro lado, el jefe de compras decide que quiere exponer una forma para que los usuarios de otras áreas busquen información de los proveedores, pero no quiere que la tarifa quede expuesta. Además, si el proveedor no buscado no existe, el usuario debería poder darlo de alta (nuevamente, sin capturar la tarifa). ¿Qué podemos hacer?

   

No podemos simplemente dar permisos de lectura a los usuarios generales porque entonces podrían visualizar la página AllItems.aspx de la lista y les mostraría la tarifa. Luego, lo que tenemos que hacer es un WebPart que consulte la lista y haga la búsqueda ahí, aunque el usuario autenticado no tenga permisos de lectura en la misma. Y lo mismo ocurre con el alta de un proveedor: el usuario deberá ingresar la información sin tener permisos para hacerlo.

   

Afortunadamente, el modelo de objetos de SharePoint nos proporciona una forma de hacer el "by-pass" de seguridad. La clase SPSecurity (ubicada en el espacio de nombres Microsoft.SharePoint) contiene un método llamado RunWithElevatedPrivileges. Éste toma un sólo parámetro: un delegado de un método sin parámetros que regresa void.

   

protected override void CreateChildControls()

{

SPGridView myView = new SPGridView();

...

 

SPSecurity.RunWithElevatedPrvileges( () => {

 

// aunque el usuario autenticado no tenga

// permisos para leer o modificar la lista "Proveedores",

// al ejecutarse este delegado anónimo sí que

// los tiene.

SPList list = SPContext.Current.Lists["Proveedores"];

myView.DataSource = list.Items;

myView.DataBind();

});

 

Controls.Add(myView);

}

   

El código anterior muestra la forma básica de utilizar dicho método. Como ves, le pasamos un método anónimo. Si el usuario que carga el WebPart no tiene permisos para ingresar, éste código le garantizará el pase. Lo mismo para actualizar datos.

   

protected void OnOK(object sender, EventArgs args)

{

SPSecurity.RunWithElevatedPrivileges(() => {

 

SPList list = SPContext.Current.Lists["Proveedor"];

SPListItem item = list.Items[0];

item["Título"] = "Nuevo título";

item.Update();

});

}

   

Ahora bien, hay que aclarar un asunto. SPSecurity.RunWithElevatedPrivileges no es que de un paso libre así tal cual. Lo que hace en realidad es darle los privilegios del usuario que corre el proceso. Así, si corremos el código anterior dentro de un WebPart, éste se ejecutará bajo los permisos que tenga la cuenta asociada al Application Pool (y usualmente será el administrador de la granja). Pero por otro lado, si corriésemos un código similar en, digamos, un temporizador de SharePoint o un Workflow, la cuenta que tendrá será la asociada a estos procesos (digamos, al servicio SPTimerv4).

   

Sin embargo, por lo anterior, hay que tener cuidado ya que no siempre funcionará. Si por ejemplo, ejecutamos desde un temporizador y la cuenta asociada a ésta no tiene permiso en el recurso que queremos consultar (i.e. la lista "Proveedor" en nuestro ejemplo) tendremos un SPException como si no tuviésemos permiso. Lo mismo ocurre si ejecutamos dicho código desde una aplicación (de consola o Windows Forms): el usuario que ejecuta la aplicación será quien deba tener permisos en la lista.

   

Así que ya sabes, no siempre será garantía su empleo. Sin embargo, para WebParts, páginas de aplicación y controles, siempre será una muy buena opción.

sep. 15
Uniendo fuerzas en favor del cliente

Desde su concepción, Blendwerk TI & Media se ha preocupado por seguir las buenas prácticas de desarrollo, pues de esta forma garantizamos software de calidad para nuestros clientes. Entre las buenas prácticas y procesos que seguimos, se encuentra, desde luego, la Gestión de Requerimientos y la Administración de la Configuración.

Una de las herramientas que utilizamos es el Visual Studio, para nuestros desarrollos con plataforma Microsoft, ya sea .NET, SharePoint o código nativo, y por ello es que usar Visual Studio Team Foundation Server ha sido lo más natural, por su integración con Visual Studio, y por su magnífica adaptación a procesos Agile o CMMI. Nuestros procesos de Gestión de Requerimientos y Administración de la Configuración son apoyados por esta magnífica herramienta.

Cuando Blendwerk decidió dar soporte a aplicaciones móviles, uno de los retos interesantes fue adaptar nuestros procesos para desarrollo en otras plataformas. Cuando pensamos en plataforma Windows Phone, pensamos en Visual Studio y naturalmente Visual Studio Team Foundation Server. Pero también decidimos crear aplicaciones bajo plataforma Android, y aquí es donde comenzó el reto: ¿qué hacer?

Mientras buscábamos cómo adaptar nuestros procesos, las ideas que teníamos era la de buscar alguna herramienta que se integrara con Eclipse y funcionara para ambientes Java. En el ínterin, nos topamos con que Microsoft había decidido sacar su versión de Tema Foundation Server en la nube. ¡Resultó ser una maravilla! En efecto, tras unos días de evaluación, en la cual creamos nuestra cuenta y dimos de alta algunos proyectos, tomamos la decisión de movernos hacia este servicio a la primera oportunidad. De momento, TFS Online es una versión beta, la cual puedes acceder mediante http://tfspreview.com, pero nosotros seremos de los primeros en apuntarnos en cuanto salga la versión completa.

Y fue ahí, mientras explorábamos nuestro TFS Online, que nos llegó la solución. La experiencia con TFS ha sido maravillosa, pues su integración con el Visual Studio es magnífica. Sin embargo, TFS Online no sólo está interesado en dar servicios con productos y plataformas Microsoft. De hecho, cuando das de alta un nuevo proyecto, entre otras opciones de configuración, TFS te pregunta qué repositorio quieres usar: el propio de TFS, o bien Git. Como sabemos, Git es un repositorio online, gratuito para proyectos Open Source, y de paga para otros. De alguna maneara, podría considerarse como competencia de TFS Online, en ciertos aspectos. ¡Magnífico!

Y al seguir explorando opciones para plataformas no-Microsoft, nos topamos con una descarga de Microsoft: un plug-in nada más y nada menos ¡que para Eclipse! En efecto, este plug-in lo instalas en Eclipse y ¡maravilla! Te permite conectarte a Tema Foundation Server Online. Sólo le pasas tu dirección, y tus credenciales. ¡Y totalmente integrado!

La experiencia ha sido fabulosa. Nuestro desarrollo para Android, que estamos realizando en Java con Eclipse, ahora está totalmente integrado a Team Foundation Server Online, y lo mejor: nuestros procesos de Gestión de Requerimientos y Administración de la Configuración quedaron totalmente intactos: no tuvimos que cambiar nada.

La imagen anterior muestra nuestro Eclipse con el proyecto Android en Java, y en el fondo la sección "Code" de nuestro TFS.

Gracias a esta visión por parte de Microsoft a través de su servicio en la nube, podemos decir que hemos conjuntado lo mejor de Google, Android, con lo mejor de Microsoft, Visual Studio y Team Foundation Server, en pro de sus clientes, y por tanto en pro también de nuestros propios clientes.

sep. 14
Varias listas de indicadores en SharePoint no cargan

Hoy ha sido uno de esos días en los que caracoles, mejor hubiera sido no levantarme de la cama. Les cuento que al estar encargado de una granja de SharePoint, pues yo intento promover la herramienta, para que los usuarios de diferentes áreas del negocio me pidan nuevos Team Sites.

   

Hace poco, en este afán de promover SharePoint, estuve haciendo una serie de presentaciones para unos tíos en una de las líneas de producción. Él quería usar su Team Site como puro repositorio de información. Yo, poniéndome mi chaleco de vendedor, le preparé una presentación con todas las monerías de SharePoint. Al final hubo algo que le fascinó: el poder contar con tableros de indicadores que se actualicen en tiempo real, ya sea desde una BD o desde un reporte de Excel que ellos reciben. Esto es importante, pues esta persona es el encargado de dirigir una línea de producción: poder consultar esos indicadores siempre es crucial para su operación, y pues resultó que dicha característica de SharePoint, que Microsoft tanto presume como parte de su Business Intelligence integration. Y así, logré "vender" un proyecto de colaboración más.

   

Se hizo el diseño del Team Site, el plan de trabajo, todo bien. Y hoy era el día de terminar con los indicadores. El área de mi usuario tiene tres sub-áreas, digamos, cada una encargada de un proceso diferente en la línea de producción. Así, cada área tiene sus propios indicadores. Adicionalmente, cada sub-área realiza labores de mantenimiento a diferentes máquinas por lo que, dada la importancia que tiene (i.e. la falla de una máquina detiene la línea de producción, y no quieres que eso pase nunca), existe una lista de indicadores adicionales para mantenimiento, donde las tres áreas reportan lo relacionado. Para resumir: se crearon tres Status List, una por área, más una adicional para mantenimiento.

   

Ahora bien, pasan dos cosas: queremos tener un tablero donde se desplieguen todos los indicadores, y así poder monitorear todos los indicadores desde un solo lugar. Por otro lado, también se requiere ver los indicadores con problemas (i.e. en amarillo o en rojo). Así, para el primer tablero creo una página ASPX normalita, la edito y le añado cuatro WebParts de tipo Status List. Para el segundo, modifico la página de inicio, añado igualmente los cuatro WebParts, y abro la configuración de cada uno de estos, donde selecciono la opción de "mostrar sólo aquellos indicadores con problemas", guardo cambios y listo.

   

De todo lo anterior, creo que podrás concederme que 1) no hay hilos sueltos: es justo lo que se necesita, ni más ni menos; y que 2) estamos haciendo uso de características bastante estándares de SharePoint: WebParts, listas, etc., nada fuera de lo ordinario. ¿Verdad?

   

Pues bien, resulta que esto no funciona. No funciona. No sirve. ¡NO FUNCIONA! Ahí te va, estimado lector: cuando agregas un Status List WebPart, todo funciona de maravilla. Cuando añades otro a la misma página, el primer Webpart funciona, pero el segundo ya no: se queda mostrando la barra de progreso y nunca termina de cargar los indicadores.

   

   

¿Qué tal eh? Mi primer WebPart, Crew A, se muestra súper: todos los indicadores están en verde, así que como sólo se muestran los problemas, está bien que no cargue nada. Pero Crew B y Crew C valen queso: se quedan atrapados en ese loop infinito. Obvio fui presa de la frustración. ¡Pues qué contras! Es casi casi un caso estándar run-of-the-mill de funcionalidad estándar out-of-the-box de SharePoint. ¿Por qué cuernos habría de fallar?

   

Comencé, pues, la búsqueda por Internet. Fue muy interesante ver que había muchos casos como este. Y en todos cundía la desesperación. Alguien con el mismo problema comentó en uno de los foros de MSDN, que acababa de colgar con Microsoft Support y que le habían confirmado que esto era un bug, ya conocido por ellos, pero que no sabían cuándo podrían solucionarlo. La historia no mejoraba mucho.

   

Ante eso, comencé a buscar algún workaround, y tras varios intentos tuve la fortuna de toparme con un post en los foros de MSDN, y que tenía varias contestaciones sin respuesta… salvo la última, la final, un alma bienaventurada nos regala un script a través del cual, cuando la página se ha cargado, dispara unos postbacks de los update panels de los WebParts, forzando así el renderizado correcto. Al parecer el bug impide que se lancen los postbacks.

   

A continuación transcribo ese bendito Script:

   

function updateStatusLists() {
$("div[id*='upPanel']").each(function(index){
if(index <= 1){
var udpId = this.id;
var pbId = udpId.replace("__upPanel", "$KpiPostBackEventHandler");
__doPostBack(pbId,'UpdateId=1');        
}        
});
}

$(document).ready(function(){
$("div[id*='kpiimagediv'] img").each(function(item){
if(this.src =="http://<YOURSERVERNAME>/_layouts/images/kpiprogressbar.GIF"){
updateStatusLists();
return;
}
});
});

   

Primero tenemos una función que lo que hace es buscar por IDs los Update Panels asociados a los WebParts de indicadores, y si los encuentra, invoca la función __doPostBack, que como sabemos, provoca los postbacks de ASP.NET. Luego, declaramos una función y la añadimos al evento onLoad que se dispara cuando una página está cargada. Esta función lo que hace es buscar divs con un ID específico y obtener la imagen dentro de este div.Estos divs son, por cierto, los indicadores. Luego compara la fuente de la imagen y busca que termine en kpiprogressbar.GIF (esta imagen es la barrita de progreso que no cambia a un indicador, como puede apreciarse en la imagen anterior. Si sí es, entonces se invoca a la primera función. De esta forma el script se asegura que sólo se actualice (i.e. se hagan los postbacks) la primera vez (si no, entraríamos en otro loop infinito).

   

Este script funciona perfectamente, y lo que hice fue añadirlo a las páginas en un Content Editor WebPart. Funciona a la perfección:

   

   

Y así es como mi día fue salvado. Pero caracoles, qué mentado bug tan obvio… En fin, espero que esta entrada te ayude si te topas con este bug y no sabes inglés… :-)

sep. 14
Establecer la cuenta de un Organizational Browser

Debo de admitir que SharePoint tiene algunos controles y WebParts que son muy padres y que de hecho son muy útiles. Algunos, como los que están clasificados bajo "Social y colaboración", son muy llamativos para los usuarios. De hecho, uno que vale la pena mencionar es el Organizational Browser.

   

Si no lo conoces, te comento que este WebPart crea un navegador de jerarquías de empleados, es decir, un organigrama. Comienzas con tu cuenta de usuario y te aparece en la parte superior la jerarquía de jefes hasta llegar al nivel más alto (presumiblemente un CEO). Hacia abajo, muestra a tu equipo de trabajo: quiénes te reportan directamente. Y hacia los lados están tus colegas: aquéllos quienes reportan al mismo jefe que tú.

   

Pero no solo es mostrar una estructura determinada, sino que al dar clic en cada persona (ya sea jefe, tu equipo o tus colegas) ésta pasa al centro y se muestra su propia estructura: jefes, colegas y equipo. Así, puedes navegar por toda la estructura organizacional de la empresa (claro, siempre y cuando el Directorio Activo esté sano).

   

   

Si tu granja tiene habilitado el My Site, entonces ya cuentas con una página en tu perfil donde se muestra este control. Si no, puedes agregar el control en cualquier página mediante el WebPart llamado Organizational Browser localizado en la categoría de Social Collaboration.

   

La capacidad de contar con un organigrama cuando creamos Team Sites dentro de SharePoint es fundamental, y muchas veces ignorada. Considera un Team Site para dar soporte sobre un servicio X dentro de la organización. Tener el organigrama disponible permite ver con quién dirigirse para tratar un tema determinado, o con quién realizar una escalación. Asimismo, para los miembros del equipo, nos pone a la mano la información de contacto: teléfonos, correos, direcciones, etc.

   

Por todos los bienes que trae este control, cuenta sin embargo con una gran deficiencia. Cuando añadimos el WebPart, éste siempre va a renderizar el organigrama a partir de la cuenta que ha ingresado. Es decir, siempre mostrará tu cuenta. Este es, a mi juicio, un error de funcionalidad. Si bien es cierto que este comportamiento es deseable dentro del My Site, debería dar la capacidad de configurar la cuenta de inicio de forma sencilla. Esto es lógico: si creamos un Team Site para el área de finanzas, seguramente querremos que el organigrama se despliegue con el CFO (Chief Financial Officer, o director de finanzas) en el centro. Y esto es algo que los tíos del SharePoint Development Team no nos dejaron hacer, al menos de forma sencilla: editar

   

Al respecto, estuve investigando formas para trabajar alrededor de esto. Hay varias ideas al respecto, la mayoría sugieren crear una página ASPX y empotrar un par de controles: un ProfilePropertyLoader y un ProfileBrowser. En este hilo dentro de los foros de MSDN es lo que sugieren, con este código.

   

<%@ Page language="C#" … otras declaraciones de ASP.NET… %>

   

<asp:Content contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">

 

</asp:Content>

   

<asp:Content contentplaceholderid="PlaceHolderMain" runat="server">

   

<SPSWC:ProfilePropertyLoader id="m_objLoader" LoadFullProfileOfCurrentUser="true" runat="server" />

   

<div class="orgBrowser">

<SPSWC:ProfileBrowser ChromeType="None" runat="server" __WebPartId="{553DA676-D81E-4B0B-B8FA-58518A90C5D8}" />

</div>

</asp:Content>

   

Según esto, con invocar al ASPX pasándole como parámetro por el Query String un accountname=midominio\micuente es suficiente. De hecho, encontré varios foros y blogs donde sugieren este mismo enfoque. Sin embargo, plantea un problema: se requiere del SharePoint Designer para poder editar los ASPX de esta forma. Y en muchos casos las granjas corporativas bloquean por seguridad el uso de esta herramienta. De hecho, mi granja sufre de esto mismo.

   

Así las cosas, estuve buscando alternativas para solventar este problema. Dado que no encontré una, me decidí por investigar el control a fondo. Una rápida examinada al código HTML generado por una página que contenga este control indica que en realidad es un control de Silverlight, y que el WebPart no es más que un envoltorio que lo invoca. El objeto en cuestión se localiza en la url '/_layouts/ClientBin/hierarchychart.xap'.

   

Ahora bien, tras descargar dicho XAP, cambiarle la extensión por ZIP, y descomprimir el archivo, tuve acceso a HierarchyChart.dll. Tras abrirlo con algún desensamblador (yo usé el integrado de SharpDevelop) y explorar las clases, pude ver que en realidad el control toma el parámetro llamado initialParameters, y espera que éste sea una cadena de texto con los parámetros separados por coma. En particular, espera que el primer parámetro sea el Profile ID, es decir, el dominio + cuenta del usuario inicial que el control habrá de renderizar; y el segundo parámetro el tipo de usuario, usualmente UserType.

   

Mi reacción ante este hallazgo fue de gusto: entonces la solución sería crear un WebPart de tipo Silverlight Content, apuntar al XAP en cuestión, y en los parámetros iniciales (que sí pueden editarse desde el propio WebPart) poner mi cuenta, seguido del UserType separado por comas. Sin embargo, al probar esto no me funcionó. Tras revisar, resulta que nuestro buen WebPart Silverlight Content añade la URL a los initialParameters que configuramos, y por tanto el control de Silverlight nunca leería bien el ID de cuenta, pues estaría leyendo la URL.

   

Este hecho me frustró y obligó a cambiar de enfoque. Regresé a revisar el código generado por el Organizational Browser, con mayor detenimiento, y me topé que en realidad el WebPart no genera el enlace hacia el XAP, sino que hay un JavaScript que crea dinámicamente el control Silverlight. El pedazo de código más importante se encuentra en la función CreateHierarchyChartControl, cuyo extracto reproduzco a continuación.

   

function CreateHierarchyChartControl(parentId, profileId, type, persistControlId)

{

var initParam = profileId + ',' + type + ',' + persistControlId;

var host = document.getElementById(parentId);

   

host.setAttribute('width', '100%');

host.setAttribute('height', '100%');

   

Silverlight.createObject(

'/_layouts/ClientBin/hierarchychart.xap',

host,

'ProfileBrowserSilverlightControl',

{

top: '30',

width: '100%',

height: '100%',

version: '2.0',

isWindowless: 'true',

enableHtmlAccess: 'true'

},

{

onLoad: OnHierarchyChartLoaded

},

initParam,

null);

}

   

Como puedes ver, esta función crea el objeto XAP y le pasa los parámetros iniciales. La función toma como segundo parámetro un profileId, que es de hecho la cuenta (dominio\cuenta) inicial del Organizational Browser. Así, si de alguna forma pudiéramos invocar la función con el parámetro cambiado… pero intentar hacerlo resulto un completo dolor en el cu...erpo, puesto que hay muchas otras funciones involucradas.

   

Tras explorar el código, recordé sin embargo una característica interesante de JavaScript, más bien oscura. Toma este ejemplo.

   

<script type="text/javascript">

   

function foo() {

alert('Foo 1');

}

   

function foo() {

alert('Foo 2');

}

   

foo();

   

</script>

   

Al ejecutarse, ¿qué pasará con este script? Evidentemente tenemos definida la función un par de veces. Sin embargo, no ocurre un error. Lo que sucede aquí al ejecutarse es que se muestra una alerta con el texto Foo 2. Esto se debe a una característica de JavaScript: cuando dos funciones con la misma firma y nombre son declaradas, el compilador / intérprete deberá tomar en cuenta la definición más reciente (es decir, la última). ¡Con esto ya tenemos la solución, y ni siquiera tenemos que usar el SharePoint designer! En efecto, lo que tendríamos que hacer es crear una segunda función llamada CreateHierarchyChartControl y añadir los initParams que queremos.

   

Vamos paso a paso.

   

1.- Crear una página que permita añadir WebParts, ya sea a un WebPartZone o a un RichContent.

2.- Editar la página y añadir un Organizational Browser WebPart.

3.- Abajo del WebPart anterior (y es importantísimo que sea abajo) añadir un Content Editor WebPart (localizado en la categoría Media and Content.

4.- Seleccionar nuestro Content Editor WebPart y en las propiedades de WebPart, hacer clic en editar.

5.- Colocar el cursor en el espacio vacío del Content Editor WebPart y se habilita la cinta contextual (Ribbon) de Format Text. En el panel Markup, seleccionar la opción Edit HTML Source.

6.- En el panel HTML Source que aparece, añadir este código y guardar

   

<script type="text/javascript">

function CreateHierarchyChartControl(parentId, profileId, type, persistControlId)

{

//var initParam = profileId + ',' + type + ',' + persistControlId;

var initParam = 'midominio\\micuenta,' + type + ',' + persistControlId; // editar

var host = document.getElementById(parentId);

   

host.setAttribute('width', '100%');

host.setAttribute('height', '100%');

   

Silverlight.createObject(

'/_layouts/ClientBin/hierarchychart.xap',

host,

'ProfileBrowserSilverlightControl',

{

top: '30',

width: '100%',

height: '100%',

version: '2.0',

isWindowless: 'true',

enableHtmlAccess: 'true'

},

{

onLoad: OnHierarchyChartLoaded

},

initParam,

null);

}

</script>

   

7.- En la segunda línea de la función, donde está el comentario "// editar", hay que cambiar "midominio\\micuenta" por el dominio/cuenta del usuario que deba mostrar por vez primera el Organizational Browser. Nota que la diagonal invertida tiene que escaparse, de ahí que sean dos.

8.- Guardar la edición de la página con todos sus cambios, y regenerar la página.

   

¿Sí notaste lo que hicimos? El Content Editor WebPart añade una función que se llama CreateHierarchyChartControl. Como pusimos el WebPart después que el Organizational Browser, esta segunda función será la que el Organizational Browser ejecute en lugar de la normal, debido a que JavaScript siempre tomará como válida la última. Y en nuestra función JavaScript, armamos el initParameters a nuestro gusto, hardcodeando en este caso la cuenta (en tu caso, puedes leer desde query string o algún otro lado, si no te gusta hacer un hardcode).

   

La siguiente imagen muestra cómo quedó mi sitio.

   

   

   

Por favor, no dejes de enviar comentarios con sugerencias para monitorear si esta solución es escalable o presenta algún problema raro. En mi caso funcionó súper bien, pero ya ves que luego esto puede variar: your mileage may vary, dirían en EE.UU.

   

Eso es todo amigos.

   

   

   

   

   

   

jul. 02
¡Bienvenidos a mi blog!

Aquí compartiré mis pensamientos sobre temas que son importantes para mí. Quién sabe... tal vez hasta muestre algunas fotos, vídeos y vínculos a otras cosas interesantes.

Si les resulta interesante, comuníquense conmigo.

BLENDWERK

©2013 Todos los derechos reservados. info@blendwerk.com.mx
REDES SOCIALES
Twitter Linkedin Facebook