diff --git a/CityInfo.API/CityInfo.API.csproj b/CityInfo.API/CityInfo.API.csproj index 39f3cde..d952264 100644 --- a/CityInfo.API/CityInfo.API.csproj +++ b/CityInfo.API/CityInfo.API.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -6,9 +6,33 @@ enable + + False + + - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + Always + diff --git a/CityInfo.API/CityInfo.API.http b/CityInfo.API/CityInfo.API.http deleted file mode 100644 index ca0985d..0000000 --- a/CityInfo.API/CityInfo.API.http +++ /dev/null @@ -1,6 +0,0 @@ -@CityInfo.API_HostAddress = http://localhost:5244 - -GET {{CityInfo.API_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/CityInfo.API/CityInfo.db b/CityInfo.API/CityInfo.db new file mode 100644 index 0000000..383a214 Binary files /dev/null and b/CityInfo.API/CityInfo.db differ diff --git a/CityInfo.API/Controllers/AuthenticationController.cs b/CityInfo.API/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..a705474 --- /dev/null +++ b/CityInfo.API/Controllers/AuthenticationController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace CityInfo.API.Controllers +{ + [Route("api/authentication")] + [ApiController] + public class AuthenticationController : ControllerBase + { + private readonly IConfiguration _configuration; + + public class AuthenticationRequestBody + { + public string? UserName { get; set; } + public string? Password { get; set; } + + } + internal class CityInfoUser + { + public int UserId { get; set; } + public string UserName { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } + public CityInfoUser( + int userId, string userName, string firstName, string lastName, string city) + { + UserId = userId; + UserName = userName; + FirstName = firstName; + LastName = lastName; + City = city; + } + } + + public AuthenticationController(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + [HttpPost("authenticate")] + public ActionResult Authenticate(AuthenticationRequestBody authenticationRequestBody) + { + var user = ValidateUserCredentials(authenticationRequestBody.UserName, authenticationRequestBody.Password); + if (user == null) + { + return Unauthorized(); + } + + var securityKey = new SymmetricSecurityKey(Convert.FromBase64String(_configuration["Authentication:SecretForKey"])); + var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var claimsForToken = new List + { + new Claim("sub", user.UserId.ToString()), + new Claim("given_name", user.FirstName), + new Claim("family_name", user.LastName), + new Claim("city", user.City) + }; + + var jwtSecurityToken = new JwtSecurityToken( + _configuration["Authentication:Issuer"], + _configuration["Authentication:Audience"], + claimsForToken, + DateTime.UtcNow, + DateTime.UtcNow.AddHours(1), + signingCredentials); + + var tokenToReturn = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return Ok(tokenToReturn); + } + + private CityInfoUser ValidateUserCredentials(string? userName, string? password) + { + return new CityInfoUser( + 1, + userName ?? "", + "Nathan", + "Pire", + "Charleroi"); + } + } + +} diff --git a/CityInfo.API/Controllers/CitiesController.cs b/CityInfo.API/Controllers/CitiesController.cs new file mode 100644 index 0000000..65bdcdc --- /dev/null +++ b/CityInfo.API/Controllers/CitiesController.cs @@ -0,0 +1,63 @@ +using AutoMapper; +using CityInfo.API.Models; +using CityInfo.API.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; + +namespace CityInfo.API.Controllers +{ + [ApiController] + [Authorize] + [Route("api/cities")] + public class CitiesController: ControllerBase + { + private readonly ICityInfoRepository _cityInfoRepository; + private readonly IMapper _mapper; + const int maximumPageSize = 20; + + public CitiesController( + ICityInfoRepository cityInfoRepository, + IMapper mapper) + { + _cityInfoRepository = cityInfoRepository ?? throw new ArgumentNullException(nameof(cityInfoRepository)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + [HttpGet()] + public async Task>> GetCities( + string? name, string? searchQuery, int pageNumber = 1, int pageSize = 10) + { + if(pageSize > maximumPageSize) + { + pageSize = maximumPageSize; + } + + var (cityEntities, paginationMetaData) = await _cityInfoRepository + .GetCitiesAsync(name, searchQuery,pageNumber, pageSize); + + Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(paginationMetaData)); + + return Ok(_mapper.Map>(cityEntities)); + } + + [HttpGet("{id}")] + public async Task GetCity(int id, bool includePointsOfInterest = false) + { + var city = await _cityInfoRepository.GetCityAsync(id, includePointsOfInterest); + + if (city == null) + { + return NotFound(); + } + + if (includePointsOfInterest) + { + return Ok(_mapper.Map(city)); + } + + return Ok(_mapper.Map(city)); + } + + } +} diff --git a/CityInfo.API/Controllers/FilesController.cs b/CityInfo.API/Controllers/FilesController.cs new file mode 100644 index 0000000..7ab1423 --- /dev/null +++ b/CityInfo.API/Controllers/FilesController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; + +namespace CityInfo.API.Controllers +{ + [Route("api/files")] + [Authorize] + [ApiController] + public class FilesController : ControllerBase + { + private readonly FileExtensionContentTypeProvider _fileExtensionContentTypeProvide; + public FilesController( + FileExtensionContentTypeProvider fileExtensionContentTypeProvider) + { + _fileExtensionContentTypeProvide = fileExtensionContentTypeProvider + ?? throw new System.ArgumentNullException( + nameof(FileExtensionContentTypeProvider)); + } + [HttpGet("{fileId}")] + public ActionResult GetFile(string fileId) + { + var pathToFile = "email_template.pdf"; + if (!System.IO.File.Exists(pathToFile)) + { + return NotFound(); + } + if (!_fileExtensionContentTypeProvide + .TryGetContentType(pathToFile, out var contentType)) + { + contentType = "application/octet-stream"; + } + var bytes = System.IO.File.ReadAllBytes(pathToFile); + return File(bytes, contentType , Path.GetFileName(pathToFile)); + } + [HttpPost] + public async Task CreateFile(IFormFile file) + { + if(file.Length == 0 || file.Length > 20971520 || file.ContentType != "application/pdf") + { + return BadRequest("No file or an invalid one has been inputted."); + } + var path = Path.Combine( + Directory.GetCurrentDirectory(), + $"uploaded_file_{Guid.NewGuid()}.pdf"); + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + return Ok("Your file has been uploaded successfully."); + } + } +} diff --git a/CityInfo.API/Controllers/PointsOfInterestController.cs b/CityInfo.API/Controllers/PointsOfInterestController.cs new file mode 100644 index 0000000..d957346 --- /dev/null +++ b/CityInfo.API/Controllers/PointsOfInterestController.cs @@ -0,0 +1,183 @@ +using AutoMapper; +using CityInfo.API.Entities; +using CityInfo.API.Models; +using CityInfo.API.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; + +namespace CityInfo.API.Controllers +{ + [Route("api/cities/{cityId}/pointsofinterest/", Name = "GetPointsOfInterest")] + [Authorize(Policy = "MustBeFromCharleroi")] + [ApiController] + public class PointsOfInterestController : ControllerBase + { + private readonly ILogger _logger; + private readonly IMailService _mailService; + private readonly ICityInfoRepository _cityInfoRepository; + private readonly IMapper _mapper; + + public PointsOfInterestController( + ILogger logger, + IMailService mailService, + ICityInfoRepository cityInfoRepository, + IMapper mapper) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _mailService = mailService ?? throw new ArgumentNullException(nameof(mailService)); + _cityInfoRepository = cityInfoRepository ?? throw new ArgumentNullException(nameof(cityInfoRepository)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + [HttpGet] + public async Task>> GetPointsOfInterest(int cityId) + { + //throw new Exception("pipi"); + try + { + //throw new Exception("caca prout"); + var cityName = User.Claims.FirstOrDefault(claim => claim.Type == "city")?.Value; + if (!await _cityInfoRepository.CityNameMatchesCityId(cityName, cityId)) + { + return Forbid(); + } + + if(!await _cityInfoRepository.CityExistAsync(cityId)) + { + _logger.LogInformation($"City with Id {cityId} wasn't found when accessing points of interest."); + return NotFound(); + } + var pointsOfInterest = await _cityInfoRepository.GetPointsOfinterestForCityAsync(cityId); + return Ok(_mapper.Map>(pointsOfInterest)); + } + catch (Exception ex) + { + _logger.LogCritical($"Exception while getting points of interest for city with id {cityId}", ex); + return StatusCode(500, "A problem happened while handling your request."); + } + + } + [HttpGet("{pointOfInterestId}", Name = "GetPointOfInterest")] + public async Task> GetPointOfInterest(int cityId, int pointOfInterestId) + { + if (!await _cityInfoRepository.CityExistAsync(cityId)) + { + _logger.LogInformation($"City with Id {cityId} wasn't found when accessing point of interest."); + return NotFound(); + } + + var pointOfInterest = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId); + + if (pointOfInterest == null) + { + return NotFound(); + } + return Ok(_mapper.Map(pointOfInterest)); + } + + [HttpPost(Name = "CreatePointOfInterest")] + public async Task> CreatePointOfInterest( + int cityId, + PointOfInterestForCreationDto pointOfInterest) + { + if (!await _cityInfoRepository.CityExistAsync(cityId)) + { + return NotFound(); + } + + var finalPointOfInterest = _mapper.Map(pointOfInterest); + await _cityInfoRepository.CreatePointOfInterestForCityAsync(cityId, finalPointOfInterest); + + await _cityInfoRepository.SaveChangesAsync(); + var createdPointOfInterestToReturn = _mapper.Map(finalPointOfInterest); + + return CreatedAtRoute("GetPointOfInterest", + new + { + cityId = cityId, + pointOfInterestId = createdPointOfInterestToReturn.Id + }, + createdPointOfInterestToReturn); + } + [HttpPut("{pointOfInterestId}", Name = "UpdatePointOfInterest")] + public async Task UpdatePointOfInterest( + int cityId, + int pointOfInterestId, + PointOfInterestForUpdateDto pointOfInterest) + { + if (!await _cityInfoRepository.CityExistAsync(cityId)) + { + return NotFound(); + } + var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId); + + if (pointofInterestEntity == null) + { + return NotFound(); + } + _mapper.Map(pointOfInterest, pointofInterestEntity); + await _cityInfoRepository.SaveChangesAsync(); + return NoContent(); + } + + [HttpPatch("{pointOfInterestId}", Name = "PartiallyUpdatePointOfInterest")] + public async Task PartiallyUpdatePointOfInterest( + int cityId, + int pointOfInterestId, + JsonPatchDocument patchDocument) + { + if (!await _cityInfoRepository.CityExistAsync(cityId)) + { + return NotFound(); + } + + var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId); + + if (pointofInterestEntity == null) + { + return NotFound(); + } + + var pointOfInterestToPatch = _mapper.Map(pointofInterestEntity); + + patchDocument.ApplyTo(pointOfInterestToPatch, ModelState); + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + if (!TryValidateModel(pointOfInterestToPatch)) + { + return BadRequest(ModelState); + } + _mapper.Map(pointOfInterestToPatch, pointofInterestEntity); + await _cityInfoRepository.SaveChangesAsync(); + + return NoContent(); + } + [HttpDelete("{pointOfInterestId}")] + public async Task DeletePointOfInterest( + int cityId, + int pointOfInterestId) + { + if (!await _cityInfoRepository.CityExistAsync(cityId)) + { + return NotFound(); + } + + var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId); + + if (pointofInterestEntity == null) + { + return NotFound(); + } + + _cityInfoRepository.DeletePointOfInterest(pointofInterestEntity); + await _cityInfoRepository.SaveChangesAsync(); + _mailService.Send("Point of interest deleted", + $"Point of Interest {pointofInterestEntity.Name} with Id {pointofInterestEntity.Id} was deleted."); + + return NoContent(); + } + } +} diff --git a/CityInfo.API/Controllers/WeatherForecastController.cs b/CityInfo.API/Controllers/WeatherForecastController.cs deleted file mode 100644 index 19d9710..0000000 --- a/CityInfo.API/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace CityInfo.API.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = - [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - ]; - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/CityInfo.API/DbContexts/CityInfoContext.cs b/CityInfo.API/DbContexts/CityInfoContext.cs new file mode 100644 index 0000000..7a2fc11 --- /dev/null +++ b/CityInfo.API/DbContexts/CityInfoContext.cs @@ -0,0 +1,82 @@ +using CityInfo.API.Entities; +using Microsoft.EntityFrameworkCore; + +namespace CityInfo.API.DbContexts +{ + public class CityInfoContext: DbContext + { + public DbSet Cities { get; set; } + public DbSet PointsOfInterest { get; set; } + + //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + //{ + // optionsBuilder.UseSqlite("connectionstring"); + // base.OnConfiguring(optionsBuilder); + //} + public CityInfoContext(DbContextOptions options): base(options) + {} + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new City("New York City") + { + Id = 1, + Description = "The one with that big park." + }, + new City("Antwerp") + { + Id = 2, + Description = "The one with the cathedral that was never really finished." + }, + new City("Paris") + { + Id = 3, + Description = "The one with that big tower." + }); + + modelBuilder.Entity() + .HasData( + new PointOfInterest("Central Park") + { + Id = 1, + CityId = 1, + Description = "The most visited urban park in the United States." + + }, + new PointOfInterest("Empire State Building") + { + Id = 2, + CityId = 1, + Description = "A 102-story skyscraper located in Midtown Manhattan." + }, + new PointOfInterest("Cathedral") + { + Id = 3, + CityId = 2, + Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans." + }, + new PointOfInterest("Antwerp Central Station") + { + Id = 4, + CityId = 2, + Description = "The the finest example of railway architecture in Belgium." + }, + new PointOfInterest("Eiffel Tower") + { + Id = 5, + CityId = 3, + Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel." + }, + new PointOfInterest("The Louvre") + { + Id = 6, + CityId = 3, + Description = "The world's largest museum." + } + ); + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/CityInfo.API/Entities/City.cs b/CityInfo.API/Entities/City.cs new file mode 100644 index 0000000..3057795 --- /dev/null +++ b/CityInfo.API/Entities/City.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CityInfo.API.Entities +{ + public class City + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + [Required] + [MaxLength(50)] + public string Name { get; set; } + [MaxLength(200)] + public string? Description { get; set; } + public ICollection PointsOfInterest { get; set; } = new List(); + + public City(string name) + { + Name = name; + } + + } +} diff --git a/CityInfo.API/Entities/PointOfInterest.cs b/CityInfo.API/Entities/PointOfInterest.cs new file mode 100644 index 0000000..13a6759 --- /dev/null +++ b/CityInfo.API/Entities/PointOfInterest.cs @@ -0,0 +1,25 @@ + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CityInfo.API.Entities +{ + public class PointOfInterest + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + [Required] + [MaxLength(50)] + public string Name { get; set; } + [MaxLength(200)] + public string? Description { get; set; } + [ForeignKey("CityId")] + public City? City { get; set; } + public int CityId { get; set; } + public PointOfInterest(string name) + { + Name = name; + } + } +} diff --git a/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.Designer.cs b/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.Designer.cs new file mode 100644 index 0000000..d609587 --- /dev/null +++ b/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.Designer.cs @@ -0,0 +1,81 @@ +// +using CityInfo.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + [DbContext(typeof(CityInfoContext))] + [Migration("20260310223100_CityInfoDBInitialMigration")] + partial class CityInfoDBInitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CityId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("PointsOfInterest"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.HasOne("CityInfo.API.Entities.City", "City") + .WithMany("PointsOfInterest") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Navigation("PointsOfInterest"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.cs b/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.cs new file mode 100644 index 0000000..9f48374 --- /dev/null +++ b/CityInfo.API/Migrations/20260310223100_CityInfoDBInitialMigration.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + /// + public partial class CityInfoDBInitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Cities", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Cities", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PointsOfInterest", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CityId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PointsOfInterest", x => x.Id); + table.ForeignKey( + name: "FK_PointsOfInterest_Cities_CityId", + column: x => x.CityId, + principalTable: "Cities", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PointsOfInterest_CityId", + table: "PointsOfInterest", + column: "CityId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PointsOfInterest"); + + migrationBuilder.DropTable( + name: "Cities"); + } + } +} diff --git a/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.Designer.cs b/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.Designer.cs new file mode 100644 index 0000000..71dfd17 --- /dev/null +++ b/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.Designer.cs @@ -0,0 +1,85 @@ +// +using CityInfo.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + [DbContext(typeof(CityInfoContext))] + [Migration("20260310223915_CityInfoDBAddPOIDescription")] + partial class CityInfoDBAddPOIDescription + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CityId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("PointsOfInterest"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.HasOne("CityInfo.API.Entities.City", "City") + .WithMany("PointsOfInterest") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Navigation("PointsOfInterest"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.cs b/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.cs new file mode 100644 index 0000000..f06f781 --- /dev/null +++ b/CityInfo.API/Migrations/20260310223915_CityInfoDBAddPOIDescription.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + /// + public partial class CityInfoDBAddPOIDescription : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + table: "PointsOfInterest", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + table: "PointsOfInterest"); + } + } +} diff --git a/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.Designer.cs b/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.Designer.cs new file mode 100644 index 0000000..6d737b0 --- /dev/null +++ b/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.Designer.cs @@ -0,0 +1,149 @@ +// +using CityInfo.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + [DbContext(typeof(CityInfoContext))] + [Migration("20260310224446_CityInfoInitialDataSeed")] + partial class CityInfoInitialDataSeed + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Cities"); + + b.HasData( + new + { + Id = 1, + Description = "The one with that big park.", + Name = "New York City" + }, + new + { + Id = 2, + Description = "The one with the cathedral that was never really finished.", + Name = "Antwerp" + }, + new + { + Id = 3, + Description = "The one with that big tower.", + Name = "Paris" + }); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CityId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("PointsOfInterest"); + + b.HasData( + new + { + Id = 1, + CityId = 1, + Description = "The most visited urban park in the United States.", + Name = "Central Park" + }, + new + { + Id = 2, + CityId = 1, + Description = "A 102-story skyscraper located in Midtown Manhattan.", + Name = "Empire State Building" + }, + new + { + Id = 3, + CityId = 2, + Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", + Name = "Cathedral" + }, + new + { + Id = 4, + CityId = 2, + Description = "The the finest example of railway architecture in Belgium.", + Name = "Antwerp Central Station" + }, + new + { + Id = 5, + CityId = 3, + Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", + Name = "Eiffel Tower" + }, + new + { + Id = 6, + CityId = 3, + Description = "The world's largest museum.", + Name = "The Louvre" + }); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.HasOne("CityInfo.API.Entities.City", "City") + .WithMany("PointsOfInterest") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Navigation("PointsOfInterest"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.cs b/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.cs new file mode 100644 index 0000000..cc38bf4 --- /dev/null +++ b/CityInfo.API/Migrations/20260310224446_CityInfoInitialDataSeed.cs @@ -0,0 +1,88 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace CityInfo.API.Migrations +{ + /// + public partial class CityInfoInitialDataSeed : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Cities", + columns: new[] { "Id", "Description", "Name" }, + values: new object[,] + { + { 1, "The one with that big park.", "New York City" }, + { 2, "The one with the cathedral that was never really finished.", "Antwerp" }, + { 3, "The one with that big tower.", "Paris" } + }); + + migrationBuilder.InsertData( + table: "PointsOfInterest", + columns: new[] { "Id", "CityId", "Description", "Name" }, + values: new object[,] + { + { 1, 1, "The most visited urban park in the United States.", "Central Park" }, + { 2, 1, "A 102-story skyscraper located in Midtown Manhattan.", "Empire State Building" }, + { 3, 2, "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", "Cathedral" }, + { 4, 2, "The the finest example of railway architecture in Belgium.", "Antwerp Central Station" }, + { 5, 3, "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", "Eiffel Tower" }, + { 6, 3, "The world's largest museum.", "The Louvre" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 3); + + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 4); + + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 5); + + migrationBuilder.DeleteData( + table: "PointsOfInterest", + keyColumn: "Id", + keyValue: 6); + + migrationBuilder.DeleteData( + table: "Cities", + keyColumn: "Id", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "Cities", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "Cities", + keyColumn: "Id", + keyValue: 3); + } + } +} diff --git a/CityInfo.API/Migrations/CityInfoContextModelSnapshot.cs b/CityInfo.API/Migrations/CityInfoContextModelSnapshot.cs new file mode 100644 index 0000000..d2dbe5c --- /dev/null +++ b/CityInfo.API/Migrations/CityInfoContextModelSnapshot.cs @@ -0,0 +1,146 @@ +// +using CityInfo.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CityInfo.API.Migrations +{ + [DbContext(typeof(CityInfoContext))] + partial class CityInfoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Cities"); + + b.HasData( + new + { + Id = 1, + Description = "The one with that big park.", + Name = "New York City" + }, + new + { + Id = 2, + Description = "The one with the cathedral that was never really finished.", + Name = "Antwerp" + }, + new + { + Id = 3, + Description = "The one with that big tower.", + Name = "Paris" + }); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CityId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.ToTable("PointsOfInterest"); + + b.HasData( + new + { + Id = 1, + CityId = 1, + Description = "The most visited urban park in the United States.", + Name = "Central Park" + }, + new + { + Id = 2, + CityId = 1, + Description = "A 102-story skyscraper located in Midtown Manhattan.", + Name = "Empire State Building" + }, + new + { + Id = 3, + CityId = 2, + Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", + Name = "Cathedral" + }, + new + { + Id = 4, + CityId = 2, + Description = "The the finest example of railway architecture in Belgium.", + Name = "Antwerp Central Station" + }, + new + { + Id = 5, + CityId = 3, + Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", + Name = "Eiffel Tower" + }, + new + { + Id = 6, + CityId = 3, + Description = "The world's largest museum.", + Name = "The Louvre" + }); + }); + + modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => + { + b.HasOne("CityInfo.API.Entities.City", "City") + .WithMany("PointsOfInterest") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("CityInfo.API.Entities.City", b => + { + b.Navigation("PointsOfInterest"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CityInfo.API/Models/CityDataStore.cs b/CityInfo.API/Models/CityDataStore.cs new file mode 100644 index 0000000..0e78e38 --- /dev/null +++ b/CityInfo.API/Models/CityDataStore.cs @@ -0,0 +1,77 @@ +namespace CityInfo.API.Models +{ + public class CityDataStore + { + public List Cities { get; set; } + + public CityDataStore() + { + Cities = new List() + { + new CityDto() + { + Id = 1, + Name = "New York City", + Description = "The one with that big park.", + PointsOfInterest = new List() + { + new PointOfInterestDto() + { + Id = 1, + Name = "Central Park", + Description = "The most visited urban park in the United States." + }, + new PointOfInterestDto() + { + Id = 2, + Name = "Empire State Building", + Description = "A 102 Story skyscrapper located in Midtown Manathan." + } + } + }, + new CityDto() + { + Id = 2, + Name = "Antwerp", + Description = "The one with the cathedral that was never really finished.", + PointsOfInterest = new List() + { + new PointOfInterestDto() + { + Id = 3, + Name = "Antwerp Cathedral", + Description = "a beautiful Cathedral" + }, + new PointOfInterestDto() + { + Id = 4, + Name = "Antwerp central station", + Description = "A train station" + } + } + }, + new CityDto() + { + Id = 3, + Name = "Paris", + Description = "The one with that big tower.", + PointsOfInterest = new List() + { + new PointOfInterestDto() + { + Id = 5, + Name = "Eiffel Tower", + Description = "A big tower." + }, + new PointOfInterestDto() + { + Id = 6, + Name = "The Louvre", + Description = "A Museum." + } + } + }, + }; + } + } +} diff --git a/CityInfo.API/Models/CityDto.cs b/CityInfo.API/Models/CityDto.cs new file mode 100644 index 0000000..4f4603c --- /dev/null +++ b/CityInfo.API/Models/CityDto.cs @@ -0,0 +1,16 @@ +namespace CityInfo.API.Models +{ + public class CityDto + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public int NumberOfPointsOfInterest { + get + { + return PointsOfInterest.Count; + } + } + public ICollection PointsOfInterest { get; set; } = new List(); + } +} diff --git a/CityInfo.API/Models/CityWithoutPointsOfInterestDto.cs b/CityInfo.API/Models/CityWithoutPointsOfInterestDto.cs new file mode 100644 index 0000000..0190a74 --- /dev/null +++ b/CityInfo.API/Models/CityWithoutPointsOfInterestDto.cs @@ -0,0 +1,9 @@ +namespace CityInfo.API.Models +{ + public class CityWithoutPointsOfInterestDto + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + } +} diff --git a/CityInfo.API/Models/PointOfInterestDto.cs b/CityInfo.API/Models/PointOfInterestDto.cs new file mode 100644 index 0000000..bc560ae --- /dev/null +++ b/CityInfo.API/Models/PointOfInterestDto.cs @@ -0,0 +1,9 @@ +namespace CityInfo.API.Models +{ + public class PointOfInterestDto + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + } +} diff --git a/CityInfo.API/Models/PointOfInterestForCreationDto.cs b/CityInfo.API/Models/PointOfInterestForCreationDto.cs new file mode 100644 index 0000000..0214bbd --- /dev/null +++ b/CityInfo.API/Models/PointOfInterestForCreationDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace CityInfo.API.Models +{ + public class PointOfInterestForCreationDto + { + [Required(ErrorMessage = "You should provide a name value.")] + [MaxLength(50)] + public string Name { get; set; } = string.Empty; + + [MaxLength(200)] + public string? Description { get; set; } + } +} diff --git a/CityInfo.API/Models/PointOfInterestForUpdateDto.cs b/CityInfo.API/Models/PointOfInterestForUpdateDto.cs new file mode 100644 index 0000000..087231c --- /dev/null +++ b/CityInfo.API/Models/PointOfInterestForUpdateDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace CityInfo.API.Models +{ + public class PointOfInterestForUpdateDto + { + [Required(ErrorMessage = "You should provide a name value.")] + [MaxLength(50)] + public string Name { get; set; } = string.Empty; + + [MaxLength(200)] + public string? Description { get; set; } + } +} diff --git a/CityInfo.API/Profiles/CityProfile.cs b/CityInfo.API/Profiles/CityProfile.cs new file mode 100644 index 0000000..0745308 --- /dev/null +++ b/CityInfo.API/Profiles/CityProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using CityInfo.API.Entities; +using CityInfo.API.Models; + +namespace CityInfo.API.Profiles +{ + public class CityProfile: Profile + { + public CityProfile() + { + CreateMap(); + CreateMap(); + } + } +} diff --git a/CityInfo.API/Profiles/PointOfInterestProfile.cs b/CityInfo.API/Profiles/PointOfInterestProfile.cs new file mode 100644 index 0000000..10e3fe8 --- /dev/null +++ b/CityInfo.API/Profiles/PointOfInterestProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using CityInfo.API.Entities; +using CityInfo.API.Models; + +namespace CityInfo.API.Profiles +{ + public class PointOfInterestProfile: Profile + { + public PointOfInterestProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/CityInfo.API/Program.cs b/CityInfo.API/Program.cs index 6101d42..65fcc21 100644 --- a/CityInfo.API/Program.cs +++ b/CityInfo.API/Program.cs @@ -1,13 +1,87 @@ -var builder = WebApplication.CreateBuilder(args); +using CityInfo.API.DbContexts; +using CityInfo.API.Models; +using CityInfo.API.Services; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Serilog; +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.File("logs/cityinfo.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseSerilog(); +//builder.logging.clearproviders(); +//builder.logging.addconsole(); // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers((options) => +{ + options.ReturnHttpNotAcceptable = true; +}) + .AddNewtonsoftJson() + .AddXmlDataContractSerializerFormatters(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddDbContext( + (dbContextOptins) => dbContextOptins.UseSqlite( + builder.Configuration["ConnectionStrings:CityInfoDBConnectionString"] + ) + ); +builder.Services.AddScoped(); + +builder.Services.AddProblemDetails(); +//builder.Services.AddProblemDetails((options) => +// { +// options.CustomizeProblemDetails = ctx => +// { +// ctx.ProblemDetails.Extensions.Add("additionalInfo", "Addional info example"); +// ctx.ProblemDetails.Extensions.Add("server", Environment.MachineName); +// }; +// }); + +#if DEBUG +builder.Services.AddTransient(); +#else +builder.Services.AddTransient(); +#endif + +builder.Services.AddAutoMapper(cfg => { }, AppDomain.CurrentDomain.GetAssemblies()); +builder.Services.AddAuthentication("Bearer") + .AddJwtBearer(options => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Authentication:Issuer"], + ValidAudience = builder.Configuration["Authentication:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(builder.Configuration["Authentication:SecretForKey"])) + }; + } + ); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("MustBeFromCharleroi", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("city", "Charleroi"); + }); +}); var app = builder.Build(); +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler(); +} + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -21,8 +95,15 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +app.UseRouting(); + +app.UseAuthentication(); + app.UseAuthorization(); -app.MapControllers(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); app.Run(); diff --git a/CityInfo.API/Services/CityInfoRepository.cs b/CityInfo.API/Services/CityInfoRepository.cs new file mode 100644 index 0000000..e88ac26 --- /dev/null +++ b/CityInfo.API/Services/CityInfoRepository.cs @@ -0,0 +1,101 @@ +using CityInfo.API.DbContexts; +using CityInfo.API.Entities; +using Microsoft.EntityFrameworkCore; + +namespace CityInfo.API.Services +{ + public class CityInfoRepository : ICityInfoRepository + { + private CityInfoContext _context; + public CityInfoRepository(CityInfoContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(CityInfoContext)); + } + public async Task> GetCitiesAsync() + { + return await _context.Cities.OrderBy((city) => city.Name).ToListAsync(); + } + public async Task<(IEnumerable, PaginationMetadata)> GetCitiesAsync(string? name, string? searchQuery, int pageNumber, int pageSize) + { + var collection = _context.Cities as IQueryable; + + if (!string.IsNullOrEmpty(name)) + { + name = name.Trim(); + collection = collection.Where((city) => city.Name == name); + } + + if (!string.IsNullOrEmpty(searchQuery)) + { + searchQuery = searchQuery.Trim(); + collection = collection.Where( + (city) => ( + city.Name.Contains(searchQuery) | + (city.Description != null && city.Description.Contains(searchQuery)) + ) + ); + } + var totalItemCount = await collection.CountAsync(); + var paginationMetaData = new PaginationMetadata(totalItemCount, pageSize, pageNumber); + var collectionToReturn = await collection + .OrderBy((city) => city.Name) + .Skip(pageSize * (pageNumber - 1)) + .Take(pageSize) + .ToListAsync(); + + return (collectionToReturn, paginationMetaData); + } + + public async Task GetCityAsync(int cityId, bool includePointsOfinterest) + { + if (includePointsOfinterest) + { + return await _context.Cities + .Include((city) => city.PointsOfInterest) + .Where((city) => city.Id == cityId) + .FirstOrDefaultAsync(); + } + return await _context.Cities + .Where((city) => city.Id == cityId) + .FirstOrDefaultAsync(); + } + + public async Task CityExistAsync(int cityId) + { + return await _context.Cities.AnyAsync((city) => city.Id == cityId); + } + + public async Task> GetPointsOfinterestForCityAsync(int cityId) + { + return await _context.PointsOfInterest.Where((poi) => poi.CityId == cityId).ToListAsync(); + } + + public async Task GetPointOfInterestForCityAsync(int cityId, int pointOfInterestId) + { + return await _context.PointsOfInterest.Where((poi) => poi.CityId == cityId && poi.Id == pointOfInterestId).FirstOrDefaultAsync(); + } + + public async Task CreatePointOfInterestForCityAsync(int cityId, PointOfInterest pointOfInterest) + { + var city = await GetCityAsync(cityId, includePointsOfinterest: false); + if (city != null) + { + city.PointsOfInterest.Add(pointOfInterest); + } + } + public async Task SaveChangesAsync() + { + return (await _context.SaveChangesAsync() >= 0); + } + public void DeletePointOfInterest(PointOfInterest pointOfInterest) + { + _context.PointsOfInterest.Remove(pointOfInterest); + } + + public async Task CityNameMatchesCityId(string? cityName, int cityId) + { + return await _context.Cities.AnyAsync((city) => city.Name == cityName && city.Id == cityId); + } + + } +} diff --git a/CityInfo.API/Services/CloudMailService.cs b/CityInfo.API/Services/CloudMailService.cs new file mode 100644 index 0000000..30150e0 --- /dev/null +++ b/CityInfo.API/Services/CloudMailService.cs @@ -0,0 +1,20 @@ +namespace CityInfo.API.Services +{ + public class CloudMailService : IMailService + { + private string _mailTo = string.Empty; + private string _mailFrom = string.Empty; + public CloudMailService( + IConfiguration configuration) + { + _mailTo = configuration["mailsettings:mailToAddress"]; + _mailFrom = configuration["mailsettings:mailFromAddress"]; + } + public void Send(string subject, string message) + { + Console.WriteLine($"Mail from {_mailFrom} to {_mailTo} with {nameof(CloudMailService)}"); + Console.WriteLine($"Subject: {subject}"); + Console.WriteLine($"Message: {message}"); + } + } +} diff --git a/CityInfo.API/Services/ICityInfoRepository.cs b/CityInfo.API/Services/ICityInfoRepository.cs new file mode 100644 index 0000000..2da0b91 --- /dev/null +++ b/CityInfo.API/Services/ICityInfoRepository.cs @@ -0,0 +1,18 @@ +using CityInfo.API.Entities; + +namespace CityInfo.API.Services +{ + public interface ICityInfoRepository + { + Task> GetCitiesAsync(); + Task<(IEnumerable, PaginationMetadata)> GetCitiesAsync(string? name, string? searchQuery, int pageNumber, int pageSize); + Task GetCityAsync(int cityId, bool includePointsOfinterest); + Task CityExistAsync(int cityId); + Task> GetPointsOfinterestForCityAsync(int cityId); + Task GetPointOfInterestForCityAsync(int cityId, int pointOfInterestId); + Task CreatePointOfInterestForCityAsync(int cityId, PointOfInterest pointOfInterest); + void DeletePointOfInterest(PointOfInterest pointOfInterest); + Task CityNameMatchesCityId(string? cityName, int cityId); + Task SaveChangesAsync(); + } +} diff --git a/CityInfo.API/Services/IMailService.cs b/CityInfo.API/Services/IMailService.cs new file mode 100644 index 0000000..995f8bc --- /dev/null +++ b/CityInfo.API/Services/IMailService.cs @@ -0,0 +1,7 @@ +namespace CityInfo.API.Services +{ + public interface IMailService + { + public void Send(string subject, string message); + } +} diff --git a/CityInfo.API/Services/LocalMailService.cs b/CityInfo.API/Services/LocalMailService.cs new file mode 100644 index 0000000..43e5f2a --- /dev/null +++ b/CityInfo.API/Services/LocalMailService.cs @@ -0,0 +1,21 @@ +namespace CityInfo.API.Services +{ + public class LocalMailService:IMailService + { + private string _mailTo = string.Empty; + private string _mailFrom = string.Empty; + public LocalMailService( + IConfiguration configuration) + { + _mailTo = configuration["mailsettings:mailToAddress"]; + _mailFrom = configuration["mailsettings:mailFromAddress"]; + } + + public void Send(string subject, string message) + { + Console.WriteLine($"Mail from {_mailFrom} to {_mailTo} with {nameof(LocalMailService)}"); + Console.WriteLine($"Subject: {subject}"); + Console.WriteLine($"Message: {message}"); + } + } +} diff --git a/CityInfo.API/Services/PaginationMetadata.cs b/CityInfo.API/Services/PaginationMetadata.cs new file mode 100644 index 0000000..9f5a8ba --- /dev/null +++ b/CityInfo.API/Services/PaginationMetadata.cs @@ -0,0 +1,18 @@ +namespace CityInfo.API.Services +{ + public class PaginationMetadata + { + public int TotalItemCount { get; set; } + public int TotalPageCount { get; set; } + public int PageSize { get; set; } + public int CurrentPage { get; set; } + + public PaginationMetadata(int totalItemCount, int pageSize, int currentPage) + { + TotalItemCount = totalItemCount; + PageSize = pageSize; + CurrentPage = currentPage; + TotalPageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize); + } + } +} diff --git a/CityInfo.API/WeatherForecast.cs b/CityInfo.API/WeatherForecast.cs deleted file mode 100644 index 944c31f..0000000 --- a/CityInfo.API/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CityInfo.API -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/CityInfo.API/appsettings.Development.json b/CityInfo.API/appsettings.Development.json index 0c208ae..72d219a 100644 --- a/CityInfo.API/appsettings.Development.json +++ b/CityInfo.API/appsettings.Development.json @@ -2,7 +2,17 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "CityInfo.API.Controllers": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" } + }, + "ConnectionStrings": { + "CityInfoDBConnectionString": "Data Source=CityInfo.db" + }, + "Authentication": { + "SecretForKey": "RgDldLrK+p+T0JisAKdD7THnT/npmWY14vV3UUiRSVE=", + "Issuer": "https://localhost:7289", + "Audience": "cityinfoapi" } } diff --git a/CityInfo.API/appsettings.Production.json b/CityInfo.API/appsettings.Production.json new file mode 100644 index 0000000..548d4e4 --- /dev/null +++ b/CityInfo.API/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "mailSettings": { + "mailToAddress": "admin@mycompany.com", + } +} \ No newline at end of file diff --git a/CityInfo.API/appsettings.json b/CityInfo.API/appsettings.json index 10f68b8..3467020 100644 --- a/CityInfo.API/appsettings.json +++ b/CityInfo.API/appsettings.json @@ -5,5 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, + "mailSettings": { + "mailToAddress": "developers@mycompany.com", + "mailFromAddress": "noreply@mycompany.com" + }, "AllowedHosts": "*" } diff --git a/CityInfo.API/email_template.pdf b/CityInfo.API/email_template.pdf new file mode 100644 index 0000000..28f32bb --- /dev/null +++ b/CityInfo.API/email_template.pdf @@ -0,0 +1,1359 @@ +%PDF-1.3 +% +9 0 obj +<< +/Type /ExtGState +/ca 1 +>> +endobj +15 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +16 0 obj +<< +/Subtype /Link +/A 15 0 R +/Type /Annot +/Rect [390.077676 117.781612 495.095723 129.978018] +/Border [0 0 0] +/F 4 +>> +endobj +17 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +18 0 obj +<< +/Subtype /Link +/A 17 0 R +/Type /Annot +/Rect [385.52002 105.585205 496.76916 117.781612] +/Border [0 0 0] +/F 4 +>> +endobj +19 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +20 0 obj +<< +/Subtype /Link +/A 19 0 R +/Type /Annot +/Rect [385.52002 93.388799 414.642754 105.585205] +/Border [0 0 0] +/F 4 +>> +endobj +8 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 6 0 R +/Resources 7 0 R +/UserUnit 1 +/Annots [16 0 R 18 0 R 20 0 R] +>> +endobj +7 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F2 10 0 R +/F3 11 0 R +/F4 12 0 R +/F5 13 0 R +/F1 14 0 R +>> +>> +endobj +24 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +25 0 obj +<< +/Subtype /Link +/A 24 0 R +/Type /Annot +/Rect [390.077676 170.753154 495.095723 182.94956] +/Border [0 0 0] +/F 4 +>> +endobj +26 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +27 0 obj +<< +/Subtype /Link +/A 26 0 R +/Type /Annot +/Rect [385.52002 158.556748 496.76916 170.753154] +/Border [0 0 0] +/F 4 +>> +endobj +28 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +29 0 obj +<< +/Subtype /Link +/A 28 0 R +/Type /Annot +/Rect [385.52002 146.360341 414.642754 158.556748] +/Border [0 0 0] +/F 4 +>> +endobj +23 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 21 0 R +/Resources 22 0 R +/UserUnit 1 +/Annots [25 0 R 27 0 R 29 0 R] +>> +endobj +22 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +/F1 14 0 R +>> +>> +endobj +33 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +34 0 obj +<< +/Subtype /Link +/A 33 0 R +/Type /Annot +/Rect [390.077676 210.343577 495.095723 222.539984] +/Border [0 0 0] +/F 4 +>> +endobj +35 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +36 0 obj +<< +/Subtype /Link +/A 35 0 R +/Type /Annot +/Rect [385.52002 198.147171 496.76916 210.343577] +/Border [0 0 0] +/F 4 +>> +endobj +37 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +38 0 obj +<< +/Subtype /Link +/A 37 0 R +/Type /Annot +/Rect [385.52002 185.950765 414.642754 198.147171] +/Border [0 0 0] +/F 4 +>> +endobj +32 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 30 0 R +/Resources 31 0 R +/UserUnit 1 +/Annots [34 0 R 36 0 R 38 0 R] +>> +endobj +31 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +/F1 14 0 R +>> +>> +endobj +42 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +43 0 obj +<< +/Subtype /Link +/A 42 0 R +/Type /Annot +/Rect [390.077676 231.358913 495.095723 243.555319] +/Border [0 0 0] +/F 4 +>> +endobj +44 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +45 0 obj +<< +/Subtype /Link +/A 44 0 R +/Type /Annot +/Rect [385.52002 219.162506 496.76916 231.358913] +/Border [0 0 0] +/F 4 +>> +endobj +46 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +47 0 obj +<< +/Subtype /Link +/A 46 0 R +/Type /Annot +/Rect [385.52002 206.9661 414.642754 219.162506] +/Border [0 0 0] +/F 4 +>> +endobj +41 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 39 0 R +/Resources 40 0 R +/UserUnit 1 +/Annots [43 0 R 45 0 R 47 0 R] +>> +endobj +40 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +/F1 14 0 R +>> +>> +endobj +51 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +52 0 obj +<< +/Subtype /Link +/A 51 0 R +/Type /Annot +/Rect [390.077676 104.787639 495.095723 116.984045] +/Border [0 0 0] +/F 4 +>> +endobj +53 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +54 0 obj +<< +/Subtype /Link +/A 53 0 R +/Type /Annot +/Rect [385.52002 92.591233 496.76916 104.787639] +/Border [0 0 0] +/F 4 +>> +endobj +55 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +56 0 obj +<< +/Subtype /Link +/A 55 0 R +/Type /Annot +/Rect [385.52002 80.394826 414.642754 92.591233] +/Border [0 0 0] +/F 4 +>> +endobj +50 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 48 0 R +/Resources 49 0 R +/UserUnit 1 +/Annots [52 0 R 54 0 R 56 0 R] +>> +endobj +49 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +/F1 14 0 R +>> +>> +endobj +59 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 57 0 R +/Resources 58 0 R +/UserUnit 1 +>> +endobj +58 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +>> +>> +endobj +62 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 60 0 R +/Resources 61 0 R +/UserUnit 1 +>> +endobj +61 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F3 11 0 R +/F4 12 0 R +/F5 13 0 R +>> +>> +endobj +65 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 63 0 R +/Resources 64 0 R +/UserUnit 1 +>> +endobj +64 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +>> +>> +endobj +68 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 66 0 R +/Resources 67 0 R +/UserUnit 1 +>> +endobj +67 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F3 11 0 R +>> +>> +endobj +72 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +73 0 obj +<< +/Subtype /Link +/A 72 0 R +/Type /Annot +/Rect [390.077676 553.735545 495.095723 565.931952] +/Border [0 0 0] +/F 4 +>> +endobj +74 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +75 0 obj +<< +/Subtype /Link +/A 74 0 R +/Type /Annot +/Rect [385.52002 541.539139 496.76916 553.735545] +/Border [0 0 0] +/F 4 +>> +endobj +76 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +77 0 obj +<< +/Subtype /Link +/A 76 0 R +/Type /Annot +/Rect [385.52002 529.342733 414.642754 541.539139] +/Border [0 0 0] +/F 4 +>> +endobj +71 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 69 0 R +/Resources 70 0 R +/UserUnit 1 +/Annots [73 0 R 75 0 R 77 0 R] +>> +endobj +70 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F1 14 0 R +/F3 11 0 R +>> +>> +endobj +81 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +82 0 obj +<< +/Subtype /Link +/A 81 0 R +/Type /Annot +/Rect [390.077676 593.325938 495.095723 605.522345] +/Border [0 0 0] +/F 4 +>> +endobj +83 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +84 0 obj +<< +/Subtype /Link +/A 83 0 R +/Type /Annot +/Rect [385.52002 581.129532 496.76916 593.325938] +/Border [0 0 0] +/F 4 +>> +endobj +85 0 obj +<< +/S /URI +/URI (https://admin.app.doffice.info/reservationDetail/47) +>> +endobj +86 0 obj +<< +/Subtype /Link +/A 85 0 R +/Type /Annot +/Rect [385.52002 568.933126 414.642754 581.129532] +/Border [0 0 0] +/F 4 +>> +endobj +80 0 obj +<< +/Type /Page +/Parent 1 0 R +/MediaBox [0 0 595.280029 841.890015] +/Contents 78 0 R +/Resources 79 0 R +/UserUnit 1 +/Annots [82 0 R 84 0 R 86 0 R] +>> +endobj +79 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ExtGState << +/Gs1 9 0 R +>> +/Font << +/F4 12 0 R +/F5 13 0 R +/F1 14 0 R +>> +>> +endobj +88 0 obj +(react-pdf) +endobj +89 0 obj +(react-pdf) +endobj +90 0 obj +(D:20260306104716Z) +endobj +87 0 obj +<< +/Producer 88 0 R +/Creator 89 0 R +/CreationDate 90 0 R +>> +endobj +14 0 obj +<< +/Type /Font +/BaseFont /Helvetica +/Subtype /Type1 +/Encoding /WinAnsiEncoding +>> +endobj +92 0 obj +<< +/Type /FontDescriptor +/FontName /CZFZEA+Inter18pt-Bold +/Flags 4 +/FontBBox [-790.527344 -334.472656 2580.566406 1114.746094] +/ItalicAngle 0 +/Ascent 968.75 +/Descent -241.210937 +/CapHeight 727.539063 +/XHeight 539.0625 +/StemV 0 +/FontFile2 91 0 R +>> +endobj +93 0 obj +<< +/Type /Font +/Subtype /CIDFontType2 +/BaseFont /CZFZEA+Inter18pt-Bold +/CIDSystemInfo << +/Registry (Adobe) +/Ordering (Identity) +/Supplement 0 +>> +/FontDescriptor 92 0 R +/W [0 [1344 660.644531 587.890625 905.761719 623.046875 265.625 574.21875 366.699219 231.933594 623.046875 323.730469 265.625 551.269531]] +/CIDToGIDMap /Identity +>> +endobj +10 0 obj +<< +/Type /Font +/Subtype /Type0 +/BaseFont /CZFZEA+Inter18pt-Bold +/Encoding /Identity-H +/DescendantFonts [93 0 R] +/ToUnicode 94 0 R +>> +endobj +96 0 obj +<< +/Type /FontDescriptor +/FontName /PBAZWT+Inter18pt-SemiBold +/Flags 4 +/FontBBox [-774.414062 -331.054687 2580.078125 1113.28125] +/ItalicAngle 0 +/Ascent 968.75 +/Descent -241.210937 +/CapHeight 727.539063 +/XHeight 539.0625 +/StemV 0 +/FontFile2 95 0 R +>> +endobj +97 0 obj +<< +/Type /Font +/Subtype /CIDFontType2 +/BaseFont /PBAZWT+Inter18pt-SemiBold +/CIDSystemInfo << +/Registry (Adobe) +/Ordering (Identity) +/Supplement 0 +>> +/FontDescriptor 96 0 R +/W [0 [1344 652.832031 583.007813 893.066406 615.722656 254.882813 566.894531 352.539063 300.292969 246.09375 506.835938 539.550781 603.027344 615.722656 469.238281 617.1875 387.695313 603.515625 615.722656 600.097656 562.011719 254.882813 750.488281 576.660156 716.308594 574.21875 459.472656 718.75 312.5 559.570313 830.078125 577.636719 603.515625]] +/CIDToGIDMap /Identity +>> +endobj +11 0 obj +<< +/Type /Font +/Subtype /Type0 +/BaseFont /PBAZWT+Inter18pt-SemiBold +/Encoding /Identity-H +/DescendantFonts [97 0 R] +/ToUnicode 98 0 R +>> +endobj +12 0 obj +<< +/Type /Font +/BaseFont /Courier-Bold +/Subtype /Type1 +/Encoding /WinAnsiEncoding +>> +endobj +100 0 obj +<< +/Type /FontDescriptor +/FontName /ETDIPK+Inter18pt-Regular +/Flags 4 +/FontBBox [-742.1875 -323.242187 2579.589844 1109.375] +/ItalicAngle 0 +/Ascent 968.75 +/Descent -241.210937 +/CapHeight 727.539063 +/XHeight 539.0625 +/StemV 0 +/FontFile2 99 0 R +>> +endobj +101 0 obj +<< +/Type /Font +/Subtype /CIDFontType2 +/BaseFont /ETDIPK+Inter18pt-Regular +/CIDSystemInfo << +/Registry (Adobe) +/Ordering (Identity) +/Supplement 0 +>> +/FontDescriptor 100 0 R +/W [0 [1344 596.191406 867.675781 551.757813 234.375 234.375 274.414063 572.753906 581.054688 550.78125 588.378906 550.78125 572.753906 551.757813 323.730469 581.542969 516.113281 602.539063 364.257813 602.050781 289.550781 602.050781 234.375 272.949219 714.355469 602.050781 602.050781 272.949219 558.59375 452.148438 599.121094 612.304688 352.539063 626.953125 589.355469 581.542969 635.742188 272.949219 355.957031 560.546875 453.125 396.972656 639.648438 272.949219 612.304688 681.640625 572.753906 610.351563 893.066406 632.8125 260.742188 554.6875 581.542969 650.878906 560.546875 743.652344 728.515625 967.773438 582.519531 742.675781 735.351563 537.109375 761.230469 637.695313 803.710938]] +/CIDToGIDMap /Identity +>> +endobj +13 0 obj +<< +/Type /Font +/Subtype /Type0 +/BaseFont /ETDIPK+Inter18pt-Regular +/Encoding /Identity-H +/DescendantFonts [101 0 R] +/ToUnicode 102 0 R +>> +endobj +4 0 obj +<< +>> +endobj +3 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/Names 2 0 R +/ViewerPreferences 5 0 R +>> +endobj +1 0 obj +<< +/Type /Pages +/Count 11 +/Kids [8 0 R 23 0 R 32 0 R 41 0 R 50 0 R 59 0 R 62 0 R 65 0 R 68 0 R 71 0 R 80 0 R] +>> +endobj +2 0 obj +<< +/Dests << + /Names [ +] +>> +>> +endobj +5 0 obj +<< +/DisplayDocTitle true +>> +endobj +91 0 obj +<< +/Length 1310 +/Filter /FlateDecode +>> +stream +x]T}LSW?AixԶZ k~ 0m (U@"ʦq-&s if2-HLӘlW_w^[}{9n +&O7҂),bVq¾BmIhnh-(ٜ06oK,lʀ`3iy G@+[[J' \p.ET G/#}@n!HEG|.1M죂> 򟯣0 ,=c䦀XC)T95GX[y5jNf5fVϛ$/k,i +Fb,Iҏc5Ԉ$իb5"h8D#b4""DD8tH =vDp6!*ps(" d{.11 .*ʙX"2c,i]&xP/,1R|4(b7(69r9fj"*ˠӏl`,A_`*Qv?Oyg\_ٱtYWu_ݮ%<q0ϷflRsB\%;-d[&쌦mdzstmi>Y'찹J~uS#);:ܿ}+6n+\W@_-+`zf%k kN)sF^Az&):G{tNWޭ +X*gVe.]8/+P=#WR09P9%^= +1Y$i0 )B0H1-j, *3hE%#yʷ[-V}ݮw֟s'tw@`iY`_;viYdvԠ혳/;V\=-ʩtNnI~5 jpL.Dx곘$ɝg\?>ܹCT~ I'3}f'N?)b3KUOevvpw$WsIetnZ7jv/zK4k?\rxD +E\OEs,rAybC807܈2Hӊi2$2^ -KD:](~ϥ^5yI\WQЦKw9U|ux;Ďq]V^gU[`(u,*ʵ^ }D05q<(xrg#'YHdH|O^o2_o r +([ѩX C]XB= spRpL+?2 +endstream +endobj +94 0 obj +<< +/Length 279 +/Filter /FlateDecode +>> +stream +x]Mn >,E؎F,UƋna Հ0^R@xF#vϝwyFE# 8I%]":L{s-N&?8Iݛh`u_Cֶ p^ye3BN}'.ݶ Eka-Sfj3T\T5o5E p#T"19yjRxALec2y.TF9@%$Wk}TIQHG*&h +endstream +endobj +95 0 obj +<< +/Length 2599 +/Filter /FlateDecode +>> +stream +xmW PSW>ܛ" hHP+! K \b h BC@D@>]-*ݩպt:ݱTYl;n-mwŢrO?7J{?KCeHDPBEm{$^ǯDW:=rfsSoರlz\`[EMCo}*d{4DPDϰoa!YWS77HHLhT + R:!٢S9N0XbV,&6a#Dyod`OȍXFF # ȋFH am ==tnN"Aؓ!6X k0ۅ`MpTpJx[)@pL@Tec'rzADzO" ~oQH,$(uDѠdM!տ}WOr& MGnkr|^41:wsYO_;;dv=.`WyO>#XL.thB;0C:1/nj4ѧYiy+ l^Ywv/#h=mzxvGJi +oS'xڵ7L%+6'mb.ycœ#;?F #EiV:rh4s)5IνxEGkBҋ\-x.[+(RzJ5'I 4K|1%K4L,LII 9B +BH]nUN5JwdB}{.^Y5# jm/iGb"hyM64RA·n8zSE*OobEϛ]9 qlbΡ )QaaŬr?6ͳr˼6 լcyۋ[/23wA7իZiE ͗$whxZtߕ¢_-edFm.wzh̪Uo֚I-o 6)J?aؓ3KH97& i$ +b ՐO ìhjDdP~?AJQ}lUCmuƝޯg "U݀R |iv=lJKkr6YMC=ɡC=Kz*] + KhFMP밁6NƸΈ˨Bs0x\oĵ`Hn=pM ,YK 2u8hq"GȢVRpŮ/N7H66^|KsD.np|c@ͻGp<g. +"݆'jmZ=[ygj9q36mj"M=,<OcW.߰9M7|~g ~A_{_}muZ3*8MCf^e^‘ӭЉE ]΄Es +ZD2qyiô呮AW9{i͉q?bŰ$af1yG Fdx"iu`̣ih^ !l]qpg|׽vJ#a["G/V`A2cʴ RMX_ô ?XB|Qβ<1u'. y}kVϻ v)a?jQ Py'o58@\!Dz:i#&j#)E[Uuz=<&ZL%:Z|i~ }0!dv4L|@&7lRlOQ!>2Kd +=NDk> ++Wz wTa.%utRqS~R=`_`}P((R8qeI̒eq╙ӟ&'%فQ3?_C2<$VXh9xA5?ߚZ jY +_FE F$P602#'Cл|Ik)G|#C^6܏W~ZLhEdO"=t0Pzՙk +endstream +endobj +98 0 obj +<< +/Length 364 +/Filter /FlateDecode +>> +stream +x]n0E|""DBHUaчRb! JSmEonZD[hoNhH&z ][ejۍ,#!-w-هOrFᣎ Q!$)xl] gFߜ9[4N>  +endstream +endobj +99 0 obj +<< +/Length 4762 +/Filter /FlateDecode +>> +stream +x}X \׺?gf  VEB0q +1  A@((;UjeS +XzoZm}jo7jk]3yLBE}_9|7Ɉ@S7 V Txa9dd'M@;d&k ]~촹> +6flN1 ~I/6[;I!k^?mD k7Xm1. @Q +vLybϫ0fc82҇%P܎@^@@cq"#ąuw` oPP)W@2 4!4ET4 'A,Wy 0pќFhJ(-i@`"K)̳<B@: B@p5`M&#X+X<$tK iA W P  #0aG1!g@ˑBb +B,|1'Q1<%B.(d̨`.c0ξ3ߒtjb67ћ7R9:GX9 + %h-W*˱|+Q-`2)eJ\afVrS?Xڀ3"JB"4>pĞ˝jǖ3N<?Xx<_J}?*|ugxOn1%uUNN܏SG R K%+ BDÓ1l  BPX< B`#m{CK+RU9~N+Lvx%z/Oґ_co:B<:AZ\T^wRmn34LdU2yepsԆ{c$0 rƞ8 ?P쥘+%V@< |9MdAZ͞5{S3Clgl=;nLOk 6 Y*2 B"E^l+.&VV]tl=ר%*[a/gˍR5D,*Tk97蘋?;,bBrCu֭H=2МZ l.+ ;eewۿ-+=ϯ%+߿iz&0Di6g$'g8 B:EnX&:Z{iD9CJG=/.DЋNGYQIW j]@<:u&={}1Yxxk Q[6D\.@QZ)gS<9?O.fbZP[ 7nʴ@\.aK)- <7{itkTF>oTNɀaa'n{2jx(@"( lo{2#ؾ1oF}}wPadw7Tvwd}0ݧv]ltϲ`E0pgzߕy{^k[˨O- s"O-`LYb? 8N>N[m;EjzN.Oz]Ryhjŭg_T%߱sLA_^#xl7TJکD#͚ux9 D$N8^9I%&6P LPV 50YM[ݵ6 ,E!  )) Ũ3j.S4@ڹg3!M`G-cyJO1%DԲA3QHkTB>Z4 GIȎ>sIM$>ޏ1 `XRT!nn! rUo^JJ&8 +ow_aEg}2U9N`k>FgLvv+FlLc2/qR\jq + 01 ѹQztoN 2Φo*)9Ɯ p ?я7+BD}f2G&+z8c'}`XSs@";/)1&b@aI!ѵ.XJE\QRd.lZ|93~+TWU^+v-ymXcwaKx Ova r~#ղTutĮX_ݱn.lpLv8wc##8s[Zsg/۶=/,l_+8#CCtϽӝM| %tBmx2kHyAoMR2d}:9nMۂjR+_֤̓Ul*Z7}1xI{9֞l]aL%,Y5J :0D_≿2 Pμ3 wT_/B$<Ȗdνj(7͸oWEr{TRCw%뷻4+.ZUnyȞ˕Y~V<_a(-s|Lhpk1FoD*G4-UeSj?&|?~]_uPSBC?;6潐xN.,ȉGZnoޝ}cۄ+ Z^$]jkHlUH'8q'!!EeVY9);;ad-ܪ3Qs!ze=} ̛4 + % c՘O#Ҧvz+>W%u=ӛvux=Wzqߡȿ .y @dKa{M٥RYQCAAg4gWH;I h˖7c̼Ada_`[F6ʰ<9ƍ's7ɵ8ydaB,0L.$. +]eb`#[8b[@ ŕCr"Iܩbs%Sqr$,^$ަ{-::$Apƞ_f9.IǁN=|+Fbg=`D,c`#{~칭͹C༼o|"gZu[Bꢣwk]qzNߧjLTk*^Xڠ6n[97@nU%W3 8$clf*_4 u?[B`'IAnE$TsgxĹݱJ(jIN}Io4[{ʹv{ {ZsAY88IXdž<^t1ۛlF,Uښ[Jo +[g"Q?ő#Oͯ-XS=U͸:9P4SH8*K11IdΚ-CI!5{_.6([t%~ơcnjge,LR[u*6s(b[a|Rx// +XK#o^a}pػTn^IUj0/ZLMiߜ:Cs)vYl&?` +MS4FSXzItݫD.%T =ܚ}/P QjSfi٭ҋKiobA`Gr~2ӿ@XG?9G?ϸTmy v)&:Aɖs~d%D'.f/+YC)aXɱKdF[b:zzKNSȕ ?Bl%n6^zCVU=6D ; U˹i>J_ξ>/+*+VQQ##?H;|?O?it$FH2ÜE]]OqppkKW&}`r GFpUm=fW1cM l6Sj6m6ak˟G5o_h AJts&cFlI?]\s_gggg_F㟸<m/%MØ')>.O "s9,d}D| h;ߐ( *隤֐Ck`F3pLco}Tv-f6YC"q ە::K-lf8 + +(?CT:rNATOE)h3O#ڑ5u(COOxAW'M^Agڙ>Uɷ`P#ƐC< *`Q_[Q"y$Z-JcN>rdqFyc !E +hZ0v 朇}ކ`++(> +stream +x]M0 +ˌ])Tm/9CMH Bn"a챟a藐t%tvosps?dOLYN>ޯ]C7. !|do_~8G|9ަ]lXS߇ֺ5f\,xhxYGO\pKil:5f8[{Z[}fC_*9ԥf3o@A, AP"P@<[g{e"4t":m`-SEB:|j% @&(#> >5M T >B>Ї{+G=|"KUG||>|((>GS?> >n |5(M_?sc>(|J(|")|-QtOd>e(|"k§vOd>% qXmn#tzqo'io +endstream +endobj +78 0 obj +<< +/Length 4556 +/Filter /FlateDecode +>> +stream +x]ݮ $+"dFj/f}I"M4) + ЧYSmMW +TX} jZ 'rDi_̢/iw¦J.t}?O?TvϯOo'RϷvJI%j`Gs*IOKEkjGuܝsmB-#%jT1WܢFM-Uqr6,YҦT(rhlغˁ㷮а6yΞg=旄!LV]^}Q6h1D1U7)6t# +G S[}ɦ UЮ~6h^ϵAcfHN po8r})w8>b+d]0H#!,wx(Va04lakԽ|(:)h#6G)Z%USZbm}57;9}گ,?'_}RWZ}˟ohūME +Z}/ԝF]".{0> H%14N1%q"5Qrć02|suNEfz6{ؑ&v35lbU]8\Azgq (ϥ-lihvE/Lx]J<؞$uV&qdE8\e !bכ1ߑv(vh +e:l51}<{xh"h|3;:B#mM1q܁9[F4 'N{ZUAajd(SM 3&$i N_N_>6IDm\ 0޲/kUo|Y)i+, VkùFfЦ*h -&t̎6?X/_/_zBpiow-H,2gH+S_餘] #"6 +Fg?/QđXHG{il^5R-`jZ knp jM mU$U >- @i˙_qߧaI%|>q-Fyn犡;DRSᴭ?Gh,{|pH=NX6sq[͋a}6*j5}hUCQ[ZuRnN VU^2Dr/5ͤP-$\m݅H넳.5xsuxǦX""-,#YPhjpܦT54ќyVgg=΀|F?(nCcs{2}(ar"Nj\)Y$1.  %A! +{rv q,i)@sCqSԸ&'x"Mf/hGigù~80yI5yLq%%cf? +u:yH V}i>fX$/wN`^q +ac͘gilgD}$)KI +{[hJ/ˏulp\Zqxu*ւ) kc K9I4TvOihm=n$IˎNwyaSXr@:2a[u~-lB-> +4 $/>sV7=bpOޛ!|g?Z+0 9 ӹ0 ӹ0 ӹ0 ӹ0 ӹ02S1tS;g;kuLvICeiꜤr[*+sha:!ϙس"P*2E L(j +Bf)E6M^NU@z6쏃|85@deJQ-H"z< kZ\Fٴ^&&MnHt&-ikݒİ`*"I$lg D/o0c uSwdQ#Xg8\h LXz{bػ.#()Zx_x>>PY1]o{@Ϯ׮ ,V!uyo>Q(ϣlzQ@8ˠV^CE j@xQkcrS$P='%D- pOZD:\ ’X7)%q ܪ3"4@` +hte5v:BuV +EP$uP +EP +EP +EP +EP +E⸤{@qNu!Z#s'`mmP\mꜤx[*+Dh.9'ҁ: ϘS"P*2 L,]zB"g+G,6MHW@{6?|85~0֯+o( =ϛv˴s00W#ctmjV֚7"gQ!֙bِͺ'lvn/[FhOcV$`X><*+M{{%`\?;/##)\xߐxV>xPY1]oG{`ѡخ!,VAu yoF8OClzF@&eQҢF5M}Xۯ:ZkgVM*TS$m{beAKlIUeXu{ҲA:x^\*2Egue5L-leeA5>V`$/+D>VVZ7]bee2=UXY l]XYUXYUXYUXYUXY%/,{昪 n6C&a!߼mM1k$N lۨDSVwdi .wxσ‡:[B`ōG.WO.D2rJuS( uI{w Pi@T 3 ݁>ĦCm~+O硵w|jQ]\H1KZy=kI*{Z-P[;c]ǺE1,LV齴yK+[-;AH&~'C~YWoE{Y_'uY_eyY_eyY_]ޏE'`ӻQpG^kkdS$P]M KS0qDrG^( uXuvSv!qcvx^ܟ.tghvA5~䛳;^ۿgY XUFÅYxxmUwV% VѦ9> +stream +x][븑~hwR@C9>Vw' !u)R7>#X*UQ煘?+8ӯcD%4fQ} >a׌7fD_S~?NoNsۉ1F`HÔ%:9VDaМlŶkiVl>u dRHAcqF@_GɨakŞutvk$ǟOبPcVQ"Tq,ˉy_ Fkd4!j.:y:*ur9i'oa#QqdvL\bͳp=^G2ضxHbAAki9R َ<1ѮAȑ8#Nk5$p/\YMH#_ӡf>>9kJ8'ZaHS-\Ed]0ה !an$!J @jX. +󆇣1ĕ9C{KvǡR7'շ #kG€#v=ZhC +2=/tw`6 ͌[(Â!(9wDn[uwm6BB |iKgbNQt 0`|sݦML^_׽mJ4{uhG=\05A(kgQF_EJWr +5:gC|,]_6[$:6u`Y;mtq1e+!q$r1Yk=v=}a64Bm;<޳Pl dX \!%NJrA+n}\M{ h?C RLO?9_+O)$r09MmzmtbF_bL[v "18ҊmۈtC+=!Qs;4t8}Ax#|1)OQ8mK|J穹z֜紳af"kRzobλ쇉 :}$땙\Z} 0[ +D69<dzuϔw̚V Y"MJfqOo):t6>sAQR\f + +C6^DeH1#l L)WL[VkqC%X?ÂM|1_}0B2etp)A$WU7|~=KĸѴ6t+(bc&L*R.e'VM[.GAxBĨX5[#!H%;Y?٨P ji谩N0k$nZYʮݩ(1$ \h:?qQ1ΚC2c7"}1v( ^ɽ2Pаsm8FLRNiHpL"N:p F w$q2ڌ74zNЊmO.kJ+ĭ:wI$?`3; 4ן\#g^Wؘ|b A4A'֫!W G$ f8N.6 B+Zn=3Lj.>ЎT1z_00hFĆkzcs}o3Q8~'Af0J[. $^hm +e@K>3d5aӚy;d;UKؓ&m(<e7`sc˺.y̯uv= +/@z"ğKȃDg@w[ fBWr +:`Fw}+Ptx,# }|(qۓP:|iDI+-: !w> Ѽ>-Z^P]2$3e4,k%y79UIJ$g4N:fa 7׌76|xcriێEwZٍ-@wWb9#LQ\2HdNG'kX(4AE ֻl`ZSE*'&ՆT/Ye~?OvÅxBCE.Ig3cۺLαI&E.]0#}ťe$IrLR{07`R,yJ9Ccg Z"]R2 PSf$&br{5YWL3* e6\qDzKEmaP"pҘ"XҌ-eԗK1-K}MoMK <USsii/(y ~ #Vl^R<(F[ P zc1lp|S}A-oڗe}@*s@kJåܗqݦl45[uivnu-X$iOWD!X&RTV+N46V0ML=jp!j. VZ<USsij+=|7)E\~ Sӧ^x*3 s!Ak{ 1a93mx gW{eX}ł!>V00ł[t秼aBxt {yH8ڸAHq=QǨc//$ܫd{Sm5ӋƚçɆc^Ѩw 9]ksİVN;({W90G؛\Z}!0c + 0eA4(aăqq#kE3ktz=uA.p1im['ф4/xKVIk@{+4TXiv!#VlIbPbAAkib'68yɤ5m]</LZ[YUHcW'%"?gwNZ+,,,,,,,,,wTeXQ)ebO!|" 0RucV1qAL=umKR=穹U4A&?vōN‘_Iͥ _ky sȱI6I<dzfW{,IC8ac=Wϯx7/mUqC9q rABS\(_&Y /s)*ѹ/d`Z-kY^K2wyIt;`,(gMQM^TUly;3˫2=rkє1D ;qOeF>/K;hOܪ(*nvIͥ +=F HagN2#ypˌv3@]ae=(mK~C!LG4 RQ^joJM)7MYޔڛR{SjoJMKM)7ޔڛR{SjoJM9$Fs$K|ȮޔқRzSJoJM)KM)YkW&1Yc=VΥ7]ʿof \-6LCԷRr.-ԷRR[2K}K'^[J}Ko)-n-d '*S^(s1崔2RR\ݳRYI)s)e.̥2|Y2RR\JK)s)e.̥2RrTJP︗ V[ƑRR[JuKn)-%o.-e]fG84VQja}VҔЮeoGy*(SS`JL9*RS`JL)U0 +&c/U0 +T*Rs*Ɠg'AX_C5"\hb_D!yHBegb 26M獜\tgرU +he`K1b|4 dةq?θ2hmwz= +3Ƨ>M6,efYmܲjB?a5W ČB+Rbq/f RS K6P \}Jyټ{e~I6[ōzAlNX^opgnґieήXa]#VOsyhvNtQuD,z_-JAALwq>{$T 10b{d2f1({eg!$xι'aVr#'WSgˋ S#Xeyވ5ҒǼkNBr3BآXFuo5㍆p6_~3ޘgFcVvc T+R\2HdNG1)kX(fAA7ـc&uw]%NRT`&u2bIayh[5i: tOÑ4k'X" aNWT@[u"Q&Lr{h:F/]|ZXG'P&:I嘖H3g1>H5!: Ɛa*{6щ?>}Ś-@[jŻv{E8RKNX^L MӞxI.Rڸ$ׄ˷^ &Dͥyy<USsijI. y4N*C=;/ɵnTz +[f&h+]j乿V@o,\ NorI.m]pIhUe4z\I%"?gw^rNӸԔ~) ;k ZKy/`!A&eE}VͶ&zE05C{<USsikE+W +7)EnQsb OWe&5Vd.$smrt97!r&IG$Wͮu;˰IC8}``Oy3ۇأ6>OC B5y5cFJfF౗kU˽kkuW1D;eOwbL;|Oܪ<1{S4K4${W{agNry8ݫw3@]e=Yτ3ceMUFA:(6UfS>eA4(aăq4^ +endstream +endobj +60 0 obj +<< +/Length 5711 +/Filter /FlateDecode +>> +stream +x][q~_`hI<ر ! f43vi%Rn}3;gR,~%JU%rB?+8חӯcD%4fQ} >a׌7j7ooO87rowùD[OwNx4y:pDSrGs#>3JwDʝ돥K7__[?LO? ?3Qf2_Y+1e-?f%J^JPa^S=6O-_-B,7evMcMKcmuӅ4翟 +U3,J.N?bBԄ+ΐtå6IɥD$D)iE}+WZR{jϛ푠TuSsG€%cO3}i~KF4f%ByN;B͔4 +!Ҫ"ʉP3Eo7+۴ |Sfi8z.~aq+FI`):>Ͱ.!o^%?b}X~=/t4r :bŔk{ڸtD,z_ ^1;(J4]},'^" ó^%g {q8 (;TO&{6ٕ9-{y=}o9$&p7aؽ dXk{ +aEV + )XągMj7Ӎ[C8X~H"+Y7)goLgQA`1Q'v/FЂZ9V7yxov$̳׈l zalVUTRɂ^AyY3LDў wqtLXs@8D0fҤq ּׯ֣" 2 + FUG:Gf G}f|(Gbi=J +R5ꄈ#mKa|˙Uz97d9FAh X;ON&1ghߴ\%Ι^ۢPgu +B%.œP-*?7uĵmNGmN>Н:hF2TVL?ZJV)>\iaeGRNv oѓ$$]D^WJ ߪX[atv^;GZ=<|% <\)s~w 9<*/lҋwd.ϞpV.O +[4KT${̺K<8!O<T>g}]{@.OZsr3\? $Hk5xj~}EyzzȠQD#;CլTR\J=$) .kR\JKIpɗuIp) .%$R\JKIp) .G%ԒdhdowٕR[J~Ko)-Qso)-k$v6=˹ꏜf&*CDېx8^DB pLC$$DRHJ#$DRHB%$$D֡THABI"ccME8+E::0cq]lyuv9@ o"`uD>0 {ڸtD,z_] ^1;(Jt.B}᭣qЂĒNkˍraG=qH !Fܽsy0+A+)s7̇3ɨDjc1ƪ+f!Vn@30ϛk ͷߌ7&fQ{ۑH2^+cJl~e)**d5@5tRkE7&zƄS Qsi^gO\cȼ{I~Zn+,$OۭvUfRsiAB<_5X.9h$h$yэ +P`o H*q}9nܕݢ[;;o›aأ6<$a I5Jųsm4U2\z`c//V$<ԆkCL.^[kCL>m0 9{|L["N% Cz '\U"[S4KT${̺-8!O!6 9,7 ,C!pc!:|= + Օd*9ظ=~YK% FbUfZM111 +غg~!mx GWxժ2v60d0Akp6!j. F<{*)h Jb/673%3Vgߘp=^F2w{EmžaZyJˑ7r0d pѦ"owY +ήd4WzmZ-}6s~〵RǩfTp{ͪN$C#HJStSǔ3~ن4tK{܏\ :&r3WfRsiAB<_Pw;G؆XS 9:c?[fTwHqqyz~]n9h ]eLjxxSH,XNG]9c5yv]6{p}@iIљ}i 7 ՠ׿R#o'@{+ci6Eq~MË(@ dHwz nyWj滄OkkͨLjcFLRNiHpL"V:%3vDnbc"V'b9D/Q;iO|Dܭ1+ZIvmLj.>PT1 |)aay4Qx筳2@]E? +CsR$r zʇ4‚` u~O +endstream +endobj +6 0 obj +<< +/Length 5735 +/Filter /FlateDecode +>> +stream +x]ˮ8rWoq3@ b0[[*h7}+(`2/q;Đ#)2ģ WlTiַo]VSm?bPi}J/ԮF?_qß'?^/j~ ~\~\~qBRr߅"FNb ًu7Kb>Wj4;R0~q˿`#V!Z5?f~ISoWRSal7{kJUR]__o_ͽwwG1_\ź?6J)D"s]2nc + G{JP#p%t# ]hN'm% }U"&?nMUn/')),J)7ǒΣ IױpiRVTpSRuM I22ڠ'd%MHpNT[J?/;*qMۭm2m4:4fq i V[10ySe.ML&ʙ4 +-k[?O`tSfWy NaqkFOIn K[`Ac-"Oi?/^WsL2€'HtX!}=_t*6/r:$E^nB}rGPaׯ=:נ WiS$0i才b`@7Wn~xar) u?$쭶XUxUQm2޼-QB +pCNI Rt?Ra#o6"*m FmTB]Æڰ;!wл٪3ThB.*QU~˸#D{V{҅Rƚ̭*ԫf%g\6V%4 +|@eWectvW%ܒȩ"*؉q^!sc$ jgYL*`ǪŜQYhpB=>-+B[<yQ?;Ή2s17nv ")ŶK{us},oLJ5uwHsƓZTXnڄK4UVoV_w<៙n3oĜ\}<\62*,ŶmA1Zmי@-:͌w1w Fr"<}Hq L +WtXkSs:WQ=l44톟}}˪Ǔ-݌ϭXv;܄@ T3Ϡ  su0+a)];GJm'Ko1baց"}Q+Yy2uıŇ9ax[!i(uR9/M՛=|?H 7fQOi]qOg{LLͅu-XN.5q[8gטpnI- : LVh4|3sK"S[v h,7H'us9 /55 Tw4xtvLQ^'3Y{ <{ @£$C0ou֨6bx7tJrЙUYRa#O"|q{3ܻ-V8]XCz\ ͐y@;l #O7`Xv^lGArOs(\10Gx6E:cCRay +foa iiҀ v5,ۂ;gXv+=>ÙU!_`\qq8y᷷H(gsynU;#rpt$ cQ>ҽ0LM{cc. &q*3W+&kt{Ek2΢o_6̋`mBT\j^*{Ek\oCq;ǮY;pȦ۶=h)]g´~׭XۀYOX#[-{\k5A7 .o(_ Y@3 +ގp8\>x*Qnjk4X ź\>t!&&Mmaju_痜>]W}چ2bKռ4UTx !h<0qD/ . |e4UVgj-$xoorz96 ݌[ +D>; Gs>kU@z_جᔏvι^S>`ߟ(}%S #q>ZE:U=F`ԛxzҒlwSѱ]5I͕u("ZM +Ǵ(|ysP.VzZ ~`w N_h'N4Q*3#֛[bV֘&Bj9^#hzq HWp{ d`gw$4|!sZ;%=!ŎH;3$D`.SpaE =gfx Ӆ;ɂ8]L XM():p,gpش^1^h@d:#EXom@%PN%rݵފ$ױ6xvY|h?11Ba-`Z(Z(Z(Z(Z(Zx k!0E[{eނWݠ8]1 +|,ӅOE`H0<cq/G+ydq؄HXoluDqDCRsmz#hñ `q׋u=8:g=] >fJ:&mmp>*2pN'BXJrjӰLλ%ݺ[^]6$TլR=)[{l濏7*SVm9r +?6%bO 7ikr5k$G>R`{-SQRU!/0D[SDܷ3Rg$6xM?l>ᢵKat0 1*Λ;*}s}%}bmhJ1(-uߗڕ˪kN:1qMHC%F ?*EtYu2G׮6wnr+Lpͩ鰸d4؛)q^cYuֱg7DPb{-eϦ,ssX)YJ9LT-!dKK2Wڅݪ(|.|.|.|.|^BrkiK +o{+Jmd+JKmncd;+ge]2P-7ŽM4_*婋aYf^16皂D хV/9 tg [d+J:R2d牃,f t%erGe#WC|J(gd<2ܶ t5H \A,g&xXhe+AGN8 hش^1^hAd:#EXom@PN%ݵފ+4=I3rAGݲ-E6yC7dqܸTDw>(l8wXsDCb; dH;,/|cAK@|xb۶K-ŶLA{if2(xK +}݂ +(:kpV2\gm5¡(l]8CQ8CQ8CQ8CQ8CCJ"AK`.t<1U Xې4U~:{)ʞ<-mafQyǁ>^ +AcDi\HPނsLoXCJ}x6O׮tY  ![sPVpskő'd x2-",țpfb>;d*`SLMo_ +#u1+w20 +_ %Age`>\8>Yv^F +ơp◦ *M I&|,F55Imz4Ie +endstream +endobj +63 0 obj +<< +/Length 5458 +/Filter /FlateDecode +>> +stream +x]K丑?P4ߤw 4c]I=)Rr zH)DCG+u[QԵ]~u*EW#l5< F݌Go/w/o]~~?ZvaW{sM7x_~j5LUK_'J)Ev VghKwuxN&qC4Uhj#vx5\|om PBKEřVs#~]_/ިMZJC/SKi6ZK]Nťl/J + 3t%2o=coV?QKLRI6OӉs?lт6\I\ !,hƌQ԰S &4/STrv$ +8^B/.7}Le~ uvu;ZѶq[S1Xb w}W@ՊyQwWfs*$\.m=S1>Pk;8̅?-}z9Ѕlvi? ?];`G=X{Qnioҩv*c6d/c+=a嚜3!>|5OwGMMr0nuƾ#hpLsK&2ހCF8r)Y_kcr(2,6pg;g\-ѳ'0Vę6]kE8~- U;*l~s_A"?|=_}qO_eVw]kf~LJ~?$ ~@$H 8)字Vgh}jC麱:']Ѳ8[ 8řjB{zxrNv?>UjA bbubk 1@9 A P 1@X 1@B b !1@0@JLWfyfPAПi?ABПI#? +?y&rT0cVj;Pد!I0`!!DD"D!BBvBB"] !BGi !B!B$ l:фd<:6 1@|-IeUY1*z # AF2Bd P2BdtEd #!AF2BѐFLK]WMk+ 6|+"Sv8B"q#D%LG8U8B3!d6òԴl;#3G3L^Rcj{x[+*? zU @1K?$t IwN(އЭd뢉։ tcSج'%?:>JB3 8 ?A? ZBcA.Dl?}D"#>;7B9 &k^G3'jg3x8Q !ρJg@I, TXSc;Ǚ} xm"TJGJ`V>ɨUZ`zS1G; 7فZ7-6r.d'u ֺiPVN\ڔ/9ǻWYgvp˜zӱ9۠.V :0y<0_=t& OTdz+J/ci5ig{4VLK7`dܵL3 0`8(T|G2͐ˉ +)"k8+D4͡r'sdPRKid|&^'9́QH}48?=byYzCcA&.R.&Qkq|("#@XJ*]8 q6Oq6[ q6A le8 q6A lg8ٜQ:՚(;,&` 6I`~渷8BY6o9LΉԴ@Dd@I `@Ĉ@ 8ę@ U8<lM!a^ lDRZBuic+\6FiF: mj}2Cl؉#wDUJӫwLyErt"ƹ2rL$|"CDv[wH99WO mMfNLKnt؄ >vJT)~;b6ķ8K}*I8AJiR#8qWDk˕ C-E/rw upB#EsM0NPNE84n++ޭ3+&7⏀8\8HӝjVk0%+=eq>*=\SJKLU 'и'm{ku?bG"YHǻ>kfXȲӾ YHUB >6KQLHUa-SrMJXՁfL>nn4l:L7an;I[e7VM U* yio:IM>b9 F=4|laٛq镠ț*=8q4юV1u6i|Ϟ1䀜Ƽgɕ$Kqw*.DRH_[/@Cʠ!SԻdϤ} FLꣃf,H9T- 84ltu\`%[uMq5H]s{E5x |CX7u+ᜤ9Q}2d=YAZA.K~o|G_?k#%pM}| @[[BFf.gk20 lv.5 lĨ`ա@RS-$l/ 劚a&p'pD:n\l듌ZEy 7s|-f]` hyx lrֺiP2,p%\qSS1Ogi[PI!D+hEL;8sD8fq[`S/F\ssej)(|`R{SIfw'LG{.܅Ey-\>]Ns(Yy2}/p_ +p:yc%xvq7dپ$->>aj˚?5D1uBx;g!n;:m^{qs %C\iw$ _& y坉>\9/M"zK0s(s9nn|<ˇ3@]a#,vK;6)l%d+u\d*x-%k 6Wuo[,܊~UmEMH\[ Φ|?` w!qGHw!qGHwąXpGH\eQFhezZWn4ߵ&+JLB'`?!.ǜg~ƨ4X|ZJ{*Gi +MNO@?Xt.I< $oaQk{)l Jf)Њshq^+Lo[Uz8#y帏ڑn.t_+ 4քuRZ(X"J.Gm_~D`KFڟQᤎ6l*$X]͍E,[Y[S,7ۙeOΜA o݁_Oh_kr'o{@!JY#MXQ)d忼s63dp72Y{^c:7Fa&Mg-Kc([u 4}@lb'+rb d!U:5`݉8CN  ^H-'=G[{E.%7Π̙fMj8>GQXy0U{<KR> rCjlLa谼;> +stream +x]ۮq}2`?I ! f4{bN~H.EKٷMlϱT]VI{?UB幽~3I6b5!1?N T ȷHqU:᝶_{8Ͽ~ÿ~ۏ'z~?88ҳݿUVFAwFp/݂o7"nOwӟ mⷌUmMyښkQsگbpoŞevv{,_O&aTZ1- ^P_'v_j~t 'ZcW(R݇qoͷkoujjMw&L_|t ~#^f6{m{Eq m0-:{i;V&<1AHN~'1{wߌg1h_w>Ѣ?RTnvh*:.N8ZK TWh%":WB3RYNj3ƣ"4ְִOg+ 3^0dl7̻ofܵn6*xUkFf]۽5u } +ǟCL) +_Npov4,?=K\%aT9%u-e?LTr8mbAQ:_QF$tݨ*h rKp^4ϏwɟFہݛ 9mw~_fgĠK x|#C %`dnk6DCw߶cj~I= ̇_d[V ăhfVBY`>!ɦ=r=&Ü7pJ3}1|+?Nh~L[UKwL'N'IFF m[ 셶mC9i/,-1sQ.2]Cૺu(E=>YGFbe-@su)VB5l@߼H"MOM۸+H:Lѱ#BJ6SPe|9֯7H(:fx@Ar~gF>t?յIɆ̖8 9eZIO}!U*!a*a1dS~a +A~( fp۬~A_N 5ՍTCIdLHSzmfKXZ~Fu6gU\68W{ Y%%TM6N2;~ѝ *g7/w*ޱ3 ˧2o;/uSk$a¹H,\a{{b{sWtI#b yumpH0 Iw)I MRx ş1+k3Ƒ=ސi>R%}ofD$}/lTf$hS8+vݛJJJ^K1oSy.5~f)Y`hdi.΁s ;d59Jgjcl0Zxe\鸺֬_߭`mRo 8K]zdׅKz(zpWX ^ܙ!) 8u<\HBQjT*ʵ1\vI7?h&UZ6W^_ _R.Nf:nxg;iǹ$_<$"rdwBlpLTL<[<@Ua}`fAw[/i:m*ivllXr0#:B5}%:Y߿|"`XU3_PhtKϵ;^7mu~zUz?T;`_B2i^T[L?˫LM=f7{^BUPp4>)V_j]=ӫ%f2>f'឴Jx:efTڶv ҫ3v@onfˠ=۠wF EA@+1꽯e׆-:(&*q)MMa~dgm\'ג+{#a2̇pcSY?`H)Vf $כ!y (k" 5 f>3 ݀mM|@CċJ,6OA:1*2jjlDf,̀Fi>z|J֢|dAG3wZyW$#b;`Ӫig㨴Ӣ6xMw ̕Nү7v raxRl 9jqQN3xo["{c 4ޣ׉ CY_V'uY_Ve5Y_Ve5Y_Vg]ͯ^Vg5'=ߣ;Bgw1^T r.˳?->nx2@Xxa1_ou@q@*k!ㅞ{x-X< h|0c=V.1dË>ة~Xߵ=;]mX6֝&u|o dikz1H>46'nX0tJPxj_J +RM~dbg^lH(ӑER3i]xQ+boj#*.}eLk5d_u|+ ds?u%e׏>^qk!;۟9_\EFE#} T7v%i]Gsbvw',{GiRiƁ3W/˽13[Bq潚)R5hQm_Y+IV\yE r#,,˕21'5:Hi|y3h|%?ɷHpjX@1#29rn*b1.C6sp|P-5ed-"AZ +г{2oAG9@pQr4}J\&9Fp }=4I Q]-?\<{pRҾj)fÇA7Bo3Xp7b"Tܟ׹Ҍ +smW&r~]0oky>B^j!O̔I2uuR.ʞ -7VWMm/qeGCm;2) m+^ḧ́iՙKۑ7 +(F!y.~1w`uՓk=obq PE{a:s[sa:sa:sa:sa:sa:e:(m-K00Y!R+h}R 22]TK9c& (AtyR-ԫ3w@_y+RE{En&g!!_YJ :F+|sS!\J?{6pWﶥgf$8B+2" #EsU eBa 7zStܴnx8\ .z8n"-GzcG;{`؉kv\ {t:~a(hH`h(h(h(h(h(0V{FCfF'=m9 pbaz'§"7 )@1,D]ߐ7zOgjILbbBԯ7@|C:8R\BÓHoPmor6&8>!z[/K]Ԟ#LqA#"4(3b2LV>>M+oq~rجq_-}+4WnT?Ń}Ѿr/toLW7˘|Y:P &7f&GέE+I - AaԹq+,/|Ѵ[y6gb$sZ~ѮG+Yܩ. RnF-d"W sVZ;f) JC-]ְRȕ.rk^TRaRu\* +p p p p ppC7~5(y+-J B+-.-J QMi!ӟ.nSF#cPd`*dkZ \ظ^ҥ#]Xass_\RRP*ӤGX1hŮw6w'!J6W=:{.Jmn{W͕ns\d)J#a_M>ԥNsNb&NQ\|bp4>)VW:G)YbeğXUn5=i  pOǦ +B@1n3ZzueP@5$z1 +H^ +mh]u%\Cp ZEP$uPEPEPEPEPEV**UIVխ'!R+'h}R .vuR.ʞ> +stream +x]ˮ8rW\ ߔ.fl4`T*h7}J=)$2_ETW#NHPF?UB幽~3GldjBXcv~o'tv;m7ݿßߧ6=ɉd2C_F<]h]u%\w PLaMJ{ +58mC]_vq&H qns})w&1/dC0<wB"'r wPvӆhhnv \W /gP l@}xsSͬp1Jh"k';ٴA?tSLGWsVׯ_I1e|STZ~)Jο_~5GtiVIO8!n&!!`/݂o7$ڶ 립\=E0t Թ +BV^0g5OeQZKv5߯ygRE"֩܍m +Bf/*+k3 L=XCz03cx6/+7{& +H7`ocmX]ll |q::c^֪nN$RFsמ-?߯b;6{y JӦQD 7SeOj.^wa_a2Na޴Q h]6LD9~vS2g8텶ޙ#D0a EYI@ loB0Pq?s˜C]Bc;/lwCba[0823&)\nxO&RI.|oH^p y2þ-s~g.>΀t!vu:a"), #瘏}7MBZ>[B+7Q6[TP3d2yv|J֢|ߣxx*uD>Sgc9|1Jۛ 38enZg7]hdwڌ@~y :8XӾãދ3xo߉kvmubPK3[f_K3i/4f̟_ 4ߧߓ{v~g^1^4ڻz'qO׏/ŰuY $O`bm~y* Z:Bp =wx-X< h|0c=V*1dS5 lh"Ou!mkFѺsfe&DL@)@ ;i?>ӋOVZ&+e]),BYE՛0zQ1VۊQl5df>`{)]xQ+boj#*.3 j#—֘6W\I~U%릷+)u}.0ZB\w?]sֿv-.CG#nT3^Km#i]Gsbrw',{GiRiƁ3W/˽13[Bq潚)R5hQͪFpa&)ntk$+Dk.|׼JғdY4<_Er]@^[ʷ@5,N ܘq97 +x1Ә@mxa9t@w>(_s@2 -Y^н9i Уm NODHg8y)|>I(GJe5ܱiC_P8MҭhC?eOi;.oZo %" geu4$R}[R yb4OAsu)VLhhujj{{'-9:n鈗M^h^Q$Bm&LK^ڎ}4VD6ƀ>>6=|vXi}Cweφb- ѹغ ѹ ѹ ѹ ѹ 9e}DQ%T7ca2=Dv^{e3OA6su)VTgl.u*71?7mCf*+k3E LPƗj +0q3cvx6.* Om6A59c9<*3V EYzВ"ӷ $]%]jNtM59뉓0TEINQhŮw[HI1RwQ#8f/tXI#bYYG6\.H9J>6?^] )ڌqz}7$z~~`ZasF=ttQ IDpWAy҇a&⌌olHg=nΪb&􉦮-O(jn]'Z?Gbe9Jɺ+{&:ިtCtIKHx:efTڶv ҫ3v@onf/!Q(wFR(\oP@+18\{m(p( +" p( +p( +p( +p( +p(.'5SޫT%Ọ ybY;DX枇KuLI2d]=y[-fsTD:Pұ'&BJ6SPރe|9ׯ9X8:fxhsoD fV(⁐)c=PVpX|ex肳1ȴ8bLWc&=r2Ep2.Sָ&W#Nlςl5afܻLa``gkAG30biwNbag.ĝ.7B|,e8^U'o )ڌz}7$~ pZaFY$tq YxWy2O r3x3͸kG1}Lި?l].+J;uJS[D6ĭ.'M d?@ZebJ$GgvA׮Rg&_ej{v?vzt"_L4Th=xwM*6e7^,E;7x? .wo<6crvYYi0H4AR?+7]' #gfeX:Gi.ʞ-73@f۰=iYY pOǦ +B]bp/fLЛʂjH|c,c݃,XY;кJ56z/++/J`*****,}ee+qSY-a-C6M=bem@~y b\8-A^poO8 _؆D<Sq$3x\Vul^aMSnF-˥/+7{ `J]FjDRI"?bH)Vf#כ{!+"| 50&F2 œMMϋ@Cc /"pI:nܘ0wN цaB(BXN?<^q{b),^ + +WQPPPP 5WR\a?Ca?Ca?C҅P; + Nm#l rHY|szjՍ=,7Kkhb#CKP%dŹBIlx=q^#HVIA ?;Qs-܉5DNDN< p'^ +wbj +wzʥk)<~?3V E,04iZ4,LVPm.~6s~Xƅz yi h \zA Fk6Mߜ_7-K]iTeQ_JYɴCY㽻dդiyBwxX0b8n|_O&3ZbCePJINgp՝~ii1>=<GP8phJ_l: JqIk<0\VI$.zK9;fdB#[!c&W] ^[h\4'$4 WP_9 4~ڃPgiP 8Eqaj¼44j0E!{0+mf*!QUjՇZ/gJys>Д 'Qsmi?U懻Ywqn;3 {m7IhfB!D6T8܃ΊQ^| Z[.gZW]W}O.ý6k0QMMlj +' v2:g63FLAVt7çà?3Z 3LNL^!fR+3h}R *]TKb&ctlt g!aj4ޗ_J(2f`ɁBXQ?!R+S?h}R v:Gi.ʞVaX9*#(nX3W!XY)\Wgzo2׀y@k<7#]\l ɩ@:@K uΆχ-FMa>$a>,c p^1^tfz'q|ވ`;,_Ɲ.7B|l +{<' )ڌz}7$/~ pZaḞtq< xWx0Pɑ2صHG?`t ^!#Uɔ͖(,? +endstream +endobj +39 0 obj +<< +/Length 7075 +/Filter /FlateDecode +>> +stream +x]ێ8r}ϯ(-E=^3}VW:}J])$2oM{G '$F(HQ?UB幽~3I6b5!1?N TȷHqU:᝶яߧ8N~_tn?ѳ{`hZJYietgB-~#{>6=ɉc2_C<]X]$xO,-}6kѾp>'Z0ղ%N .**jS>΢*E:U'ZN( FTV~q: i񨽈d<55aYlJ6=DÌ ?Y0Mmh>w +^՚svjneMv_'+㭭Sp—> KOaj9c~It1puIoK(d#!@a`P6qνsWxT07*]7j +58gC]_vq&H qns})w&1/dC0<wB"'r wPvӆhhnv \W /gPW0ٖv %"YbD68X#OHwiσ\?~ 0 R_~5/|_4ο5 Uw&tGקN1WIF m[ 셶m1C9i/,-1s'Q15]Cku(=>Y€GFbe-@su)VB5l@߼HRMOM۸+H:Lѱ$2BJ6SPe|9֯7H(:fxLqr~gF>t?յIɆ̖8<9ef!f^B/J($L(59[b|GW_KI1~(?x!$P}C~b@yL2ZV[[$^&\@ƭ5>z3'&95isn4l0&emT?~_N˄IAii; ysf^h{eE󺩄p.i33>WA` ^漵;-w/lwCba[08p]hB-7|lBORHxoz +px˾b5s~g">ʀtJ6vu*a)H<1;QTIiNtNޜfY\YIwJ F494mHsV-Qᇱ-He––!%x)l–(l^EaKDaKDaKDaKDaKyqj +ObY+<“(<“(<“@$ +ObgTDI<Oܘ2)Fƾ_,8c{@|[$GnŢŜ[m3[6 Yq.0;zLEO_?|a Oj.^cvqmJ +J(`Ea^R0/PVyQy%¼[z)̋m)̋g^x9="x?3V E,̼М͸?AQ<&>QUi\Lm "\k Y yٗL_VȷC92ntR wמYĐ:UUYan$[]k8/W|x 3s1RWZr-żS(ٲRfR!9]Ʀ(czpWhwx4uv^#Ǟ4vQ'E D}hvK(8±kBIP8Fm.|uuE*z̍QsuО(0C\B}q{7戂KiCA- Ձӈ!Cs¬>Fh`{vZk'? )1&1ceN,NPqAU3g/= qF˽c^(kaIż +c=~dsd"⹹-nMk?=_*CU3t>8߬:lvQ20w_~` G`EE,V&s?o>WWOzz@Xd6ᖮ$'nfje=Vg`p}qf"F뵫ܡ+淢D1"j*r)W~1Iw֢4gϋZ +'r\TKgz1:L@#ܓP pOǦ +B@1n3Zzu}5TC{7HwCnh]u%\Pq P_Du)MMavdg$m\ג,cmN*LuQP'dPN 5S~DܓܗqsT4w8Z+]W(URh+=|71Gṧm >Ubemp^齽rH_&rqtpޛ.t'۰|cT[>OFjJbN顆>>}>qCm& j$a>,c p^1^tfz'q|ވ`;,_Ɲ.7B|l +{<' )ڌz}7$/~ pZaḞtq< xWx0Pɑ2վHG?`t ^!#%͖(,xH#4JMsST# 5ܑ + ^yW$#bZsDi43qTh !/s:B se7f\׊ 9Ǻ^ޟ{[NG\@hG_غtn_K7/tnZ拏>P;G+;B;/|~|(mV&y+/l3=P(h@Ԣ=^нoA Oarύ ^I|6tw ;oF uIFrNFrQ +r|=ЁAl-sgQsǾԌAaYadV= L?]hoHhxj_J +)VFv?E13~c/ aEƳ5df>`{)7wrpEeݏ%7325^3l/%1%\I~U%릷+)s׺隳ŵkQkTpZ=AuZj;yN: ۸3=E=a;HJ7,9}\6^(?Lߏٜ3L@ˏjr.xF]LB7x+j{aaY,=AO,A[. uLEK|T)ɑsW< vZKt.o)54 lYR兞 ݛ6x "=LT}Xc(qrd]VX6$݊6DO\fj_ĚVZsHJJos*x + Zh`])RG߰PqVV^J3+*̵ \uܿqUz<1S' K9J+{&4:X]5HĽțp/m({6Wgz/mG>+Bg@C>|ށUObノ;2gC\ l]΅\΅\΅\΅\΅w2nkԍL1 0yb;/ս2' 9J+{*3ha6!?3W}Rzu(~G +D1;S{En6|qy "X,=yhɑ{Z \دB S^ .vng:ӦhkIi`"ӤGh [8Hb;F- $iy;H@(|yW:4W$1^#}.fqe%Eˍ.bem8~y=^\0-B9^so>Q( _G,"D8䫠UgeMS }\%'YՆ7]' #gfeX:Gi.ʞ-73@f۰=iYY pOǦ +B]bp/fLЛʂjH|c,c݃,XY;кJ56z/++/J`*****,}ee+qSY-a-C6M=bem@~y b\8-A^poO8 _؆D<Sq$3x\Vul^润9ZK#i_jy QץX3CKo::y tph^Q$Bm&LK^ڎ @ m?݂{X]$xOmeφb-K{iO`_K{i/_Ӷ*.XA_^'`ksu)VT Um9>*+k3E LPƗj +0q3cvx'4nOU@zljx{fXΟ}B,WBnGC,Y"*0yeU Gk!$ƍX^r#< +_Nﶫw/|Vnf f"ԈhcEŐRGگ7{CvW E@H6kaMHe@'|e*^Dt1a.[ok +endstream +endobj +21 0 obj +<< +/Length 7177 +/Filter /FlateDecode +>> +stream +x]ۮq}2o"%vypr{4{&?ZD%#UwĪUXKgިFЪi ~?TuBXwۻ~v'bv;]?Ou}_t~?ѳG`?NWJR vVu])g|#<{4|<&n(U5zФ>[۪n F֯jpoŜeqv1']2ZF2%4^P_'v_~t%:c[$mXYio˷[mmD 3\\m<0*mKEoک3"43 !w%7#9L^چˈvMWbծ{g^xVMˇ-Wr|2Uη .**C!ӓT%)U^ |VB1x%>CI/+b8xtI{ZtADF2,Cc'#,ZbO`ɂ'Զ3~nTKVF12}50ٻlS0Wzxk)$|L'!- k0s 1$ azڰuIokw(M8mbAQ_QFjV)s/UAz.ExK<4 dn ]o"vX$]ℬ#Cb$$r"CٶZ p݆÷_R?C/a4+>G)9QWJ +Ek~4A>PLZ~ӧo|oNb~&;Mkt[QKL7$V%k'ՌvhmJso{]<-1}.2mC+u(u^#]+5{"컌̈hCNj d!XY1[o{GvV U@H6ka͋He@œ]MO@Cc -"ܘߟO~hJ4Ѻ + +jKBHa?`RP~(~(~(~(~(URSV]~fn@ +DB(g b-ኛB(<4/@` K)`!P6p"91,|AKE~LvCvCdϥTkCmuPK-t-C;C;<b %5t +r'gNf,,KaQMsISglښ-ANyS.Q.luBB!%x)tB(tAEKDKDKDKDKĞ"jŽD 5(.D!JD!JD!Jx ](U(QDx?zpq/V?eE|_O^!'rŚeofH60 8k`vԋjxf娏k7U*}^/zA b ezQzQ_)ԋK^CM^<8IZJH2d~lAb$MS+m.~&Fs~nXƅF?LNӶ__CYv2( N!k,c/J4d~=ب~p:U*qpRVRˉUJOQiXǜW35`VWL_0}⽬+곙m+^?#F?HDLO21YkywѸX|ssm)VLߛ~DM=i_|¨pϷm.){.k^η@5$x@/o F&FDuӕkpu{bpﵾ +;'~|h:>~a60.sn|]y yŸQJx=x]!Z+3^h]R lTnKbb 7!G塼m z|:W%Ru…:uv >n Y +1KFyok؋O8a5952*Y4C,̦>JKPw#)5Ϳj{aGlxŔ5ϫd15RGc )աIpN1^hlXx᳒RwbHV aed&ˍz?^pgS )ʺz= +;7b\8A^ro^j8Clzb*"F.'>TvݬO\HvcۯϼE"$ˍz=廓 be]@RnyɋG[.VAu Qf7!]Ɨ!6=U~%fդo0:?_KȈo+i%Jv|J4hjǧ%Gj#SZYx3[`Ӫmg㨴ipR`;^.mq0yeLN׌K kuɼ>W_YM;XnףW\Fk.kECQbhHwUlkUc8m迣o_n"z ׃qzrcKy Da~Lǖ9fTm 1,7otc N'+D).\WFґY4<[Gjm@ބ{J@5,N ܘr97 +p1lb\ʣ;`рUdYHKa7)3mADLTyh21K\:8F6p}I-Z7wbM+u-޹xuaDUUK-8>߬y*: + Zh`]*RG߲ _qQVJ3** \uB߿qZ yb4KAsm)VLihmjs{'-;:nMo\Q,$z|]&LN齴yK+xP m }|l1K}MOb뾉;2g}\ l]΅\΅\΅\΅\΅w2*"TL*1 0yb;ս2% 9Iն+{*3ha6!ϙ)ʺLB:{;G[&nQt1ksorЋpy"+3V IFYК#3i,_f}%LڑRLjuK'~Ҋ8L%l ъ]0HKACbТAzg +gapt]FQRwŐRGʭ7c#su+*| 512 :.g!!_屪z,ZY|32O#mZ9T^Ou]?!6Fts>ŸXkyfKr[$%R{R-QB'-"nQo\R,(z|]&PN +7D^>6PG@+1XE.[_8CօCQ8CQ8CQ8CQ8CQ8yW+eU×ĺXw<='^<Gbe9Iɶ+{*Zma&feqL'YՆ}' #gfeѸX:'i-ʞ-7=@fڰ=iYY pG, +|}wϷeuԙK[ʂjH|C,cH^V}ncee2=<UXY l]XYUXYUXYUXYUXY,w n"6C> +stream +x]ˮ丑W\oJ@.1=, /nny4ӀC*J|q.)R +'$2GgbިZЪn^N?$Y֘nx-D\nxZ=8Ͽ?>=ۿt>ON?wD;_oGjU*eQ m(ڝ#{{Mhoejh"Em)kgERXxOLMxèTbZD?O/ڹ^hOǮQץXY[ޚo׼Ǜߺ{cuԚ6Lj pNWl +B 1n3aZzuvͭL"%ѻycc݃8&3.k3.܅UObb2gCK CEJ ]-=9ࢢf;c/Rj-SuJ4Dt:F6whRY{tAv75EU{xEkX{5tʰq-}%UC\q ,vyM FjH̽K`rw5ݲϰݡPpdeb +vQ@Ѱ4~.{=旄!L] Z}2QA6hf.eclG;G|GSk}uf]}fФ˽y!Φr:C7pLw~Ͽ~;'MTJοٜT(GN/IF m(ڝC{mPNnn m/3{ss̼D !i݀:WAKu,^+]I9J+kw{[w6 o^$ba&P&m$&nX?ëR-ԫ3w@b+WAN^|;-w/lwCba[08̰K]SA-7|< l1+k3Ƒ=0+"| 509gOG$}OljWfC'hS8KRIinF]å1ann9E6 f/~( ka?ZIgVq +D@)B?AyP R()B3MS(1뿔e=W 2#"n0#h5w܏n0M㪧}n97jiTM6(tBwh; +ݡ +.7QRPSNw`Q( V94i$I-s`6mɖh+–(l–XNaK<^%{%b)l ,^ +[% +[WQ-Q-Q-Q-Q-ey'L7$“(<“(<“(< O$vFUI3$ύi #hdWa,OhכW`AsȭX#bͮF⭗`^Ȋs)d^DוPUe͎>@VIA L¼(̋_ + +0/ +0/?S{S/y 5y /[DXg*H?$ 3/4g3uix-̹QUi\Lm "\k Y 8 WW4=mXuFZ!_ޘ9{Ԗ"Lyy=j=9*+ɤ&D_ZW ^qeSFh0FCI-OpřQQ)v2:Zjz(R\wyNfnxgqY cvn{m7Ihf{B!D6T8܋rM>| +Z[̱Dؔ*eY+w4ηa6Ï;/#ȼzAs5\IƏV .'Q鋔CqFKkR0%Xڟe"\_0Zytil’= +i YX8 8a?7^4d4 + +~x/ǘZ4嘥gr lOi:GZbehuޟmHIt +^fF{mHq m@-:{if~;!}<zX{`$c\oxc 8\wm(_a ߢkg]ӡ9ޯn3m5O?ЍdU!F֕?Z l&.vy>)V9|Q֥XS`Oz +aӁ6uUbemp^齽rH_&rqt176]\pnjr.l<@+>Ը)_H-_=i7iӏy vGv: gvĎvy FU]>RU= +_ +;UD>+U&&a;/#W5)\nXx?bH)Vf $כaP׀ EAP6kYVMg@#|ʋ5:n-zR[g`~gB8h5F5a%#1e`hY 3 ^f7 (k t~x EMlEk5>)V+y\^beT-7@fq.I[t̨p/ݑk {6Wgz/ޢ5D7V|{0hE𶢵h]u%\h-=sZY,yV<+K%ʒg!H%ʒgeɳh^eɳWXbZ(MkVI >p_t!ryTZ+UQZK~`%TfmbCH@:<>gO*+k3 L=X'Cz03cxi{S?nimXMNByǚRS?[-$U͆߳~+h~&}uhSI=|(|ylW:ٮI\Gx2/"X ߗqed>$ˍ )ڌz}7 ^p y4ý H7 pz`b3//Xig]t$o0:?_GHh ,eGKjx־"4JMcST#5\4+ ZyW$#bJsDi47qTh %/s:A se7f\W =Ǫn^ޟ{[NG\@h|GR_غTjR_K5/TjZ>!r~WwNw1^T +P 9Q)O ~ ^?ѭ%BO83X!Zsk5Ƚ,WxXAO,F[. uLEK 3R.#F"& vZKt.o)54 lYRMLPr8=;#ypky9>PqpǞӆp8͠[ц(~].`M+}M9yyD%R~7gA7D&K8I +V1J\iFuEy'9.WZ5<J/'fJXd:G^beDXwҲ!6xAbp/f´Lț[cZHnchc݃gۘ;I571|8Zl(ע0 9 ӹ0 ӹ0 ӹ0 ӹ0 ӹ0[WMUI6f;;/KuLvI2HtuR.ʞMn@&<t cys&BJ6SPe|9֯7H(:fט7y9]Tퟵxy"+3V EYzВ#״ ,^&]n@tM59퉓0 TE$l ъ]1Joa ;HGAG`bСAz'gapл.#()ZnXx>?bH)Vf#כ{aW E@H6kc'Je@(E|ʞ?+hvAnnSM0Y[*8CQy.?BQct|? +UQJ֥X3'%FUkpOZE:/3½ж}8P L^齴zs3{P oB|{0B"xbZW]WP2kCCQ8 l]8CQ8CQ8CQ8CQ8Cw8j^* ?f\d!ryȾρiedJdԋՙ튮]@Ljej{v?v.zt"_L4T0z26e7^E;&7x?+.w_<6czYYԩ1H83O &AVMWIC+kYY5>)V*QZKgbeAK42,pOZVV:/3½vWE m@-:{if+ zX{`$/+E6Vc ʊe{- +غ ++ ++ ++ ++ +++)+K_YY13AܔElVsxX!R+`k}R RH\^beOG`>t,x|ğUH)Vf +ՙ{O25`"g+G,4a+ ݀4۰|65<8QMZ,7_EfEW\?Ng?/mBuD>+w^FORnRHگ7w{%دN:lH%ܛ.΀t!6=? 3x\Vu7B/kMSIEc/FS%)a^TKg*XuU?$hж}8H L^齴ys+{ X} $o}m;I5[G ZRޟ֥R_Ky)/-'UTq%]RHL?P߿n_{~Z+ɮQץXSc$dib~SޟAt,x|~ğUH)Vfՙ;O1`f)E34.OU@zgh6A5ym<<. `8_íO,_%+,b?i1 +<J60Zɧ&OwUEq#Ϸ—F6{xS:" }YÂYХ.#5")ZnX'x?bH)Vf#כ{atW E@H6k$aMHe@'|iJ^Dtoޘ0ǻK'8puT +endstream +endobj +57 0 obj +<< +/Length 8067 +/Filter /FlateDecode +>> +stream +x]ˎ8rWHߤA.f n,2UߤDIA}uĸǡC u^؊R.˯EUpK)\G'jx"GƾZ8(OM~4/᯿|Կ[tiOMšLEK_hO۪L)bˠ)LkpkT~ ^?MXhtC4Uh\jQUQI}sWIaj/רt[Z~ -gJ[͍tx)2{ow~k+SV>vZjaO\-ݺ{uTt6\ pw5*L#%b6;OJy|/m޸byx#nșH#O_m1xnBlWƊN;ߵ%p޵?7M|^'[OMa!$ar'L3)3FS 8*ƕ_Ɲn +ե\F]ifd }toKSXJ*zACank}mm@H m3>Gj.A&!qqTq#$D.5+{X6a14R`ٿd[m"BKE8~- U^I*lrvs_A?J~_]y}_>_GB~^sw-wؿ}?5E$)q b54HsC*w< o qѲi]e Qdgkivq-}xf+S۔VyV;o-՛=~wV 5ݼ4K亻r(饠o$ .꓆ Ky|oory9o:3+GZ9>lkL*W{?ӬdŌf@8}}B^Ftϯh>HsQ3׮KKñr3ІUu̴?:iw3Z] W-9wVg{5{z+-{67F;@[#Gv?ݏ~d#Dz5BttAzzEf~X۔q[KmE/ @:(,90qD/IGxuV4ZX2{{C̙lH(F/4z8y澾?0?pFl;B@wgL(1rBYAa*ctx`rF*$n4VJ%*bJ4bffO1}F$2|zis\nq,ˍ*(}YC,R,݉ʈCr%/MA/ǑI{No +.o:4>R90E7ʀuOVxYDp8/lr_!sEQh#fƥ;(\Z"L~@ãQPT?~@C~@PT?~X~PFhe8m14?(~@C(~@Q?[aàh#zHe +Yi {.^PQ{DuRKQJ("R@AX*a:G'6%e N s(CJ'P:҉tbkP҉;N" zͼ]8'ReW+ + fs*4Z~ȳKBQ5eok܈Bʦxh3 r6OE1D߰U|/əmُpw]3FWpR$&܆0o~u60LkL 3h"d &ByZjx="2M%󣾬+d٠R#85WDk˕wuNC`'<h?݃/ ΓBl~*k2D Z,ŕ 'a.8({Lvf_'„GdsX⯧3Ac?jvv*&6W^"JVzlĐ\/Pxb o!=c,t196i6qۓ@4IGJ|yaJJN7tk]G +-s`UjI[.K!.sJ|A>n.O4 'ut&Ov$eWomPU+]'TɏMG(CgX*ƇwBޟԁ)!(0Y}Hg3 AnҪwxNǣ) /CUl%ߛhIC?03l-rdb"ֶQDэ)fvʥ$9(82;I=4M +M{Uwe?ҏy70tMC<1n~5[ȋڤ6FgoR#-[bn| EMAzq;~rA1'Z%<6@oc`6tK^W-9w֏ـٮ,i]uet7^@K"o65i7Vcao\`u~"ֻ*[/i'-eRCqK +;)k <{J +[oYsǁ:^>U]h-' ނr.t%#gcig>cB}3I1$;u^$Ň[gˮ*+ad%dZ$,e{Y(@Y"PE=E,eE,e(@Y"PE J7ZWFjt7MoӬ]1Q^THB>@D䑿ѣBb:ZP!7@9jP!q_U!_3f﫱ّBRzA4|p̴;-]1_,^_^;b3 lLIk-HNPqbbce@*6P TllbcjPqߊ ahr' 0`ЃnqXυnDv3IR2Asx.{\Yk =γyk%*=Ctϱ pw5*L +b6;OJy|/m޸ ],nȹ4:ە%:ҳ3nE5:PԁuE(@Q:PqJiN9"QqcssL{f=mZj\8j筥sFI j:AqBCZjO.,\JFx@kXskIq +XW`oǚuX=\ӱ<zs_UA.EIm`% ^=Bm a77oW]Yywz;AwwP:pt`:wJ>t9i.7F8lG C;Dp◦= ād¡dΡl8MyíǍ3`]!u=^>P0΋WGM^/Vn;d Λh],0Z d c_-{&ηrI*W );,LIbaV$(nĬX`yv 6,dokkσ+ǫ[FE1DVsPWzlBG8_+}zE]^'ӡr/}wC`}&xlC)d3|KqF-q(R"VŽR#2(J3[5|R7xR s8ȻZ Ь%R\o p(Cvdnu(LDƞ!z4Njg׭ki}u7dN 'Pּ3FBO[chfsig=r d[L#yQ!ɗw~fH׼TLWZ)ޅqЂ?i98V'N]6=mTu O8%&4?d>1$󉺟ێm7yߚ+[jjP&?t~7 iaH# 0O+?Q@$=zTeam&*׸O+!p:#]圎f<'Ꝩ5k7:ܔL3Y.]@|ؘҭC쐜BZ%pIsY+W.l+W*Lk$5|,`crOlH t-VR.9 oB-=dܔ|RXԓ)y)E,88, +dQ Y,km}tng#KHgF7 ++ lYCd|sb 3\@?w-?pCnC2 P ȧ6*M]RaWMق)\2蝀ak\훿CAJgKc+J<4@7 DUJh+{8]]e UK¦Yt-z|Iz.:὾6>K{8N6g>fus; KX07كe8{{vj/ZhMcurVJ"]!J"77PQ7\THE.uD.uI"]7\Y H}6.wا8߻Lﭣ0!0'*o-aRY}0|("6aA-rnsa׎"9FE-rns["9ȹMs{+έПZqz +O")HEmr[.r[H FZ-iՔI9'6(z 6kȽVXfX8Znٽ{[T Rew*TY"UHE,Re*ocPCI󡅹c61ć[gˮ*+a^<ӕ9B+2]s d"tvA+2]:iTd"tE+2]L[bVѺ2R+-էiz*튙 !IHzMIHz'ts&$"HufbBsSucE>m +jk7-αlC#\avLϧ]8c=e6*txa zugh#Y#`# IHE.p$t$\$% W\+~t4%W:hK%JwJbV %) Rr\v %)Hɝ4*Rr\"%)HEJn-;?g$7C5s)Zbu6$]$"I7 H$"IwD.tӑ5$I7}aq0mWxEn!!Zjm5g[ie4 z,NΖ/}X@E,e.tY"]"]HK߯b .Ns;kKcfk(RJi Rd" z E)HElkG Rd"I"E)HE,Rd"YȦނHEJiN9" \X"qXxC.,ra vF.,ra㯾Yu"mσVodB&zXVЖ([jqӼF+侷q"Mi5?AF'=DͅDϜӆD 1=DϛyDyxbH׾|~3hNvR 0Z d }'ߊtn\Cnoi&$,t0 +endstream +endobj +xref +0 103 +0000000000 65535 f +0000011552 00000 n +0000011680 00000 n +0000011465 00000 n +0000011444 00000 n +0000011727 00000 n +0000038083 00000 n +0000000882 00000 n +0000000721 00000 n +0000000015 00000 n +0000008876 00000 n +0000009859 00000 n +0000010012 00000 n +0000011290 00000 n +0000008165 00000 n +0000000059 00000 n +0000000148 00000 n +0000000281 00000 n +0000000370 00000 n +0000000501 00000 n +0000000590 00000 n +0000070316 00000 n +0000001870 00000 n +0000001706 00000 n +0000001044 00000 n +0000001133 00000 n +0000001265 00000 n +0000001354 00000 n +0000001485 00000 n +0000001574 00000 n +0000077567 00000 n +0000002849 00000 n +0000002685 00000 n +0000002022 00000 n +0000002111 00000 n +0000002244 00000 n +0000002333 00000 n +0000002464 00000 n +0000002553 00000 n +0000063167 00000 n +0000003826 00000 n +0000003662 00000 n +0000003001 00000 n +0000003090 00000 n +0000003223 00000 n +0000003312 00000 n +0000003443 00000 n +0000003532 00000 n +0000049423 00000 n +0000004802 00000 n +0000004638 00000 n +0000003978 00000 n +0000004067 00000 n +0000004200 00000 n +0000004289 00000 n +0000004419 00000 n +0000004508 00000 n +0000084834 00000 n +0000005087 00000 n +0000004954 00000 n +0000032298 00000 n +0000005361 00000 n +0000005228 00000 n +0000043891 00000 n +0000005635 00000 n +0000005502 00000 n +0000026651 00000 n +0000005909 00000 n +0000005776 00000 n +0000056141 00000 n +0000006877 00000 n +0000006713 00000 n +0000006050 00000 n +0000006139 00000 n +0000006272 00000 n +0000006361 00000 n +0000006492 00000 n +0000006581 00000 n +0000022021 00000 n +0000007856 00000 n +0000007692 00000 n +0000007029 00000 n +0000007118 00000 n +0000007251 00000 n +0000007340 00000 n +0000007471 00000 n +0000007560 00000 n +0000008089 00000 n +0000007997 00000 n +0000008025 00000 n +0000008053 00000 n +0000011770 00000 n +0000008263 00000 n +0000008527 00000 n +0000013154 00000 n +0000013506 00000 n +0000009025 00000 n +0000009292 00000 n +0000016179 00000 n +0000016616 00000 n +0000010113 00000 n +0000010376 00000 n +0000021452 00000 n +trailer +<< +/Size 103 +/Root 3 0 R +/Info 87 0 R +/ID [ ] +>> +startxref +92975 +%%EOF