Feat Widgets
This commit is contained in:
		| @@ -50,7 +50,6 @@ public class AdminController : ControllerBase | ||||
|     // Actualizamos las propiedades de la entidad con los valores del DTO. | ||||
|     agrupacion.NombreCorto = agrupacionDto.NombreCorto; | ||||
|     agrupacion.Color = agrupacionDto.Color; | ||||
|     agrupacion.LogoUrl = agrupacionDto.LogoUrl; | ||||
|  | ||||
|     // Guardamos los cambios en la base de datos. | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
| @@ -178,4 +177,32 @@ public class AdminController : ControllerBase | ||||
|  | ||||
|     return NoContent(); | ||||
|   } | ||||
|  | ||||
|   [HttpGet("logos")] | ||||
|   public async Task<IActionResult> GetLogos() | ||||
|   { | ||||
|     return Ok(await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync()); | ||||
|   } | ||||
|  | ||||
|   [HttpPut("logos")] | ||||
|   public async Task<IActionResult> UpdateLogos([FromBody] List<LogoAgrupacionCategoria> logos) | ||||
|   { | ||||
|     // Lógica de "Upsert" | ||||
|     foreach (var logo in logos) | ||||
|     { | ||||
|       var logoExistente = await _dbContext.LogosAgrupacionesCategorias | ||||
|           .FirstOrDefaultAsync(l => l.AgrupacionPoliticaId == logo.AgrupacionPoliticaId && l.CategoriaId == logo.CategoriaId); | ||||
|  | ||||
|       if (logoExistente != null) | ||||
|       { | ||||
|         logoExistente.LogoUrl = logo.LogoUrl; | ||||
|       } | ||||
|       else if (!string.IsNullOrEmpty(logo.LogoUrl)) | ||||
|       { | ||||
|         _dbContext.LogosAgrupacionesCategorias.Add(logo); | ||||
|       } | ||||
|     } | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     return NoContent(); | ||||
|   } | ||||
| } | ||||
| @@ -92,59 +92,69 @@ public class ResultadosController : ControllerBase | ||||
|     [HttpGet("provincia/{distritoId}")] | ||||
|     public async Task<IActionResult> GetResultadosProvinciales(string distritoId) | ||||
|     { | ||||
|         _logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId); | ||||
|  | ||||
|         // PASO 1: Encontrar el ámbito geográfico de la provincia. | ||||
|         var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() | ||||
|             .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); | ||||
|  | ||||
|         if (provincia == null) | ||||
|         { | ||||
|             _logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId); | ||||
|             return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" }); | ||||
|         } | ||||
|  | ||||
|         // PASO 2: Obtener el estado general del recuento para la provincia. | ||||
|         // Como las estadísticas generales (mesas, participación) son las mismas para todas las categorías, | ||||
|         // simplemente tomamos la primera que encontremos para este ámbito. | ||||
|         var estadoGeneral = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() | ||||
|             .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == provincia.Id); | ||||
|  | ||||
|         // PASO 3: Obtener el resumen de votos por agrupación para la provincia. | ||||
|         // Hacemos un JOIN manual entre ResumenesVotos y AgrupacionesPoliticas para obtener los nombres. | ||||
|         var resultados = await _dbContext.ResumenesVotos | ||||
|             .AsNoTracking() | ||||
|             .Where(r => r.AmbitoGeograficoId == provincia.Id) | ||||
|             .Join( | ||||
|                 _dbContext.AgrupacionesPoliticas.AsNoTracking(), | ||||
|                 resumen => resumen.AgrupacionPoliticaId, | ||||
|                 agrupacion => agrupacion.Id, | ||||
|                 (resumen, agrupacion) => new AgrupacionResultadoDto | ||||
|                 { | ||||
|                     Nombre = agrupacion.Nombre, | ||||
|                     Votos = resumen.Votos, | ||||
|                     Porcentaje = resumen.VotosPorcentaje | ||||
|                 }) | ||||
|             .OrderByDescending(r => r.Votos) | ||||
|         var todosLosResumenes = await _dbContext.ResumenesVotos.AsNoTracking() | ||||
|             .Include(r => r.AgrupacionPolitica) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         // PASO 4: Construir el objeto de respuesta (DTO). | ||||
|         // Si no hay datos de recuento aún, usamos valores por defecto para evitar errores en el frontend. | ||||
|         var respuestaDto = new ResumenProvincialDto | ||||
|         { | ||||
|             ProvinciaNombre = provincia.Nombre, | ||||
|             UltimaActualizacion = estadoGeneral?.FechaTotalizacion ?? DateTime.UtcNow, | ||||
|             PorcentajeEscrutado = estadoGeneral?.MesasTotalizadasPorcentaje ?? 0, | ||||
|             PorcentajeParticipacion = estadoGeneral?.ParticipacionPorcentaje ?? 0, | ||||
|             Resultados = resultados, | ||||
|             // NOTA: Los votos adicionales (nulos, en blanco) no están en la tabla de resumen provincial. | ||||
|             // Esto es una mejora pendiente en el Worker. Por ahora, devolvemos 0. | ||||
|             VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 0, Nulos = 0, Recurridos = 0 } | ||||
|         }; | ||||
|         // OBTENER TODOS LOS LOGOS EN UNA SOLA CONSULTA | ||||
|         var logosLookup = (await _dbContext.LogosAgrupacionesCategorias.AsNoTracking().ToListAsync()) | ||||
|             .ToLookup(l => $"{l.AgrupacionPoliticaId}-{l.CategoriaId}"); | ||||
|  | ||||
|         _logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count); | ||||
|         if (provincia == null) return NotFound($"No se encontró la provincia con distritoId {distritoId}"); | ||||
|  | ||||
|         return Ok(respuestaDto); | ||||
|         var estadosPorCategoria = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() | ||||
|             .Include(e => e.CategoriaElectoral) | ||||
|             .Where(e => e.AmbitoGeograficoId == provincia.Id) | ||||
|             .ToDictionaryAsync(e => e.CategoriaId); | ||||
|  | ||||
|         var resultadosPorMunicipio = await _dbContext.ResultadosVotos | ||||
|             .AsNoTracking() | ||||
|             .Include(r => r.AgrupacionPolitica) | ||||
|             .Where(r => r.AmbitoGeografico.NivelId == 30) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var resultadosAgrupados = resultadosPorMunicipio | ||||
|             .GroupBy(r => r.CategoriaId) | ||||
|             .Select(g => new | ||||
|             { | ||||
|                 CategoriaId = g.Key, | ||||
|                 TotalVotosCategoria = g.Sum(r => r.CantidadVotos), | ||||
|                 // Agrupamos por el ID de la agrupación, no por el objeto | ||||
|                 Resultados = g.GroupBy(r => r.AgrupacionPoliticaId) | ||||
|                               .Select(partidoGroup => new | ||||
|                               { | ||||
|                                   // El objeto Agrupacion lo tomamos del primer elemento del grupo | ||||
|                                   Agrupacion = partidoGroup.First().AgrupacionPolitica, | ||||
|                                   Votos = partidoGroup.Sum(r => r.CantidadVotos) | ||||
|                               }) | ||||
|                               .ToList() | ||||
|             }) | ||||
|             .Select(g => new | ||||
|             { | ||||
|                 g.CategoriaId, | ||||
|                 CategoriaNombre = estadosPorCategoria.ContainsKey(g.CategoriaId) ? estadosPorCategoria[g.CategoriaId].CategoriaElectoral.Nombre : "Desconocido", | ||||
|                 EstadoRecuento = estadosPorCategoria.GetValueOrDefault(g.CategoriaId), | ||||
|                 Resultados = g.Resultados | ||||
|                               .Select(r => new | ||||
|                               { | ||||
|                                   r.Agrupacion.Id, | ||||
|                                   r.Agrupacion.Nombre, | ||||
|                                   r.Agrupacion.NombreCorto, | ||||
|                                   r.Agrupacion.Color, | ||||
|                                   LogoUrl = logosLookup[$"{r.Agrupacion.Id}-{g.CategoriaId}"].FirstOrDefault()?.LogoUrl, | ||||
|                                   r.Votos, | ||||
|                                   VotosPorcentaje = g.TotalVotosCategoria > 0 ? ((decimal)r.Votos * 100 / g.TotalVotosCategoria) : 0 | ||||
|                               }) | ||||
|                               .OrderByDescending(r => r.Votos) | ||||
|                               .ToList() | ||||
|             }) | ||||
|             .OrderBy(c => c.CategoriaId) | ||||
|             .ToList(); | ||||
|  | ||||
|         return Ok(resultadosAgrupados); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -503,4 +513,77 @@ public class ResultadosController : ControllerBase | ||||
|  | ||||
|         return Ok(bancadasConOcupantes); | ||||
|     } | ||||
|     [HttpGet("configuracion-publica")] | ||||
|     public async Task<IActionResult> GetConfiguracionPublica() | ||||
|     { | ||||
|         // Definimos una lista de las claves de configuración que son seguras para el público. | ||||
|         // De esta manera, si en el futuro añadimos claves sensibles (como contraseñas de API, etc.), | ||||
|         // nunca se expondrán accidentalmente. | ||||
|         var clavesPublicas = new List<string> | ||||
|     { | ||||
|         "TickerResultadosCantidad", | ||||
|         "ConcejalesResultadosCantidad" | ||||
|         // "OtraClavePublica"  | ||||
|         }; | ||||
|  | ||||
|         var configuracionPublica = await _dbContext.Configuraciones | ||||
|             .AsNoTracking() | ||||
|             .Where(c => clavesPublicas.Contains(c.Clave)) | ||||
|             .ToDictionaryAsync(c => c.Clave, c => c.Valor); | ||||
|  | ||||
|         return Ok(configuracionPublica); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("concejales/{seccionId}")] | ||||
|     public async Task<IActionResult> GetResultadosConcejalesPorSeccion(string seccionId) | ||||
|     { | ||||
|         // 1. Encontrar todos los municipios (Nivel 30) que pertenecen a la sección dada (Nivel 20) | ||||
|         var municipiosDeLaSeccion = await _dbContext.AmbitosGeograficos | ||||
|             .AsNoTracking() | ||||
|             .Where(a => a.NivelId == 30 && a.SeccionProvincialId == seccionId) | ||||
|             .Select(a => a.Id) // Solo necesitamos sus IDs | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         if (!municipiosDeLaSeccion.Any()) | ||||
|         { | ||||
|             return Ok(new List<object>()); | ||||
|         } | ||||
|  | ||||
|         // 2. Obtener todos los resultados de la categoría Concejales (ID 7) para esos municipios | ||||
|         var resultadosMunicipales = await _dbContext.ResultadosVotos | ||||
|             .AsNoTracking() | ||||
|             .Include(r => r.AgrupacionPolitica) | ||||
|             .Where(r => r.CategoriaId == 7 && municipiosDeLaSeccion.Contains(r.AmbitoGeograficoId)) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var logosConcejales = await _dbContext.LogosAgrupacionesCategorias | ||||
|             .AsNoTracking() | ||||
|             .Where(l => l.CategoriaId == 7) | ||||
|             .ToDictionaryAsync(l => l.AgrupacionPoliticaId); | ||||
|  | ||||
|         // 3. Agrupar y sumar en memoria para obtener el total por partido para la sección | ||||
|         var totalVotosSeccion = resultadosMunicipales.Sum(r => r.CantidadVotos); | ||||
|  | ||||
|         var resultadosFinales = resultadosMunicipales | ||||
|             .GroupBy(r => r.AgrupacionPolitica) | ||||
|             .Select(g => new | ||||
|             { | ||||
|                 Agrupacion = g.Key, | ||||
|                 Votos = g.Sum(r => r.CantidadVotos) | ||||
|             }) | ||||
|             .OrderByDescending(r => r.Votos) | ||||
|             .Select(r => new | ||||
|             { | ||||
|                 r.Agrupacion.Id, | ||||
|                 r.Agrupacion.Nombre, | ||||
|                 r.Agrupacion.NombreCorto, | ||||
|                 r.Agrupacion.Color, | ||||
|                 LogoUrl = logosConcejales.GetValueOrDefault(r.Agrupacion.Id)?.LogoUrl, | ||||
|                 r.Votos, | ||||
|                 votosPorcentaje = totalVotosSeccion > 0 ? ((decimal)r.Votos * 100 / totalVotosSeccion) : 0 | ||||
|             }) | ||||
|             .ToList(); | ||||
|  | ||||
|         return Ok(resultadosFinales); | ||||
|     } | ||||
| } | ||||
| @@ -141,6 +141,13 @@ using (var scope = app.Services.CreateScope()) | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded default configuration 'MostrarOcupantes'."); | ||||
|     } | ||||
|     if (!context.Configuraciones.Any(c => c.Clave == "TickerResultadosCantidad")) | ||||
|     { | ||||
|         context.Configuraciones.Add(new Configuracion { Clave = "TickerResultadosCantidad", Valor = "3" }); | ||||
|         context.Configuraciones.Add(new Configuracion { Clave = "ConcejalesResultadosCantidad", Valor = "5" }); | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded default configuration 'TickerResultadosCantidad'."); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Configurar el pipeline de peticiones HTTP. | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3b8c6bf754cff6ace486ae8fe850ed4d69233280")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+608ae655bedf6c59be5fec1e14fc308871d2fd62")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","BlOQCaw/bt9UsCnDEIqO6LwzwEh4i0OxBfeIZgKDR4U=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","2x9HRdaMF3CjEHo\u002BFx\u002BfhG7CTomq/ExTkOKw2bUeHms="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","BlOQCaw/bt9UsCnDEIqO6LwzwEh4i0OxBfeIZgKDR4U=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","2x9HRdaMF3CjEHo\u002BFx\u002BfhG7CTomq/ExTkOKw2bUeHms="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["BNGxWTPjjFD1Fj56FltRDUvsBzgMlQvuqV\u002BraH2IhwQ=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","ywKm3DCyXg4YCbZAIx3JUlT8N4Irff3GswYUVDST\u002BjQ=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","khGrM2Rl22MsVh9N6\u002B7todRrMuJ6o3ljuHxZF/aubqE=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","6xYke/2SzNspypSwIgizeNUH7b\u002Bfoz3wYfKk6z1tMsw="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
		Reference in New Issue
	
	Block a user