diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2a9061e0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# they will be requested for review when someone +# opens a pull request. +* @bvizureanu @aniri + +# More details on creating a codeowners file: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners diff --git a/.gitignore b/.gitignore index 8dacc1ab..35053583 100644 --- a/.gitignore +++ b/.gitignore @@ -246,13 +246,17 @@ ModelManifest.xml *.pubxml *.ps1 *.psm1 -/private-api/app/src/VotingIrregularities.Api/appsettings.development.json -/private-api/app/src/VotingIrregularities.Api/appsettings.localdevelopment.json -/private-api/app/src/VotingIrregularities.Domain/appsettings.target.json -/private-api/app/src/VotingIrregularities.Domain/appsettings.development.json -/private-api/app/src/modules/WebAPIContrib.Core/ -/private-api/app/src/VotingIrregularities.Domain/appsettings.localdevelopment.json -/private-api/app/test/VotingIrregularities.Tests/appsettings.localDevelopment.json -/private-api/app/test/VotingIrregularities.Tests/conturi.txt -/private-api/app/test/VotingIrregularities.Tests/conturi-cu-parole.txt -.vscode \ No newline at end of file + +# Specifics + +**/appsettings.*.json +**/conturi*.txt +/src/api/modules/WebAPIContrib.Core/ + +.vscode +.idea +/src/api/VotingIrregularities.Api/etc +/src/api/VotingIrregularities.Api/api-docs +**/api-docs + +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dd689e4a..6be9a920 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,27 @@ -FROM microsoft/dotnet:2.1-sdk AS build-env +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env WORKDIR /app -COPY private-api/app/VotingIrregularities.sln ./ -COPY private-api/app/src/VotingIrregularities.Api/VotingIrregularities.Api.csproj src/VotingIrregularities.Api/ -COPY private-api/app/src/VotingIrregularities.Domain/VotingIrregularities.Domain.csproj src/VotingIrregularities.Domain/ -COPY private-api/app/test/VotingIrregularities.Domain.Tests/VotingIrregularities.Domain.Tests.csproj test/VotingIrregularities.Domain.Tests/ -COPY private-api/app/test/VotingIrregularities.Tests/VotingIrregularities.Tests.csproj test/VotingIrregularities.Tests/ - -# Copy all at once (big image probably?) -#COPY private-api/app/. ./ +# Copy sources +COPY /src/. ./ +# Restore packages RUN dotnet restore -# Copy everything else and build -COPY private-api/app/. ./ +# Build and publish as `Release` RUN dotnet publish -c Release -o ./out # test application -- see: dotnet-docker-unit-testing.md FROM build-env AS testrunner WORKDIR /app/tests -COPY private-api/app/test/. . +COPY /src/test/. . ENTRYPOINT ["dotnet", "test", "--logger:trx"] # Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine +RUN apk add --no-cache --repository https://alpine.global.ssl.fastly.net/alpine/edge/testing/ \ + libgdiplus-dev \ + fontconfig \ + ttf-dejavu WORKDIR / -COPY --from=build-env /app/src/VotingIrregularities.Api/out/ . -ENTRYPOINT ["dotnet", "VotingIrregularities.Api.dll"] \ No newline at end of file +COPY --from=build-env /app/api/VoteMonitor.Api/out/ . +ENTRYPOINT ["dotnet", "VoteMonitor.Api.dll"] \ No newline at end of file diff --git a/README.md b/README.md index 93f54494..29d32e80 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,43 @@ -# Monitorizare Vot - Rest API for mobile apps +# Monitorizare Vot - Rest API for mobile apps & web NGO platform [![GitHub contributors](https://img.shields.io/github/contributors/code4romania/monitorizare-vot.svg?style=for-the-badge)](https://github.com/code4romania/monitorizare-vot/graphs/contributors) [![GitHub last commit](https://img.shields.io/github/last-commit/code4romania/monitorizare-vot.svg?style=for-the-badge)](https://github.com/code4romania/monitorizare-vot/commits/master) [![License: MPL 2.0](https://img.shields.io/badge/license-MPL%202.0-brightgreen.svg?style=for-the-badge)](https://opensource.org/licenses/MPL-2.0) -[See the project live](http://monitorizarevot.ro/) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=code4romania_monitorizare-vot&metric=alert_status)](https://sonarcloud.io/dashboard?id=code4romania_monitorizare-vot) +[![Build status](https://dev.azure.com/code4romania/monitorizare-vot-ci/_apis/build/status/monitorizare-vot/mv-api)](https://dev.azure.com/code4romania/monitorizare-vot-ci/_build/latest?definitionId=20) -Monitorizare Vot is a mobile app for monitoring elections by authorized observers. They can use the app in order to offer a real-time snapshot on what is going on at polling stations and they can report on any noticeable irregularities. +[See the project live](https://votemonitor.org/) -The NGO-s with authorized observers for monitoring elections have real time access to the data the observers are transmitting therefore they can report on how voting is evolving and they can quickly signal to the authorities where issues need to be solved. +Monitorizare Vot is a mobile app for monitoring elections by authorized observers. They can use the app in order to offer a real-time snapshot on what is going on at polling stations and they can report on any noticeable irregularities. -Moreover, where it is allowed, observers can also photograph and film specific situations and send the images to the NGO they belong to. +The NGO-s with authorized observers for monitoring elections have real time access to the data the observers are transmitting therefore they can report on how voting is evolving and they can quickly signal to the authorities where issues need to be solved. -The app also has a web version, available for every citizen who wants to report on election irregularities. Monitorizare Vot was launched in 2016 and it has been used for the Romanian parliamentary elections so far, but it is available for further use, regardless of the type of elections or voting process. +Moreover, where it is allowed, observers can also photograph and film specific situations and send the images to the NGO they belong to. -[Built with](#built-with) | [Repos and projects](#repos-and-projects) | [Deployment](#deployment) | [Contributing](#contributing) | [Feedback](#feedback) | [License](#license) | [About Code4Ro](#about-code4ro) +The app also has a web version, available for every citizen who wants to report on election irregularities. Monitorizare Vot was launched in 2016 and it has been used for the Romanian parliamentary elections so far, but it is available for further use, regardless of the type of elections or voting process. + +[Contributing](#contributing) | [Built with](#built-with) | [Repos and projects](#repos-and-projects) | [Deployment](#deployment) | [Feedback](#feedback) | [License](#license) | [About Code4Ro](#about-code4ro) + +## Contributing + +This project is built by amazing volunteers and you can be one of them! Here's a list of ways in [which you can contribute to this project](.github/CONTRIBUTING.MD). ## Built With - .Net Core 1.1 + .Net Core 3.1 - Swagger docs for the API are available [here](https://mv-mobile-prod.azurewebsites.net/swagger/ui/index.html). + Swagger docs for the API are available [here](https://app-vmon-api-dev.azurewebsites.net/swagger/index.html). ## Repos and projects +![alt text](https://raw.githubusercontent.com/code4romania/monitorizare-vot/develop/vote_monitor_diagram.png) + Client apps: -- android - https://github.com/code4romania/monitorizare-vot-android +- Android - https://github.com/code4romania/mon-vot-android-kotlin - iOS - https://github.com/code4romania/monitorizare-vot-ios +- Web admin for NGOs - https://github.com/code4romania/monitorizare-vot-ong -Other MV related repos: - -- https://github.com/code4romania/monitorizare-vot-admin -- https://github.com/code4romania/monitorizare-vot-ong -- https://github.com/code4romania/monitorizare-vot-votanti-client/ -- https://github.com/code4romania/monitorizare-vot-votanti-api -- https://github.com/code4romania/monitorizare-vot-votanti-admin -- https://github.com/code4romania/monitorizare-vot-docs - -## Creating the database +## Creating the database --- WIP you might encounter issues here. The Assembly VotingIrregularities.Domain has EF Migrations configured and can generate a database complete with test data. @@ -44,41 +45,29 @@ To do this, follow the steps bellow: Fill-in `appsetings.json` OR add in a new `appsettings.target.json` file the connectionstring to the SQL instance where the DB should be created. -Run the following console command from the `VotingIrregularities.Domain` folder: +Run the following console command from the `VotingIrregularities.Domain.Seed` folder: ```sh -private-api\app\VotingIrregularities.Domain> dotnet run +src\api\VotingIrregularities.Domain.Seed> dotnet run ``` -**Important:** the migrate action with delete the data from the following tables: RaspunsDisponibil, Intrebare, Sectiune, Optiune. +**Important:** the migrate action with delete the data from the following tables: `Answers`, `Questions`, `FormSections`, `Options`. ## Deployment -1. install .NetCore (Open Source/Free/Multiplatform) from [here](https://www.microsoft.com/net/core#windows) +1. install .NetCore (refer to the [Built With](#built-with) section for the proper version) (Open Source/Free/Multiplatform) from [here](https://www.microsoft.com/net/core#windows) -2. run the following console command form the `app` folder: +2. run the following console command from the `src` folder: ```sh - private-api\app> dotnet restore + src> dotnet restore ``` -3. run the following console command form the `VotingIrregularities.Api` folder: +3. run the following console command from the `VoteMonitor.Api` folder: ```sh - private-api\app\VotingIrregularities.Api> dotnet run + src\api\VoteMonitor.Api> dotnet run ``` -4. browse to indicated address: - -## Contributing - -If you would like to contribute to one of our repositories, first identify the scale of what you would like to contribute. If it is small (grammar/spelling or a bug fix) feel free to start working on a fix. If you are submitting a feature or substantial code contribution, please discuss it with the team and ensure it follows the product roadmap. - -* Fork it (https://github.com/code4romania/monitorizare-vot/fork) -* Create your feature branch (git checkout -b feature/fooBar) -* Commit your changes (git commit -am 'Add some fooBar') -* Push to the branch (git push origin feature/fooBar) -* Create a new Pull Request - -[Pending issues](https://github.com/code4romania/monitorizare-vot/issues) +4. browse to indicated address: ## Feedback @@ -95,4 +84,4 @@ This project is licensed under the MPL 2.0 License - see the [LICENSE](LICENSE) Started in 2016, Code for Romania is a civic tech NGO, official member of the Code for All network. We have a community of over 500 volunteers (developers, ux/ui, communications, data scientists, graphic designers, devops, it security and more) who work pro-bono for developing digital solutions to solve social problems. #techforsocialgood. If you want to learn more details about our projects [visit our site](https://www.code4.ro/en/) or if you want to talk to one of our staff members, please e-mail us at contact@code4.ro. -Last, but not least, we rely on donations to ensure the infrastructure, logistics and management of our community that is widely spread accross 11 timezones, coding for social change to make Romania and the world a better place. If you want to support us, [you can do it here](https://code4.ro/en/donate/). +Last, but not least, we rely on donations to ensure the infrastructure, logistics and management of our community that is widely spread across 11 timezones, coding for social change to make Romania and the world a better place. If you want to support us, [you can do it here](https://code4.ro/en/donate/). diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..1c75e000 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,44 @@ +# https://aka.ms/yaml + +pool: + vmImage: 'windows-latest' + +variables: + BuildConfiguration: Release + +steps: +- task: DotNetCoreCLI@2 + displayName: 'restore packages' + inputs: + command: 'restore' + projects: '**/*.sln' + feedsToUse: 'select' + +- task: DotNetCoreCLI@2 + displayName: 'build solution' + inputs: + command: 'build' + projects: '**/*.sln' + arguments: '--configuration $(BuildConfiguration)' + +- task: DotNetCoreCLI@2 + displayName: 'publish web project(s)' + inputs: + command: 'publish' + publishWebProjects: true + arguments: '--no-restore --no-build --configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + +- task: PublishPipelineArtifact@1 + displayName: 'Upload artifact' + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)' + artifact: 'drop' + publishLocation: 'pipeline' + + +- task: PublishBuildArtifacts@1 + enabled: false + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' + publishLocation: 'Container' \ No newline at end of file diff --git a/GettingStarted.md b/docs/GettingStarted.md similarity index 100% rename from GettingStarted.md rename to docs/GettingStarted.md diff --git a/private-api/app/VotingIrregularities.sln b/private-api/app/VotingIrregularities.sln deleted file mode 100644 index 6f352ae7..00000000 --- a/private-api/app/VotingIrregularities.sln +++ /dev/null @@ -1,59 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8A09B442-FB79-4293-BF9B-E34DD3AE70F3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{388C55EB-26FF-46AB-9395-549E1A7A99AD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Api", "src\VotingIrregularities.Api\VotingIrregularities.Api.csproj", "{C26FC8F5-B11B-4908-B67D-5AEEB915D002}" - ProjectSection(ProjectDependencies) = postProject - {73125CAC-66E8-40D7-81A7-6A847132C7B3} = {73125CAC-66E8-40D7-81A7-6A847132C7B3} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Domain", "src\VotingIrregularities.Domain\VotingIrregularities.Domain.csproj", "{73125CAC-66E8-40D7-81A7-6A847132C7B3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Domain.Tests", "test\VotingIrregularities.Domain.Tests\VotingIrregularities.Domain.Tests.csproj", "{4E70B88E-F43A-4546-955C-4A2139C1CC9C}" - ProjectSection(ProjectDependencies) = postProject - {73125CAC-66E8-40D7-81A7-6A847132C7B3} = {73125CAC-66E8-40D7-81A7-6A847132C7B3} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Tests", "test\VotingIrregularities.Tests\VotingIrregularities.Tests.csproj", "{41FE87AE-428C-4CD5-88F1-A7A713334F2E}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C26FC8F5-B11B-4908-B67D-5AEEB915D002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C26FC8F5-B11B-4908-B67D-5AEEB915D002}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C26FC8F5-B11B-4908-B67D-5AEEB915D002}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C26FC8F5-B11B-4908-B67D-5AEEB915D002}.Release|Any CPU.Build.0 = Release|Any CPU - {73125CAC-66E8-40D7-81A7-6A847132C7B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73125CAC-66E8-40D7-81A7-6A847132C7B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73125CAC-66E8-40D7-81A7-6A847132C7B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73125CAC-66E8-40D7-81A7-6A847132C7B3}.Release|Any CPU.Build.0 = Release|Any CPU - {4E70B88E-F43A-4546-955C-4A2139C1CC9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E70B88E-F43A-4546-955C-4A2139C1CC9C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E70B88E-F43A-4546-955C-4A2139C1CC9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E70B88E-F43A-4546-955C-4A2139C1CC9C}.Release|Any CPU.Build.0 = Release|Any CPU - {41FE87AE-428C-4CD5-88F1-A7A713334F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41FE87AE-428C-4CD5-88F1-A7A713334F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41FE87AE-428C-4CD5-88F1-A7A713334F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41FE87AE-428C-4CD5-88F1-A7A713334F2E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {C26FC8F5-B11B-4908-B67D-5AEEB915D002} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} - {73125CAC-66E8-40D7-81A7-6A847132C7B3} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} - {4E70B88E-F43A-4546-955C-4A2139C1CC9C} = {388C55EB-26FF-46AB-9395-549E1A7A99AD} - {41FE87AE-428C-4CD5-88F1-A7A713334F2E} = {388C55EB-26FF-46AB-9395-549E1A7A99AD} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {AF1523BC-7F31-4564-8E1B-D2DB4552FFCB} - EndGlobalSection -EndGlobal diff --git a/private-api/app/readme.md b/private-api/app/readme.md deleted file mode 100644 index 59e36abd..00000000 --- a/private-api/app/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -## Rulare aplicatie - -1. instaleaza .NetCore (Open Source/Free/Multiplatform) de [aici](https://www.microsoft.com/net/core#windows) - -2. ruleaza din consola, in folderul app: - ```sh - private-api\app> dotnet restore - ``` - -3. ruleaza din folderul VotingIrregularities.Api: - ```sh - private-api\app\VotingIrregularities.Api> dotnet run - ``` - -4. browse to indicated address: diff --git a/private-api/app/src/VotingIrregularities.Api/Controllers/Authorization.cs b/private-api/app/src/VotingIrregularities.Api/Controllers/Authorization.cs deleted file mode 100644 index 11d2f693..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Controllers/Authorization.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using VotingIrregularities.Api.Models.AccountViewModels; -using System.Linq; -using MediatR; -using VotingIrregularities.Domain.UserAggregate; -using VotingIrregularities.Api.Options; - -namespace VotingIrregularities.Api.Controllers -{ - /// - [Route("api/v1/access")] - public class Authorization : Controller - { - private readonly JwtIssuerOptions _jwtOptions; - private readonly ILogger _logger; - private readonly IMediator _mediator; - private readonly JsonSerializerSettings _serializerSettings; - private readonly MobileSecurityOptions _mobileSecurityOptions; - - /// - public Authorization(IOptions jwtOptions, ILogger logger, IMediator mediator, IOptions mobileSecurityOptions) - { - _jwtOptions = jwtOptions.Value; - ThrowIfInvalidOptions(_jwtOptions); - - _logger = logger; - _mediator = mediator; - _mobileSecurityOptions = mobileSecurityOptions.Value; - - _serializerSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - } - - /// - /// Get the auth token to be passed to subsequent requests - /// - /// - /// - [HttpPost("token")] - [AllowAnonymous] - public async Task Get([FromBody] ApplicationUser applicationUser) - { - if (!ModelState.IsValid) - return BadRequest(ModelState); - - var identity = await GetClaimsIdentity(applicationUser); - if (identity == null) - { - _logger.LogInformation($"Invalid Phone ({applicationUser.Phone}) or password ({applicationUser.Pin})"); - return BadRequest("{ \"error\": \"La ora asta observatorii ar trebui sa doarma! :) Aplicatia va fi functionala la ora 6. Asigura-te ca ai cea mai recenta versiune. Fa un update!\" }"); - } - - var claims = new[] - { - new Claim(JwtRegisteredClaimNames.Sub, applicationUser.Phone), - new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), - new Claim(JwtRegisteredClaimNames.Iat, - ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), - ClaimValueTypes.Integer64), - identity.FindFirst("IdObservator") - }; - - // Create the JWT security token and encode it. - var jwt = new JwtSecurityToken( - issuer: _jwtOptions.Issuer, - audience: _jwtOptions.Audience, - claims: claims, - notBefore: _jwtOptions.NotBefore, - expires: _jwtOptions.Expiration, - signingCredentials: _jwtOptions.SigningCredentials); - - var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); - - // Serialize and return the response - var response = new - { - access_token = encodedJwt, - expires_in = (int)_jwtOptions.ValidFor.TotalSeconds - }; - - var json = JsonConvert.SerializeObject(response, _serializerSettings); - return new OkObjectResult(json); - } - /// - /// Test action to get claims - /// - /// - [Authorize] - [HttpPost("test")] - public async Task Test() - { - var claims = User.Claims.Select(c => new - { - c.Type, - c.Value - }); - - return await Task.FromResult(claims); - } - private static void ThrowIfInvalidOptions(JwtIssuerOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - if (options.ValidFor <= TimeSpan.Zero) - { - throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); - } - - if (options.SigningCredentials == null) - { - throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); - } - - if (options.JtiGenerator == null) - { - throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); - } - } - - /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). - private static long ToUnixEpochDate(DateTime date) - => (long)Math.Round((date.ToUniversalTime() - - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) - .TotalSeconds); - - private async Task GetClaimsIdentity(ApplicationUser user) - { - // verific daca userul exista si daca nu are asociat un alt device, il returneaza din baza - var userInfo = await _mediator.Send(user); - - if (!userInfo.IsAuthenticated) - return await Task.FromResult(null); - - if (userInfo.FirstAuthentication && _mobileSecurityOptions.LockDevice) - await - _mediator.Send(new RegisterDeviceId - { - MobileDeviceId = user.UDID, - ObserverId = userInfo.ObserverId - }); - - return await Task.FromResult(new ClaimsIdentity( - new GenericIdentity(user.Phone, "Token"), - new[] - { - new Claim("Observator", "ONG"), - new Claim("ObserverId", userInfo.ObserverId.ToString()) - })); - } - } -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Controllers/Formular.cs b/private-api/app/src/VotingIrregularities.Api/Controllers/Formular.cs deleted file mode 100644 index 4d736f9d..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Controllers/Formular.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using VotingIrregularities.Api.Models; -using Microsoft.Extensions.Configuration; - -namespace VotingIrregularities.Api.Controllers -{ - /// - /// - /// Ruta Formular ofera suport pentru toate operatiile legate de formularele completate de observatori - /// - [Route("api/v1/formular")] - public class Formular : Controller - { - private readonly IConfigurationRoot _configuration; - private readonly IMediator _mediator; - - public Formular(IMediator mediator, IConfigurationRoot configuration) - { - _configuration = configuration; - _mediator = mediator; - } - - /// - /// Returneaza versiunea tuturor formularelor sub forma unui array. - /// Daca versiunea returnata difera de cea din aplicatie, atunci trebuie incarcat formularul din nou - /// - /// - [HttpGet("versiune")] - public async Task Versiune() - { - return new ModelVersiune { Versiune = await _mediator.Send(new FormVersionQuery())}; - } - - /// - /// Se interogheaza ultima versiunea a formularului pentru observatori si se primeste definitia lui. - /// In definitia unui formular nu intra intrebarile standard (ora sosirii, etc). - /// Acestea se considera implicite pe fiecare formular. - /// - /// Id-ul formularului pentru care trebuie preluata definitia - /// - [HttpGet] - public async Task> Citeste(string idformular) - { - var result = await _mediator.Send(new FormQuestionsQuery { - CodFormular = idformular, - CacheHours = _configuration.GetValue("DefaultCacheHours"), - CacheMinutes = _configuration.GetValue("DefaultCacheMinutes"), - CacheSeconds = _configuration.GetValue("DefaultCacheSeconds") - }); - - return result; - } - } -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Controllers/Nota.cs b/private-api/app/src/VotingIrregularities.Api/Controllers/Nota.cs deleted file mode 100644 index b63731e7..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Controllers/Nota.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using VotingIrregularities.Api.Extensions; -using VotingIrregularities.Api.Models; -using AutoMapper; -using VotingIrregularities.Domain.NotaAggregate; - -namespace VotingIrregularities.Api.Controllers -{ - [Route("api/v1/note")] - public class Nota : Controller - { - private readonly IMapper _mapper; - private readonly IMediator _mediator; - - public Nota(IMediator mediator, IMapper mapper) - { - _mediator = mediator; - _mapper = mapper; - } - - /// - /// Aceasta ruta este folosita cand observatorul incarca o imagine sau un clip in cadrul unei note. - /// Fisierului atasat i se da contenttype = Content-Type: multipart/form-data - /// Celalalte proprietati sunt de tip form-data - /// CodJudet:BU - /// NumarSectie:3243 - /// IdIntrebare: 201 - /// TextNota: "asdfasdasdasdas" - /// API-ul va returna adresa publica a fisierului unde este salvat si obiectul trimis prin formdata - /// - /// - /// - /// - [HttpPost("ataseaza")] - public async Task Upload(IFormFile file, [FromForm]ModelNota nota) - { - if (!ModelState.IsValid) - return this.ResultAsync(HttpStatusCode.BadRequest); - - // TODO[DH] use a pipeline instead of separate Send commands - // daca nota este asociata sectiei - int idSectie = await _mediator.Send(_mapper.Map(nota)); - if (idSectie < 0) - return this.ResultAsync(HttpStatusCode.NotFound); - - var command = _mapper.Map(nota); - var fileAddress = await _mediator.Send(new ModelFile { File = file }); - - // TODO[DH] get the actual IdObservator from token - command.IdObservator = int.Parse(User.Claims.First(c => c.Type == "IdObservator").Value); - command.CaleFisierAtasat = fileAddress; - command.IdSectieDeVotare = idSectie; - - var result = await _mediator.Send(command); - - if (result < 0) - return this.ResultAsync(HttpStatusCode.NotFound); - - return await Task.FromResult(new { FileAdress = fileAddress, nota = nota }); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Controllers/Raspuns.cs b/private-api/app/src/VotingIrregularities.Api/Controllers/Raspuns.cs deleted file mode 100644 index 51e45f8d..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Controllers/Raspuns.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using AutoMapper; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using VotingIrregularities.Api.Extensions; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Domain.RaspunsAggregate.Commands; - -namespace VotingIrregularities.Api.Controllers -{ - /// - /// Ruta unde se inregistreaza raspunsurile - /// - [Route("/api/v1/raspuns")] - public class Raspuns : Controller - { - private readonly IMediator _mediator; - private readonly IMapper _mapper; - private readonly ILogger _logger; - - public Raspuns(IMediator mediator, IMapper mapper, ILogger logger) - { - _mediator = mediator; - _mapper = mapper; - _logger = logger; - } - - /// - /// Aici se inregistreaza raspunsul dat de observator la una sau mai multe intrebari, pentru o sectie de votare. - /// Raspunsul (ModelOptiuniSelectate) poate avea mai multe optiuni (IdOptiune) si potential un text (Value). - /// - /// Sectia de votare, lista de optiuni si textul asociat unei optiuni care se completeaza cand - /// optiunea SeIntroduceText = true - /// - [HttpPost()] - public async Task CompleteazaRaspuns([FromBody] ModelRaspunsWrapper raspuns) - { - - if (!ModelState.IsValid) - { - return this.ResultAsync(HttpStatusCode.BadRequest, ModelState); - - } - - // TODO[DH] use a pipeline instead of separate Send commands - var command = await _mediator.Send(new RaspunsuriBulk(raspuns.Raspuns)); - - // TODO[DH] get the actual IdObservator from token - command.IdObservator = int.Parse(User.Claims.First(c => c.Type == "IdObservator").Value); - - var result = await _mediator.Send(command); - - return this.ResultAsync(result < 0 ? HttpStatusCode.NotFound : HttpStatusCode.OK); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Controllers/Sectie.cs b/private-api/app/src/VotingIrregularities.Api/Controllers/Sectie.cs deleted file mode 100644 index b7690228..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Controllers/Sectie.cs +++ /dev/null @@ -1,78 +0,0 @@ -using AutoMapper; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using VotingIrregularities.Api.Extensions; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Domain.SectieAggregate; - -namespace VotingIrregularities.Api.Controllers -{ - /// - /// Controller responsible for interacting with the polling stations - PollingStationInfo - /// - [Route("api/v1/sectie")] - public class Sectie : Controller - { - private readonly IMapper _mapper; - private readonly IMediator _mediator; - - public Sectie(IMediator mediator, IMapper mapper) - { - _mapper = mapper; - _mediator = mediator; - } - - /// - /// Se apeleaza aceast metoda cand observatorul salveaza informatiile legate de ora sosirii. ora plecarii, zona urbana, info despre presedintele BESV. - /// Aceste informatii sunt insotite de id-ul sectiei de votare. - /// - /// Informatii despre sectia de votare si observatorul alocat ei - /// - [HttpPost()] - public async Task Inregistreaza([FromBody] ModelDateSectie dateSectie) - { - if (!ModelState.IsValid) - return this.ResultAsync(HttpStatusCode.BadRequest, ModelState); - - var command = _mapper.Map(dateSectie); - - // TODO[DH] get the actual IdObservator from token - command.IdObservator = int.Parse(User.Claims.First(c => c.Type == "IdObservator").Value); - - var result = await _mediator.Send(command); - - return this.ResultAsync(result < 0 ? HttpStatusCode.NotFound : HttpStatusCode.OK); - } - - /// - /// Se apeleaza aceasta metoda cand se actualizeaza informatiile legate de ora plecarii. - /// Aceste informatii sunt insotite de id-ul sectiei de votare. - /// - /// Numar sectie de votare, cod judet, ora plecarii - /// - [HttpPut] - public async Task Actualizeaza([FromBody] ModelActualizareDateSectie dateSectie) - { - if (!ModelState.IsValid) - return this.ResultAsync(HttpStatusCode.BadRequest, ModelState); - - int idSectie = await _mediator.Send(_mapper.Map(dateSectie)); - if (idSectie < 0) - return this.ResultAsync(HttpStatusCode.NotFound); - - var command = _mapper.Map(dateSectie); - - // TODO get the actual IdObservator from token - command.IdObservator = int.Parse(User.Claims.First(c => c.Type == "IdObservator").Value); - command.IdSectieDeVotare = idSectie; - - var result = await _mediator.Send(command); - - return this.ResultAsync(result < 0 ? HttpStatusCode.NotFound : HttpStatusCode.OK); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Extensions/AddFileUploadParams.cs b/private-api/app/src/VotingIrregularities.Api/Extensions/AddFileUploadParams.cs deleted file mode 100644 index 558ad31d..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Extensions/AddFileUploadParams.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Swashbuckle.AspNetCore.Swagger; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace VotingIrregularities.Api.Extensions -{ - /// - public class AddFileUploadParams : IOperationFilter - { - /// - public void Apply(Operation operation, OperationFilterContext context) - { - if (!string.Equals(operation.OperationId, "ApiV1NoteAtaseazaPost", StringComparison.CurrentCultureIgnoreCase)) - return; - - operation.Consumes.Add("application/form-data"); - operation.Parameters = new IParameter[] - { - new NonBodyParameter - { - - Name = "file", - In = "formData", - Required = true, - Type = "file" - } - }; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Extensions/ControllerExtensions.cs b/private-api/app/src/VotingIrregularities.Api/Extensions/ControllerExtensions.cs deleted file mode 100644 index ae507504..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Extensions/ControllerExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace VotingIrregularities.Api.Extensions -{ - public static class ControllerExtensions - { - - public static IAsyncResult ResultAsync(this Controller controller, HttpStatusCode statusCode, ModelStateDictionary modelState = null) - { - controller.Response.StatusCode = (int)statusCode; - - if (modelState == null) - return Task.FromResult(new StatusCodeResult((int)statusCode)); - - return Task.FromResult(controller.BadRequest(modelState)); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelActualizareDateSectie.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelActualizareDateSectie.cs deleted file mode 100644 index b58f7c65..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelActualizareDateSectie.cs +++ /dev/null @@ -1,28 +0,0 @@ -using AutoMapper; -using System; -using System.ComponentModel.DataAnnotations; -using VotingIrregularities.Domain.SectieAggregate; - -namespace VotingIrregularities.Api.Models -{ - public class ModelActualizareDateSectie - { - [Required(AllowEmptyStrings = false)] - public string CodJudet { get; set; } - - [Required(AllowEmptyStrings = false)] - public int NumarSectie { get; set; } - - [Required(AllowEmptyStrings = false)] - public DateTime? OraPlecarii { get; set; } - } - - public class ModelActualizareDateSectieProfile : Profile - { - public ModelActualizareDateSectieProfile() - { - CreateMap(); - CreateMap(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelDateSectie.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelDateSectie.cs deleted file mode 100644 index 6f03503c..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelDateSectie.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using VotingIrregularities.Domain.SectieAggregate; - -namespace VotingIrregularities.Api.Models -{ - public class ModelDateSectie - { - [Required(AllowEmptyStrings = false)] - public string CodJudet { get; set; } - - [Required(AllowEmptyStrings = false)] - public int NumarSectie { get; set; } - - public DateTime? OraSosirii { get; set; } - public DateTime? OraPlecarii { get; set; } - public bool? EsteZonaUrbana { get; set; } - public bool? PresedinteBesvesteFemeie { get; set; } - } - - - - public class ModelDateSectieProfile : Profile - { - public ModelDateSectieProfile() - { - CreateMap(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelFile.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelFile.cs deleted file mode 100644 index f13bf1da..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelFile.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Http; - -namespace VotingIrregularities.Api.Models -{ - public class ModelFile : IRequest - { - public IFormFile File { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelNota.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelNota.cs deleted file mode 100644 index 87504c4a..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelNota.cs +++ /dev/null @@ -1,25 +0,0 @@ -using AutoMapper; -using System.ComponentModel.DataAnnotations; -using VotingIrregularities.Domain.NotaAggregate; - -namespace VotingIrregularities.Api.Models -{ - public class ModelNota - { - [Required(AllowEmptyStrings = false)] - public string CodJudet { get; set; } - [Required] - public int NumarSectie { get; set; } - public int? IdIntrebare { get; set; } - public string TextNota { get; set; } - } - - public class ModelNotaProfile : Profile - { - public ModelNotaProfile() - { - CreateMap(); - CreateMap(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelRaspunsBulk.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelRaspunsBulk.cs deleted file mode 100644 index d8c4f9cc..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelRaspunsBulk.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using MediatR; -using VotingIrregularities.Domain.RaspunsAggregate.Commands; - -namespace VotingIrregularities.Api.Models -{ - public class ModelRaspunsWrapper - { - public ModelRaspunsBulk[] Raspuns { get; set; } - } - public class ModelRaspunsBulk - { - [Required] - public int IdIntrebare { get; set; } - - [Required(AllowEmptyStrings = false)] - public string CodJudet { get; set; } - - [Required(AllowEmptyStrings = false)] - public int NumarSectie { get; set; } - - //[Required(AllowEmptyStrings = false)] - public string CodFormular { get; set; } - public List Optiuni { get; set; } - } - - public class RaspunsuriBulk : IRequest - { - public RaspunsuriBulk(IEnumerable raspunsuri) - { - ModelRaspunsuriBulk = raspunsuri.ToList(); - } - - public int IdObservator { get; set; } - - public List ModelRaspunsuriBulk { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelSectie.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelSectie.cs deleted file mode 100644 index 409521e0..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelSectie.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; - -namespace VotingIrregularities.Api.Models -{ - public class ModelSectieQuery : IRequest - { - public string CodJudet { get; set; } - public int NumarSectie { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/ModelVersiune.cs b/private-api/app/src/VotingIrregularities.Api/Models/ModelVersiune.cs deleted file mode 100644 index 3f813601..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Models/ModelVersiune.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace VotingIrregularities.Api.Models -{ - public class ModelVersiune - { - public Dictionary Versiune { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Program.cs b/private-api/app/src/VotingIrregularities.Api/Program.cs deleted file mode 100644 index ad2369a0..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; - -namespace VotingIrregularities.Api -{ - public class Program - { - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/FileQueryHandler.cs b/private-api/app/src/VotingIrregularities.Api/Queries/FileQueryHandler.cs deleted file mode 100644 index 953376ce..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Queries/FileQueryHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.IO; -using MediatR; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Api.Models; -using System.Threading.Tasks; - -namespace VotingIrregularities.Api.Queries -{ - public class FileQueryHandler : AsyncRequestHandler - { - IFileService _fileService; - - public FileQueryHandler(IFileService fileService) - { - _fileService = fileService; - } - - /// - /// Uploads a file in azure blob storage - /// - /// The url of the blob - protected override async Task HandleCore(ModelFile message) - { - if(message.File != null) - return await _fileService.UploadFromStreamAsync(message.File.OpenReadStream(), message.File.ContentType,Path.GetExtension(message.File.FileName)); - - return string.Empty; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/FormVersionQueryHandler.cs b/private-api/app/src/VotingIrregularities.Api/Queries/FormVersionQueryHandler.cs deleted file mode 100644 index 2eb787e2..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Queries/FormVersionQueryHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Domain.Models; - -namespace VotingIrregularities.Api.Queries -{ - public class FormVersionQueryHandler : AsyncRequestHandler> - { - - private readonly VotingContext _context; - private readonly IMapper _mapper; - private readonly ICacheService _cacheService; - - public FormVersionQueryHandler(VotingContext context, IMapper mapper, ICacheService cacheService) - { - _context = context; - _mapper = mapper; - _cacheService = cacheService; - } - protected override async Task> HandleCore(FormVersionQuery message) - { - var result = await _context.FormVersions - .AsNoTracking() - .ToListAsync(); - - return result.ToDictionary(k => k.Code, v => v.CurrentVersion); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/FormularQueryHandler.cs b/private-api/app/src/VotingIrregularities.Api/Queries/FormularQueryHandler.cs deleted file mode 100644 index 07a368cf..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Queries/FormularQueryHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Distributed; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Domain.Models; - -namespace VotingIrregularities.Api.Queries -{ - public class FormularQueryHandler : - AsyncRequestHandler> - { - private readonly VotingContext _context; - private readonly IMapper _mapper; - private readonly ICacheService _cacheService; - - public FormularQueryHandler(VotingContext context, IMapper mapper, ICacheService cacheService) - { - _context = context; - _mapper = mapper; - _cacheService = cacheService; - } - - protected override async Task> HandleCore(FormQuestionsQuery message) - { - CacheObjectsName formular; - Enum.TryParse("Formular" + message.CodFormular, out formular); - - return await _cacheService.GetOrSaveDataInCacheAsync>(formular, - async () => - { - var r = await _context.Questions - .Include(a => a.FormSection) - .Include(a => a.OptionsToQuestions) - .ThenInclude(a => a.Option) - .Where(a => a.FormCode == message.CodFormular) - .ToListAsync(); - - var sectiuni = r.Select(a => new { IdSectiune = a.IdSection, CodSectiune = a.FormSection.Code, Descriere = a.FormSection.Description }).Distinct(); - - var result = sectiuni.Select(i => new ModelSectiune - { - CodSectiune = i.CodSectiune, - Descriere = i.Descriere, - Intrebari = r.Where(a => a.IdSection == i.IdSectiune) - .OrderBy(intrebare => intrebare.Code) - .Select(a => _mapper.Map(a)).ToList() - }).ToList(); - return result; - }, - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = new TimeSpan(message.CacheHours, message.CacheMinutes, message.CacheMinutes) - }); - } - } -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/RaspunsQueryHandler.cs b/private-api/app/src/VotingIrregularities.Api/Queries/RaspunsQueryHandler.cs deleted file mode 100644 index 45e97dec..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Queries/RaspunsQueryHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using MediatR; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Domain.RaspunsAggregate.Commands; - -namespace VotingIrregularities.Api.Queries -{ - /// - /// Hidrateaza sectiile de votare din comanda data de observator. - /// - public class RaspunsQueryHandler : - AsyncRequestHandler - { - private readonly IPollingStationService _pollingStationService; - - public RaspunsQueryHandler(IPollingStationService svService) - { - _pollingStationService = svService; - } - - protected override async Task HandleCore(RaspunsuriBulk message) - { - // se identifica sectiile in care observatorul a raspuns - var sectii = message.ModelRaspunsuriBulk - .Select(a => new { a.NumarSectie, a.CodJudet }) - .Distinct() - .ToList(); - - var command = new CompleteazaRaspunsCommand { IdObservator = message.IdObservator }; - - - foreach (var sectie in sectii) - { - var idSectie = await _pollingStationService.GetPollingStationByCountyCode(sectie.NumarSectie, sectie.CodJudet); - - command.Raspunsuri.AddRange(message.ModelRaspunsuriBulk - .Where(a => a.NumarSectie == sectie.NumarSectie && a.CodJudet == sectie.CodJudet) - .Select(a => new ModelRaspuns - { - CodFormular = a.CodFormular, - IdIntrebare = a.IdIntrebare, - IdSectie = idSectie, - Optiuni = a.Optiuni, - NumarSectie = a.NumarSectie, - CodJudet = a.CodJudet - })); - } - - return command; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/SectieQueryHandler.cs b/private-api/app/src/VotingIrregularities.Api/Queries/SectieQueryHandler.cs deleted file mode 100644 index 45e95126..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Queries/SectieQueryHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MediatR; -using System.Threading.Tasks; -using VotingIrregularities.Api.Models; -using VotingIrregularities.Api.Services; - -namespace VotingIrregularities.Api.Queries -{ - public class SectieQueryHandler : AsyncRequestHandler - { - private readonly IPollingStationService _pollingStationService; - - public SectieQueryHandler(IPollingStationService svService) - { - _pollingStationService = svService; - } - - protected override async Task HandleCore(ModelSectieQuery message) - { - return await _pollingStationService.GetPollingStationByCountyCode(message.NumarSectie, message.CodJudet); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/ICacheService.cs b/private-api/app/src/VotingIrregularities.Api/Services/ICacheService.cs deleted file mode 100644 index a512448c..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Services/ICacheService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; - -namespace VotingIrregularities.Api.Services -{ - /// - /// Interface for the caching service to be used. - /// - public interface ICacheService - { - Task GetOrSaveDataInCacheAsync(CacheObjectsName name, Func> source, DistributedCacheEntryOptions options = null); - Task GetObjectSafeAsync(CacheObjectsName name); - Task SaveObjectSafeAsync(CacheObjectsName name, object value, DistributedCacheEntryOptions options = null); - - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/IPollingStationService.cs b/private-api/app/src/VotingIrregularities.Api/Services/IPollingStationService.cs deleted file mode 100644 index 4ae96db2..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Services/IPollingStationService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace VotingIrregularities.Api.Services -{ - public interface IPollingStationService - { - Task GetPollingStationByCountyCode(int pollingStationNumber, string countyCode); - Task GetPollingStationByCountyId(int pollingStationNumber, int countyId); - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/NoCacheService.cs b/private-api/app/src/VotingIrregularities.Api/Services/NoCacheService.cs deleted file mode 100644 index 6aeee610..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Services/NoCacheService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace VotingIrregularities.Api.Services -{ - public class NoCacheService : ICacheService - { - public async Task GetOrSaveDataInCacheAsync(CacheObjectsName name, Func> source, - DistributedCacheEntryOptions options = null) - { - return await source(); - } - - public Task GetObjectSafeAsync(CacheObjectsName name) => throw new NotImplementedException(); - - public Task SaveObjectSafeAsync(CacheObjectsName name, object value, - DistributedCacheEntryOptions options = null) => throw new NotImplementedException(); - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/PollingStationService.cs b/private-api/app/src/VotingIrregularities.Api/Services/PollingStationService.cs deleted file mode 100644 index 4d0ac7bc..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Services/PollingStationService.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using VotingIrregularities.Domain.Models; - -namespace VotingIrregularities.Api.Services -{ - public class PollingStationService : IPollingStationService - { - private readonly VotingContext _context; - private readonly ILogger _logger; - - public PollingStationService(VotingContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - public async Task GetPollingStationByCountyCode(int pollingStationNumber, string countyCode) - { - try - { - var countyId = _context.Counties.FirstOrDefault(c => c.Code == countyCode)?.Id; - if (countyId == null) - throw new ArgumentException($"Could not find County with code: {countyCode}"); - - return await GetPollingStationByCountyId(pollingStationNumber, countyId.Value); - } - catch (Exception ex) - { - _logger.LogError(new EventId(), ex.Message); - } - - return -1; - } - - public async Task GetPollingStationByCountyId(int pollingStationNumber, int countyId) - { - try - { - var idSectie = await - _context.PollingStations - .Where(a => a.IdCounty == countyId && - a.Number == pollingStationNumber) - .Select(a => a.Id) - .ToListAsync(); - - if (idSectie.Count == 0) - throw new ArgumentException($"No Polling station found for: {new { countyId, pollingStationNumber }}"); - - - if (idSectie.Count > 1) // TODO[bv] add unique constraint on PollingStations [CountyId, Number] - throw new ArgumentException($"More than one polling station found for: {new { countyId, idSectie }}"); - - return idSectie.Single(); - } - catch (Exception ex) - { - _logger.LogError(new EventId(), ex.Message); - } - - return -1; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Api/Startup.cs b/private-api/app/src/VotingIrregularities.Api/Startup.cs deleted file mode 100644 index 74bd313e..00000000 --- a/private-api/app/src/VotingIrregularities.Api/Startup.cs +++ /dev/null @@ -1,407 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using AutoMapper; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.ViewComponents; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.PlatformAbstractions; -using Serilog; -using VotingIrregularities.Api.Extensions; -using SimpleInjector; -using SimpleInjector.Integration.AspNetCore.Mvc; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Domain.Models; -using ILogger = Microsoft.Extensions.Logging.ILogger; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using VotingIrregularities.Api.Models.AccountViewModels; -using VotingIrregularities.Api.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using SimpleInjector.Lifestyles; -using Swashbuckle.AspNetCore.Swagger; -using VotingIrregularities.Api.Options; - -namespace VotingIrregularities.Api -{ - public class Startup - { - private readonly Container _container = new Container() { Options = { DefaultLifestyle = Lifestyle.Scoped, DefaultScopedLifestyle = new AsyncScopedLifestyle() } }; - private SymmetricSecurityKey _key; - - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - - if (env.EnvironmentName.EndsWith("Development", StringComparison.CurrentCultureIgnoreCase)) - { - // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 - builder.AddUserSecrets(); - - // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately. - builder.AddApplicationInsightsSettings(developerMode: true); - } - - builder.AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - public void ConfigureCustomOptions(IServiceCollection services) - { - services.Configure(Configuration.GetSection("BlobStorageOptions")); - services.Configure(Configuration.GetSection("HashOptions")); - services.Configure(Configuration.GetSection("MobileSecurity")); - services.Configure(Configuration.GetSection(nameof(FileServiceOptions))); - - } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // Get options from app settings - services.AddOptions(); - - ConfigureCustomOptions(services); - - var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions)); - - _key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["SecretKey"])); - - // Configure JwtIssuerOptions - services.Configure(options => - { - options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; - options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; - options.SigningCredentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256); - }); - - var tokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], - - ValidateAudience = true, - ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], - - ValidateIssuerSigningKey = true, - IssuerSigningKey = _key, - - RequireExpirationTime = false, - ValidateLifetime = false, - - ClockSkew = TimeSpan.Zero - }; - - services.AddAuthentication(options => - { - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; - options.RequireHttpsMetadata = false; - options.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; - options.TokenValidationParameters = tokenValidationParameters; - }); - - services.AddAuthorization(options => - { - options.AddPolicy("AppUser", - policy => policy.RequireClaim("Organizatie", "Ong")); - }); - - services.AddApplicationInsightsTelemetry(Configuration); - - services.AddMvc(config => - { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .Build(); - config.Filters.Add(new AuthorizeFilter(policy)); - }); - - services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new Info - { - Version = "v1", - Title = "Monitorizare Vot - API privat", - Description = "API care ofera suport aplicatiilor folosite de observatori.", - TermsOfService = "TBD", - Contact = - new Contact - { - Email = "info@monitorizarevot.ro", - Name = "Code for Romania", - Url = "http://monitorizarevot.ro" - }, - }); - - options.AddSecurityDefinition("bearer", new ApiKeyScheme() - { - Name = "Authorization", - In = "header", - Type = "apiKey" - }); - options.AddSecurityRequirement(new Dictionary>{ - { "bearer", new[] {"readAccess", "writeAccess" } } }); - - options.OperationFilter(); - - var path = PlatformServices.Default.Application.ApplicationBasePath + - System.IO.Path.DirectorySeparatorChar + "VotingIrregularities.Api.xml"; - - if (System.IO.File.Exists(path)) - options.IncludeXmlComments(path); - }); - - services.UseSimpleInjectorAspNetRequestScoping(_container); - - ConfigureContainer(services); - - ConfigureCache(services); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, - IApplicationLifetime appLifetime, IDistributedCache cache) - { - app.UseStaticFiles(); - - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - loggerFactory.AddSerilog(); - Log.Logger = new LoggerConfiguration() - .WriteTo - .ApplicationInsightsTraces(Configuration["ApplicationInsights:InstrumentationKey"]) - .CreateLogger(); - // app.UseApplicationInsightsRequestTelemetry(); - - appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); - - app.UseExceptionHandler( - builder => - { - builder.Run(context => - { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - context.Response.ContentType = "application/json"; - return Task.FromResult(0); - } - ); - } - ); - - app.UseAuthentication(); - - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - - RegisterServices(app); - - ConfigureFileService(app); - - ConfigureHash(app); - - InitializeContainer(app); - - RegisterDbContext(Configuration.GetConnectionString("DefaultConnection")); - - RegisterAutomapper(); - - BuildMediator(); - - _container.Verify(); - - // Enable middleware to serve generated Swagger as a JSON endpoint - app.UseSwagger(); - - // Enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.) - app.UseSwaggerUI(o => o.SwaggerEndpoint("/swagger/v1/swagger.json", "MV API v1")); - - app.UseMvc(); - } - - private void ConfigureCache(IServiceCollection services) - { - var enableCache = Configuration.GetValue("ApplicationCacheOptions:Enabled"); - - if (!enableCache) - { - _container.RegisterInstance(new NoCacheService()); - return; - } - - var cacheProvider = Configuration.GetValue("ApplicationCacheOptions:Implementation"); - - - _container.RegisterSingleton(); - - switch (cacheProvider) - { - case "RedisCache": - { - - services.AddDistributedRedisCache(options => - { - Configuration.GetSection("RedisCacheOptions").Bind(options); - }); - - break; - } - - default: - case "MemoryDistributedCache": - { - - services.AddDistributedMemoryCache(); - break; - } - } - } - - private void ConfigureFileService(IApplicationBuilder app) - { - var fileServiceOptions = new FileServiceOptions(); - Configuration.GetSection(nameof(FileServiceOptions)).Bind(fileServiceOptions); - - if (fileServiceOptions.Type == "LocalFileService") - { - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - _container.RegisterSingleton(); - } - else - ConfigureAzureStorage(app); - } - - private void ConfigureAzureStorage(IApplicationBuilder app) - { - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - _container.RegisterSingleton(); - } - - private void ConfigureHash(IApplicationBuilder app) - { - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - - var hashOptions = new HashOptions(); - Configuration.GetSection(nameof(HashOptions)).Bind(hashOptions); - - if (hashOptions.ServiceType == nameof(HashServiceType.ClearText)) - _container.RegisterSingleton(); - else - _container.RegisterSingleton(); - } - - private void ConfigureContainer(IServiceCollection services) - { - services.AddSingleton( - new SimpleInjectorControllerActivator(_container)); - services.AddSingleton( - new SimpleInjectorViewComponentActivator(_container)); - } - - private void RegisterServices(IApplicationBuilder app) - { - _container.Register(Lifestyle.Scoped); - _container.RegisterSingleton(() => app.ApplicationServices.GetService>()); - } - - private void InitializeContainer(IApplicationBuilder app) - { - // Add application presentation components: - _container.RegisterMvcControllers(app); - _container.RegisterMvcViewComponents(app); - - // Add application services. For instance: - //container.Register(Lifestyle.Scoped); - - - // Cross-wire ASP.NET services (if any). For instance: - _container.RegisterInstance(app.ApplicationServices.GetService()); - _container.RegisterConditional( - typeof(ILogger), - c => typeof(Logger<>).MakeGenericType(c.Consumer.ImplementationType), - Lifestyle.Singleton, - c => true); - - // NOTE: Prevent cross-wired instances as much as possible. - // See: https://simpleinjector.org/blog/2016/07/ - - _container.RegisterInstance(Configuration); - } - - private void RegisterDbContext(string connectionString = null) - where TDbContext : DbContext - { - if (!string.IsNullOrEmpty(connectionString)) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(connectionString); - - _container.RegisterInstance(optionsBuilder.Options); - - _container.Register(Lifestyle.Scoped); - } - else - { - _container.Register(Lifestyle.Scoped); - } - } - - private IMediator BuildMediator() - { - - var assemblies = GetAssemblies().ToArray(); - _container.RegisterSingleton(); - _container.Register(typeof(IRequestHandler<,>), assemblies); - _container.Register(typeof(AsyncRequestHandler<,>), assemblies); - _container.Collection.Register(typeof(INotificationHandler<>), assemblies); - _container.Collection.Register(typeof(AsyncNotificationHandler<>), assemblies); - - // had to add this registration as we were getting the same behavior as described here: https://github.com/jbogard/MediatR/issues/155 - _container.Collection.Register(typeof(IPipelineBehavior<,>), Enumerable.Empty()); - - _container.RegisterInstance(Console.Out); - _container.RegisterInstance(new SingleInstanceFactory(_container.GetInstance)); - _container.RegisterInstance(new MultiInstanceFactory(_container.GetAllInstances)); - - var mediator = _container.GetInstance(); - - return mediator; - } - - private void RegisterAutomapper() - { - Mapper.Initialize(cfg => { cfg.AddProfiles(GetAssemblies()); }); - - _container.RegisterInstance(Mapper.Configuration); - _container.Register(() => new Mapper(Mapper.Configuration), Lifestyle.Scoped); - } - - private static IEnumerable GetAssemblies() - { - yield return typeof(IMediator).GetTypeInfo().Assembly; - yield return typeof(Startup).GetTypeInfo().Assembly; - yield return typeof(VotingContext).GetTypeInfo().Assembly; - // just to identify VotingIrregularities.Domain assembly - } - } -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/VotingIrregularities.Api.csproj b/private-api/app/src/VotingIrregularities.Api/VotingIrregularities.Api.csproj deleted file mode 100644 index d5d30858..00000000 --- a/private-api/app/src/VotingIrregularities.Api/VotingIrregularities.Api.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - netcoreapp2.1 - true - true - VotingIrregularities.Api - Exe - VotingIrregularities.Api - TBRWithASecretkey - 2.1.0 - - - - AnyCPU - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/private-api/app/src/VotingIrregularities.Api/appsettings.json b/private-api/app/src/VotingIrregularities.Api/appsettings.json deleted file mode 100644 index 501acb39..00000000 --- a/private-api/app/src/VotingIrregularities.Api/appsettings.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "ApplicationCacheOptions": { - "Enabled": false, - "Implementation": "RedisCache" // MemoryDistributedCache or RedisCache - }, - "ApplicationInsights": { - "InstrumentationKey": "TBR" - }, - "BlobStorageOptions": { - "Container": "TBR", - "AccountName": "TBR", - "AccountKey": "TBR", - "UseHttps": true - }, - "ConnectionStrings": { - "DefaultConnection": "TBR" - }, - // CACHE KEY VALABILITY - "DefaultCacheHours": 0, - "DefaultCacheMinutes": 30, - "DefaultCacheSeconds": 0, - "FileServiceOptions": { - "Type": "LocalFileService", /* LocalFileStorage for.. well.. local file storage. Anything else for Azure Blob Storage (until further implementations)*/ - "StoragePath": "\\notes" - }, - "HashOptions": { - "Salt": "", - "ServiceType": "ClearText" /* Can be set to "Hash" or "ClearText" */ - }, - "JwtIssuerOptions": { - "Issuer": "MonitorizareVotTokenServer", - "Audience": "http://localhost:53413/" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, - "MobileSecurity": { - "InvalidCredentialsErrorMessage": "Invalid Credentials for user {0}.", - "LockDevice": false - }, - "RedisCacheOptions": { - "Configuration": "TBR", - "InstanceName": "TBR" - }, - "SecretKey": "SuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBRSuperSecretKetyTBR" -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Domain/Models/FormVersion.cs b/private-api/app/src/VotingIrregularities.Domain/Models/FormVersion.cs deleted file mode 100644 index c75df7d2..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Models/FormVersion.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace VotingIrregularities.Domain.Models -{ - public partial class FormVersion - { - public string Code { get; set; } - public int CurrentVersion { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/Models/ReadMe.md b/private-api/app/src/VotingIrregularities.Domain/Models/ReadMe.md deleted file mode 100644 index 2aa3098b..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Models/ReadMe.md +++ /dev/null @@ -1,7 +0,0 @@ -## Regenerare entitati - -Din folderul proiectului VotingIrregularities.Domain se ruleaza : - -1. dotnet ef --startup-project ../VotingIrregularities.Api dbcontext scaffold "{connection string}" Microsoft.EntityFrameworkCore.SqlServer --context VotingContext --force --output-dir ./Models - -2. se sterge connectionstring din VotingContext \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextConfiguration.cs b/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextConfiguration.cs deleted file mode 100644 index 34490e76..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextConfiguration.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Design; - -namespace VotingIrregularities.Domain.Models -{ - /// - /// used only on migrations - /// - public class VotingContextConfiguration : IDesignTimeDbContextFactory - { - //public VotingContext Create(DbContextFactoryOptions options) - //{ - // var builder = new DbContextOptionsBuilder(); - // builder.UseSqlServer(Startup.RegisterConfiguration().GetConnectionString("DefaultConnection")); - // return new VotingContext(builder.Options); - //} - - public VotingContext CreateDbContext(string[] args) - { - throw new NotImplementedException(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextExtension.cs b/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextExtension.cs deleted file mode 100644 index b9085be9..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Models/VotingContextExtension.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace VotingIrregularities.Domain.Models -{ - public partial class VotingContext : DbContext - { - public VotingContext(DbContextOptions options) - :base(options) - { - - } - } -} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaCommand.cs b/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaCommand.cs deleted file mode 100644 index 31cac47a..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using AutoMapper; -using MediatR; -using System; -using VotingIrregularities.Domain.Models; - -namespace VotingIrregularities.Domain.NotaAggregate -{ - public class AdaugaNotaCommand : IRequest - { - public int IdObservator { get; set; } - public int IdSectieDeVotare { get; set; } - public int? IdIntrebare { get; set; } - public string TextNota { get; set; } - public string CaleFisierAtasat { get; set; } - } - - public class NotaProfile : Profile - { - public NotaProfile() - { - CreateMap() - .ForMember(dest => dest.IdQuestion, c => c.MapFrom(src => - !src.IdIntrebare.HasValue || src.IdIntrebare.Value <= 0 ? null : src.IdIntrebare) - ) - .ForMember(dest => dest.LastModified, c => c.MapFrom(src => DateTime.UtcNow)) - .ForMember(dest => dest.AttachementPath, c => c.MapFrom(src => src.CaleFisierAtasat)) - .ForMember(dest => dest.IdObserver, c => c.MapFrom(src => src.IdObservator)) - .ForMember(dest => dest.IdPollingStation, c => c.MapFrom(src => src.IdSectieDeVotare)) - .ForMember(dest => dest.Text, c => c.MapFrom(src => src.TextNota)); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaHandler.cs b/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaHandler.cs deleted file mode 100644 index b013eb20..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/NotaAggregate/AdaugaNotaHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading.Tasks; -using MediatR; -using VotingIrregularities.Domain.Models; -using Microsoft.Extensions.Logging; -using AutoMapper; -using System; -using System.Threading; -using Microsoft.EntityFrameworkCore; - -namespace VotingIrregularities.Domain.NotaAggregate -{ - public class AdaugaNotaHandler : IRequestHandler - { - private readonly VotingContext _context; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public AdaugaNotaHandler(VotingContext context, ILogger logger, IMapper mapper) - { - _context = context; - _logger = logger; - _mapper = mapper; - } - - public async Task Handle(AdaugaNotaCommand message, CancellationToken token) - { - try - { - if (message.IdIntrebare.HasValue && message.IdIntrebare.Value > 0) - { - var existaIntrebare = await _context.Questions.AnyAsync(i => i.Id == message.IdIntrebare.Value, token); - - if(!existaIntrebare) - throw new ArgumentException("Intrebarea nu exista"); - } - - var nota = _mapper.Map(message); - _context.Add(nota); - - return await _context.SaveChangesAsync(token); - } - catch (Exception ex) - { - _logger.LogError(new EventId(), ex.Message); - } - - return -1; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/Program.cs b/private-api/app/src/VotingIrregularities.Domain/Program.cs deleted file mode 100644 index edb40286..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Program.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using VotingIrregularities.Domain.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace VotingIrregularities.Domain -{ - public class Program - { - - public IConfigurationRoot Configuration { get; } - - public static void Main(string[] args) - { - var configuration = Startup.RegisterConfiguration(); - ILoggerFactory loggerFactory = new LoggerFactory(); - var logger = loggerFactory.CreateLogger("Ef Migrations"); - loggerFactory.AddConsole(configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - IServiceCollection services = new ServiceCollection(); - - services.AddSingleton(loggerFactory); - var conn = configuration.GetConnectionString("DefaultConnection"); - - - services.AddDbContext(options => options.UseSqlServer(conn)); - - IServiceProvider provider = services.BuildServiceProvider(); - - logger.LogDebug($"Initialized Context with {conn}"); - - - using (var serviceScope = provider.GetService().CreateScope()) - { - var context = serviceScope.ServiceProvider.GetService(); - logger.LogDebug($"Initializing Database for VotingContext..."); - context.Database.EnsureCreated(); - //context.Database.Migrate(); - logger.LogDebug($"Database created"); - - if (!args.Contains("-seed")) return; - - logger.LogDebug($"Initializing data seeding..."); - context.EnsureSeedData(); - logger.LogDebug($"Data seeded for {conn}"); - - } - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/Properties/AssemblyInfo.cs b/private-api/app/src/VotingIrregularities.Domain/Properties/AssemblyInfo.cs deleted file mode 100644 index 90534703..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("VotingIrregularities.Domain")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("73125cac-66e8-40d7-81a7-6a847132c7b3")] diff --git a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/CompleteazaRaspunsCommand.cs b/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/CompleteazaRaspunsCommand.cs deleted file mode 100644 index de55ad97..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/CompleteazaRaspunsCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using MediatR; - -namespace VotingIrregularities.Domain.RaspunsAggregate.Commands -{ - public class CompleteazaRaspunsCommand : IRequest - { - public CompleteazaRaspunsCommand() - { - Raspunsuri = new List(); - } - public int IdObservator { get; set; } - public List Raspunsuri { get; set; } - - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelOptiuniSelectate.cs b/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelOptiuniSelectate.cs deleted file mode 100644 index 32248244..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelOptiuniSelectate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace VotingIrregularities.Domain.RaspunsAggregate.Commands -{ - public class ModelOptiuniSelectate - { - public int IdOptiune { get; set; } - public string Value { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelRaspuns.cs b/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelRaspuns.cs deleted file mode 100644 index a8bccd66..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/Commands/ModelRaspuns.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace VotingIrregularities.Domain.RaspunsAggregate.Commands -{ - public class ModelRaspuns - { - public int IdIntrebare { get; set; } - public int IdSectie { get; set; } - public string CodFormular { get; set; } - public string CodJudet { get; set; } - public int NumarSectie { get; set; } - public List Optiuni { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/CompleteazaRaspunsHandler.cs b/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/CompleteazaRaspunsHandler.cs deleted file mode 100644 index 1e95fb0f..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/RaspunsAggregate/CompleteazaRaspunsHandler.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using AutoMapper; -using LinqKit; -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using VotingIrregularities.Domain.Models; -using VotingIrregularities.Domain.RaspunsAggregate.Commands; -using Z.EntityFramework.Plus; - -namespace VotingIrregularities.Domain.RaspunsAggregate -{ - public class CompleteazaRaspunsHandler : AsyncRequestHandler - { - private readonly VotingContext _context; - private readonly IMapper _mapper; - private readonly ILogger _logger; - - public CompleteazaRaspunsHandler(VotingContext context, IMapper mapper, ILogger logger) - { - _context = context; - _mapper = mapper; - _logger = logger; - } - protected override async Task HandleCore(CompleteazaRaspunsCommand message) - { - try - { - //flat answers - var lastModified = DateTime.UtcNow; - - var raspunsuriNoi = message.Raspunsuri.Select(a => new - { - flat = a.Optiuni.Select(o => new Answer - { - IdObserver = message.IdObservator, - IdPollingStation = a.IdSectie, - IdOptionToQuestion = o.IdOptiune, - Value = o.Value, - CountyCode = a.CodJudet, - PollingStationNumber = a.NumarSectie, - LastModified = lastModified - }) - }).SelectMany(a => a.flat) - .Distinct() - .ToList(); - - // stergerea este pe fiecare sectie - var sectii = message.Raspunsuri.Select(a => a.IdSectie).Distinct().ToList(); - - using (var tran = await _context.Database.BeginTransactionAsync()) - { - foreach (var sectie in sectii) - { - - var intrebari = message.Raspunsuri.Select(a => a.IdIntrebare).Distinct().ToList(); - - // delete existing answers for posted questions on this 'sectie' - _context.Answers - .Include(a => a.OptionAnswered) - .Where( - a => - a.IdObserver == message.IdObservator && - a.IdPollingStation == sectie) - .WhereRaspunsContains(intrebari) - .Delete(); - } - - _context.Answers.AddRange(raspunsuriNoi); - - var result = await _context.SaveChangesAsync(); - - tran.Commit(); - - return result; - } - } - catch (Exception ex) - { - _logger.LogError(typeof(CompleteazaRaspunsCommand).GetHashCode(), ex, ex.Message); - } - - return await Task.FromResult(-1); - } - } - - public static class EfBuilderExtensions - { - /// - /// super simple and dumb translation of .Contains because is not supported pe EF plus - /// this translates to contains in EF SQL - /// - /// - /// - /// - public static IQueryable WhereRaspunsContains(this IQueryable source, IList contains) - { - var ors = contains - .Aggregate>>(null, (expression, id) => - expression == null - ? (a => a.OptionAnswered.IdQuestion == id) - : expression.Or(a => a.OptionAnswered.IdQuestion == id)); - - return source.Where(ors); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieCommand.cs b/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieCommand.cs deleted file mode 100644 index 8e4a42c0..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MediatR; -using System; - -namespace VotingIrregularities.Domain.SectieAggregate -{ - public class ActualizeazaSectieCommand : IRequest - { - public int IdObservator { get; set; } - public int IdSectieDeVotare { get; set; } - public DateTime OraPlecarii { get; set; } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieHandler.cs b/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieHandler.cs deleted file mode 100644 index ee6b7be0..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/ActualizeazaSectieHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using VotingIrregularities.Domain.Models; -using Microsoft.Extensions.Logging; -using AutoMapper; -using Microsoft.EntityFrameworkCore; - -namespace VotingIrregularities.Domain.SectieAggregate -{ - public class ActualizeazaSectieHandler : AsyncRequestHandler - { - private readonly VotingContext _context; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public ActualizeazaSectieHandler(VotingContext context, ILogger logger, IMapper mapper) - { - _context = context; - _logger = logger; - _mapper = mapper; - } - - protected override async Task HandleCore(ActualizeazaSectieCommand message) - { - try - { - var formular = await _context.PollingStationInfos - .FirstOrDefaultAsync(a => - a.IdObserver == message.IdObservator && - a.IdPollingStation == message.IdSectieDeVotare); - - if (formular == null) - throw new ArgumentException("PollingStationInfo nu exista"); - - _mapper.Map(message, formular); - _context.Update(formular); - - return await _context.SaveChangesAsync(); - - } - catch (Exception ex) - { - _logger.LogError(new EventId(), ex.Message); - } - - return -1; - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/InregistreazaSectieCommand.cs b/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/InregistreazaSectieCommand.cs deleted file mode 100644 index 4e461bd2..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/SectieAggregate/InregistreazaSectieCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using AutoMapper; -using MediatR; -using VotingIrregularities.Domain.Models; - -namespace VotingIrregularities.Domain.SectieAggregate -{ - public class InregistreazaSectieCommand : IRequest - { - public int IdObservator { get; set; } - public string CodJudet { get; set; } - public int NumarSectie { get; set; } - public DateTime? OraSosirii { get; set; } - public DateTime? OraPlecarii { get; set; } - public bool? EsteZonaUrbana { get; set; } - public bool? PresedinteBesvesteFemeie { get; set; } - } - - public class RaspunsFormularProfile : Profile - { - public RaspunsFormularProfile() - { - CreateMap() - .ForMember(dest => dest.IdObserver, c => c.MapFrom(src => src.IdObservator)) - .ForMember(dest => dest.LastModified, c => c.MapFrom(src => DateTime.UtcNow)) - .ForMember(dest => dest.UrbanArea, c => c.MapFrom(src => src.EsteZonaUrbana)) - .ForMember(dest => dest.ObserverArrivalTime, c => c.MapFrom(src => src.OraSosirii)) - .ForMember(dest => dest.ObserverLeaveTime, c => c.MapFrom(src => src.OraPlecarii)) - .ForMember(dest => dest.IsPollingStationPresidentFemale, c => c.MapFrom(src => src.PresedinteBesvesteFemeie)); - - CreateMap() - .ForMember(dest => dest.LastModified, c => c.MapFrom(src => DateTime.UtcNow)) - .ForMember(dest => dest.ObserverLeaveTime, c => c.MapFrom(src => src.OraPlecarii)); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/Startup.cs b/private-api/app/src/VotingIrregularities.Domain/Startup.cs deleted file mode 100644 index ed395bf9..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/Startup.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.IO; -using Microsoft.Extensions.Configuration; - -namespace VotingIrregularities.Domain -{ - public class Startup - { - public static IConfigurationRoot RegisterConfiguration() - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile("appsettings.development.json", optional: true); - - return builder.Build(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/ValueObjects/JudetEnum.cs b/private-api/app/src/VotingIrregularities.Domain/ValueObjects/JudetEnum.cs deleted file mode 100644 index a5811631..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/ValueObjects/JudetEnum.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace VotingIrregularities.Domain.ValueObjects -{ - public enum JudetEnum - { - AB = 1, - AR = 2, - AG = 3, - BC = 4, - BH = 5, - BN = 6, - BT = 7, - BR = 8, - BV = 9, - B = 10, - BZ = 11, - CL = 12, - CS = 13, - CJ = 14, - CT = 15, - CV = 16, - DB = 17, - DJ = 18, - GL = 19, - GR = 20, - GJ = 21, - HR = 22, - HD = 23, - IL = 24, - IS = 25, - IF = 26, - MM = 27, - MH = 28, - MS = 29, - NT = 30, - OT = 31, - PH = 32, - SJ = 33, - SM = 34, - SB = 35, - SV = 36, - TR = 37, - TM = 38, - TL = 39, - VL = 40, - VS = 41, - VN = 42, - D = 43 - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/ValueObjects/TipIntrebareEnum.cs b/private-api/app/src/VotingIrregularities.Domain/ValueObjects/TipIntrebareEnum.cs deleted file mode 100644 index ce5e5b40..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/ValueObjects/TipIntrebareEnum.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace VotingIrregularities.Domain.ValueObjects -{ - public enum QuestionType - { - MultipleOption = 0, - SingleOption = 1, - SingleOptionWithText = 2, - MultipleOptionWithText = 3 - } - public struct TipIntrebareEnum - { - /// - /// (0) se pot alege optiuni multiple - /// - public static int OptiuniMultiple = 0; - - /// - /// (1) se alege o singura optiune selectabila - /// - public static int OSinguraOptiune = 1; - - /// - /// (2) se poate alege O singura optiune selectabila + text pe O singura optiune - /// - public static int OSinguraOptiuneCuText = 2; - - /// - /// (3) se pot alege mai multe optiuni + text pe o singura optiune - /// - public static int OptiuniMultipleCuText = 3; - - - - - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/VotingContextExtensions.cs b/private-api/app/src/VotingIrregularities.Domain/VotingContextExtensions.cs deleted file mode 100644 index 54b17746..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/VotingContextExtensions.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using VotingIrregularities.Domain.Models; -using VotingIrregularities.Domain.ValueObjects; - -namespace VotingIrregularities.Domain -{ - public static class VotingContextExtensions - { - public static void EnsureSeedData(this VotingContext context) - { - if (!context.AllMigrationsApplied()) - return; - - using (var tran = context.Database.BeginTransaction()) - { - context.DataCleanUp(); - - context.SeedNGOs(); - context.SeedVersions(); - context.SeedCounties(); - context.SeedFormSections(); - context.SeedOptions(); - context.SeedQuestions('A'); - context.SeedQuestions('B'); - context.SeedQuestions('C'); - - tran.Commit(); - } - } - - private static void SeedCounties(this VotingContext context) - { - if (context.Counties.Any()) - return; - - context.Counties.AddRange( - new County { Id = 0, Code = "AB", Name = "ALBA" }, - new County { Id = 1, Code = "AR", Name = "ARAD" }, - new County { Id = 2, Code = "AG", Name = "ARGES" }, - new County { Id = 3, Code = "BC", Name = "BACAU" }, - new County { Id = 4, Code = "BH", Name = "BIHOR" }, - new County { Id = 5, Code = "BN", Name = "BISTRITA-NASAUD" }, - new County { Id = 6, Code = "BT", Name = "BOTOSANI" }, - new County { Id = 7, Code = "BV", Name = "BRASOV" }, - new County { Id = 8, Code = "BR", Name = "BRAILA" }, - new County { Id = 9, Code = "BZ", Name = "BUZAU" }, - new County { Id = 10, Code = "CS", Name = "CARAS-SEVERIN" }, - new County { Id = 11, Code = "CL", Name = "CALARASI" }, - new County { Id = 12, Code = "CJ", Name = "CLUJ" }, - new County { Id = 13, Code = "CT", Name = "CONSTANTA" }, - new County { Id = 14, Code = "CV", Name = "COVASNA" }, - new County { Id = 15, Code = "DB", Name = "DÂMBOVITA" }, - new County { Id = 16, Code = "DJ", Name = "DOLJ" }, - new County { Id = 17, Code = "GL", Name = "GALATI" }, - new County { Id = 18, Code = "GR", Name = "GIURGIU" }, - new County { Id = 19, Code = "GJ", Name = "GORJ" }, - new County { Id = 20, Code = "HR", Name = "HARGHITA" }, - new County { Id = 21, Code = "HD", Name = "HUNEDOARA" }, - new County { Id = 22, Code = "IL", Name = "IALOMITA" }, - new County { Id = 23, Code = "IS", Name = "IASI" }, - new County { Id = 24, Code = "IF", Name = "ILFOV" }, - new County { Id = 25, Code = "MM", Name = "MARAMURES" }, - new County { Id = 26, Code = "MH", Name = "MEHEDINTI" }, - new County { Id = 27, Code = "B", Name = "BUCURESTI" }, - new County { Id = 28, Code = "MS", Name = "MURES" }, - new County { Id = 29, Code = "NT", Name = "NEAMT" }, - new County { Id = 30, Code = "OT", Name = "OLT" }, - new County { Id = 31, Code = "PH", Name = "PRAHOVA" }, - new County { Id = 32, Code = "SM", Name = "SATU MARE" }, - new County { Id = 33, Code = "SJ", Name = "SALAJ" }, - new County { Id = 34, Code = "SB", Name = "SIBIU" }, - new County { Id = 35, Code = "SV", Name = "SUCEAVA" }, - new County { Id = 36, Code = "TR", Name = "TELEORMAN" }, - new County { Id = 37, Code = "TM", Name = "TIMIS" }, - new County { Id = 38, Code = "TL", Name = "TULCEA" }, - new County { Id = 39, Code = "VS", Name = "VASLUI" }, - new County { Id = 40, Code = "VL", Name = "VÂLCEA" }, - new County { Id = 41, Code = "VN", Name = "VRANCEA" }, - new County { Id = 42, Code = "D", Name = "DIASPORA" } - ); - } - - private static void DataCleanUp(this VotingContext context) - { - context.Database.ExecuteSqlCommand("delete from OptionsToQuestions"); - context.Database.ExecuteSqlCommand("delete from Questions"); - context.Database.ExecuteSqlCommand("delete from FormSections"); - context.Database.ExecuteSqlCommand("delete from FormVersions"); - context.Database.ExecuteSqlCommand("delete from Counties"); - } - - private static void SeedOptions(this VotingContext context) - { - if (context.Options.Any()) - return; - context.Options.AddRange( - new Option { Id = 1, Text = "Da", }, - new Option { Id = 2, Text = "Nu", }, - new Option { Id = 3, Text = "Nu stiu", }, - new Option { Id = 4, Text = "Dark Island", }, - new Option { Id = 5, Text = "London Pride", }, - new Option { Id = 6, Text = "Zaganu", }, - new Option { Id = 7, Text = "Transmisia manualã", }, - new Option { Id = 8, Text = "Transmisia automatã", }, - new Option { Id = 9, Text = "Altele (specificaţi)", IsFreeText = true }, - new Option { Id = 10, Text = "Metrou" }, - new Option { Id = 11, Text = "Tramvai" }, - new Option { Id = 12, Text = "Autobuz" } - ); - - context.SaveChanges(); - } - private static void SeedFormSections(this VotingContext context) - { - if (context.FormSections.Any()) - return; - - context.FormSections.AddRange( - new FormSection { Id = 1, Code = "B", Description = "Despre Bere" }, - new FormSection { Id = 2, Code = "C", Description = "Description masini" } - ); - - context.SaveChanges(); - } - - private static void SeedQuestions(this VotingContext context, char idFormular) - { - if (context.Questions.Any(a => a.FormCode == idFormular.ToString())) - return; - - context.Questions.AddRange( - // primul formular - new Question - { - Id = idFormular * 20 + 1, - FormCode = idFormular.ToString(), - IdSection = 1, //B - QuestionType = QuestionType.SingleOption, - Text = $"{idFormular}: Iti place berea? (se alege o singura optiune selectabila)", - OptionsToQuestions = new List - { - new OptionToQuestion {Id = idFormular * 20 + 1, IdOption = 1}, - new OptionToQuestion {Id = idFormular * 20 + 2, IdOption = 2, Flagged = true}, - new OptionToQuestion {Id = idFormular * 20 + 3, IdOption = 3} - } - }, - new Question - { - Id = idFormular * 20 + 2, - FormCode = idFormular.ToString(), - IdSection = 1, //B - QuestionType = QuestionType.MultipleOption, - Text = $"{idFormular}: Ce tipuri de bere iti plac? (se pot alege optiuni multiple)", - OptionsToQuestions = new List - { - new OptionToQuestion {Id = idFormular * 20 + 4, IdOption = 4, Flagged = true}, - new OptionToQuestion {Id = idFormular * 20 + 5, IdOption = 5}, - new OptionToQuestion {Id = idFormular * 20 + 6, IdOption = 6} - } - }, - new Question - { - Id = idFormular * 20 + 3, - FormCode = idFormular.ToString(), - IdSection = 2, //C - QuestionType = QuestionType.SingleOptionWithText, - Text = $"{idFormular}: Ce tip de transmisie are masina ta? (se poate alege O singura optiune selectabila + text pe O singura optiune)", - OptionsToQuestions = new List - { - new OptionToQuestion {Id = idFormular * 20 + 7, IdOption = 7, Flagged = true}, - new OptionToQuestion {Id = idFormular * 20 + 8, IdOption = 8}, - new OptionToQuestion {Id = idFormular * 20 + 9, IdOption = 9} - } - }, - new Question - { - Id = idFormular * 20 + 4, - FormCode = idFormular.ToString(), - IdSection = 2, //C - QuestionType = QuestionType.MultipleOptionWithText, - Text = $"{idFormular}: Ce mijloace de transport folosesti sa ajungi la birou? (se pot alege mai multe optiuni + text pe O singura optiune)", - OptionsToQuestions = new List - { - new OptionToQuestion {Id = idFormular * 20 + 10, IdOption = 10, Flagged = true}, - new OptionToQuestion {Id = idFormular * 20 + 11, IdOption = 11}, - new OptionToQuestion {Id = idFormular * 20 + 12, IdOption = 12}, - new OptionToQuestion {Id = idFormular * 20 + 13, IdOption = 9} - } - } - ); - - context.SaveChanges(); - - } - - private static void SeedVersions(this VotingContext context) - { - if (context.FormVersions.Any()) - return; - - context.FormVersions.AddRange( - new FormVersion { Code = "A", CurrentVersion = 1 }, - new FormVersion { Code = "B", CurrentVersion = 1 }, - new FormVersion { Code = "C", CurrentVersion = 1 } - ); - - context.SaveChanges(); - } - - private static void SeedNGOs(this VotingContext context) - { - if(context.Ngos.Any()) - return; - - context.Ngos.Add(new Ngo - { - Id = 1, Name = "Code4Romania", Organizer = true, ShortName = "C4R" - }); - context.Ngos.Add(new Ngo - { - Id = 2, - Name = "Guest NGO", - Organizer = false, - ShortName = "GUE" - }); - context.SaveChanges(); - - } - - - private static bool AllMigrationsApplied(this DbContext context) - { - var applied = context.GetService() - .GetAppliedMigrations() - .Select(m => m.MigrationId); - - var total = context.GetService() - .Migrations - .Select(m => m.Key); - - return !total.Except(applied).Any(); - } - } -} diff --git a/private-api/app/src/VotingIrregularities.Domain/VotingIrregularities.Domain.csproj b/private-api/app/src/VotingIrregularities.Domain/VotingIrregularities.Domain.csproj deleted file mode 100644 index 7c2bf1af..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/VotingIrregularities.Domain.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - netcoreapp2.1 - VotingIrregularities.Domain - Exe - VotingIrregularities.Domain - 2.1.0 - 2.0 - false - false - false - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/private-api/app/src/VotingIrregularities.Domain/readme.md b/private-api/app/src/VotingIrregularities.Domain/readme.md deleted file mode 100644 index b15e37fd..00000000 --- a/private-api/app/src/VotingIrregularities.Domain/readme.md +++ /dev/null @@ -1,22 +0,0 @@ -### Creare baza de date - -Assembly-ul VotingIrregularities.Domain are configurat EF Migrations si poate genera o baza de date impreuna cu date de test. - -Pentru acest lucru trebuie urmati pasii de mai jos: - -1. Completeaza in appsetings.json SAU adauga intr-un fisier nou appsettings.target.json connectionstring-ul catre instanta de SQL in care vrei sa creezi baza de date - -2. ruleaza din consola, in directorul VotingIrregularities.Domain: - -```sh -private-api\app\VotingIrregularities.Domain> dotnet run -``` - - -If you also want to seed some sample data please use the `-seed` argument: - -```sh -private-api\app\VotingIrregularities.Domain> dotnet run -``` - -Important este faptul ca actiunea de migrare va *sterge* datele din tabelele RaspunsDisponibil, Intrebare, Sectiune, Optiune. \ No newline at end of file diff --git a/private-api/app/test/VotingIrregularities.Domain.Tests/AssemblyFixture.cs b/private-api/app/test/VotingIrregularities.Domain.Tests/AssemblyFixture.cs deleted file mode 100644 index 4168e9ab..00000000 --- a/private-api/app/test/VotingIrregularities.Domain.Tests/AssemblyFixture.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace VotingIrregularities.Domain.Tests -{ - public class AssemblyFixture : IDisposable - { - public static AssemblyFixture Current = new AssemblyFixture(); - - private AssemblyFixture() - { - } - - ~AssemblyFixture() - { - Dispose(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - } - } -} diff --git a/private-api/app/test/VotingIrregularities.Domain.Tests/Properties/AssemblyInfo.cs b/private-api/app/test/VotingIrregularities.Domain.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 17b8a3e5..00000000 --- a/private-api/app/test/VotingIrregularities.Domain.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("VotingIrregularities.Domain.Tests")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4e70b88e-f43a-4546-955c-4a2139c1cc9c")] diff --git a/private-api/app/test/VotingIrregularities.Domain.Tests/VotingIrregularities.Domain.Tests.csproj b/private-api/app/test/VotingIrregularities.Domain.Tests/VotingIrregularities.Domain.Tests.csproj deleted file mode 100644 index 3d21c607..00000000 --- a/private-api/app/test/VotingIrregularities.Domain.Tests/VotingIrregularities.Domain.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netcoreapp2.1 - VotingIrregularities.Domain.Tests - VotingIrregularities.Domain.Tests - true - 2.1.0 - false - false - false - - - - - - - - - - - diff --git a/private-api/app/test/VotingIrregularities.Domain.Tests/WarmUpTests.cs b/private-api/app/test/VotingIrregularities.Domain.Tests/WarmUpTests.cs deleted file mode 100644 index 7785d668..00000000 --- a/private-api/app/test/VotingIrregularities.Domain.Tests/WarmUpTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace VotingIrregularities.Domain.Tests -{ - public class WarmUpTests - { - [Fact] - public void PassingTests() - { - Assert.True(true); - } - } -} diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 00000000..2d15e48e --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,20 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.vs +**/.vscode +**/*.*proj.user +**/azds.yaml +**/charts +**/bin +**/obj +**/Dockerfile +**/Dockerfile.develop +**/docker-compose.yml +**/docker-compose.*.yml +**/*.dbmdl +**/*.jfm +**/secrets.dev.yaml +**/values.dev.yaml +**/.toolstarget \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 00000000..b239d40a --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,7 @@ + + + 1701;1702;1591;1573 + $(SolutionDir)\api-docs\$(MSBuildProjectName).xml + + \ No newline at end of file diff --git a/private-api/app/NuGet.config b/src/NuGet.config similarity index 100% rename from private-api/app/NuGet.config rename to src/NuGet.config diff --git a/src/VotingIrregularities.sln b/src/VotingIrregularities.sln new file mode 100644 index 00000000..14fa4c4d --- /dev/null +++ b/src/VotingIrregularities.sln @@ -0,0 +1,144 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.352 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{8A09B442-FB79-4293-BF9B-E34DD3AE70F3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{388C55EB-26FF-46AB-9395-549E1A7A99AD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Tests", "test\VotingIrregularities.Tests\VotingIrregularities.Tests.csproj", "{6961F352-9908-40B1-A840-A96841CF2031}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VotingIrregularities.Domain.Seed", "api\VotingIrregularities.Domain.Seed\VotingIrregularities.Domain.Seed.csproj", "{EBCE8818-41D9-4089-98F4-A03BAA909210}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Observer", "api\VoteMonitor.Api.Observer\VoteMonitor.Api.Observer.csproj", "{B8004522-7E3D-41B0-B33E-86B2F090D3AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Location", "api\VoteMonitor.Api.Location\VoteMonitor.Api.Location.csproj", "{09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Core", "api\VoteMonitor.Api.Core\VoteMonitor.Api.Core.csproj", "{7EE053E7-C2E8-43E7-B103-1D224443A632}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Note", "api\VoteMonitor.Api.Note\VoteMonitor.Api.Note.csproj", "{A2B9B61F-14DC-42AF-B872-93B341BDDCD3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Form", "api\VoteMonitor.Api.Form\VoteMonitor.Api.Form.csproj", "{476CC12D-74F8-4971-B5E2-FEA84DF52FE1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Entities", "api\VoteMonitor.Entities\VoteMonitor.Entities.csproj", "{6210AEE2-2164-4DD9-822F-E370336168BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Answer", "api\VoteMonitor.Api.Answer\VoteMonitor.Api.Answer.csproj", "{271F09F9-3068-4FB9-A17C-A6105BC7EFD2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Notification", "api\VoteMonitor.Api.Notification\VoteMonitor.Api.Notification.csproj", "{4C638FB0-96F1-4AC9-8765-8A6BB322C015}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Statistics", "api\VoteMonitor.Api.Statistics\VoteMonitor.Api.Statistics.csproj", "{8BA1E3BC-0F4A-4120-965E-D2709887D05E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.DataExport", "api\VoteMonitor.Api.DataExport\VoteMonitor.Api.DataExport.csproj", "{4EDC6262-9BA5-46E7-BB90-E31F499A9F5A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.County", "api\VoteMonitor.Api.County\VoteMonitor.Api.County.csproj", "{1D0B0723-8542-41F4-AF80-6003EAC3F20E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api", "api\VoteMonitor.Api\VoteMonitor.Api.csproj", "{87395D1E-7E9B-4099-8093-9AC16D7AE45A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoteMonitor.Api.Auth", "api\VoteMonitor.Api.Auth\VoteMonitor.Api.Auth.csproj", "{9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoteMonitor.Api.PollingStation", "api\VoteMonitor.Api.PollingStation\VoteMonitor.Api.PollingStation.csproj", "{DD341123-8FD7-4506-AA0D-4280A5112A6F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoteMonitor.Api.PollingStation.Tests", "test\VoteMonitor.Api.PollingStation.Tests\VoteMonitor.Api.PollingStation.Tests.csproj", "{C7767496-1BC5-41AD-98D1-439BAA2ACEAE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6961F352-9908-40B1-A840-A96841CF2031}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6961F352-9908-40B1-A840-A96841CF2031}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6961F352-9908-40B1-A840-A96841CF2031}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6961F352-9908-40B1-A840-A96841CF2031}.Release|Any CPU.Build.0 = Release|Any CPU + {EBCE8818-41D9-4089-98F4-A03BAA909210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBCE8818-41D9-4089-98F4-A03BAA909210}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBCE8818-41D9-4089-98F4-A03BAA909210}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBCE8818-41D9-4089-98F4-A03BAA909210}.Release|Any CPU.Build.0 = Release|Any CPU + {B8004522-7E3D-41B0-B33E-86B2F090D3AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8004522-7E3D-41B0-B33E-86B2F090D3AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8004522-7E3D-41B0-B33E-86B2F090D3AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8004522-7E3D-41B0-B33E-86B2F090D3AA}.Release|Any CPU.Build.0 = Release|Any CPU + {09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4}.Release|Any CPU.Build.0 = Release|Any CPU + {7EE053E7-C2E8-43E7-B103-1D224443A632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EE053E7-C2E8-43E7-B103-1D224443A632}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EE053E7-C2E8-43E7-B103-1D224443A632}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EE053E7-C2E8-43E7-B103-1D224443A632}.Release|Any CPU.Build.0 = Release|Any CPU + {A2B9B61F-14DC-42AF-B872-93B341BDDCD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2B9B61F-14DC-42AF-B872-93B341BDDCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2B9B61F-14DC-42AF-B872-93B341BDDCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2B9B61F-14DC-42AF-B872-93B341BDDCD3}.Release|Any CPU.Build.0 = Release|Any CPU + {476CC12D-74F8-4971-B5E2-FEA84DF52FE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {476CC12D-74F8-4971-B5E2-FEA84DF52FE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {476CC12D-74F8-4971-B5E2-FEA84DF52FE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {476CC12D-74F8-4971-B5E2-FEA84DF52FE1}.Release|Any CPU.Build.0 = Release|Any CPU + {6210AEE2-2164-4DD9-822F-E370336168BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6210AEE2-2164-4DD9-822F-E370336168BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6210AEE2-2164-4DD9-822F-E370336168BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6210AEE2-2164-4DD9-822F-E370336168BB}.Release|Any CPU.Build.0 = Release|Any CPU + {271F09F9-3068-4FB9-A17C-A6105BC7EFD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {271F09F9-3068-4FB9-A17C-A6105BC7EFD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {271F09F9-3068-4FB9-A17C-A6105BC7EFD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {271F09F9-3068-4FB9-A17C-A6105BC7EFD2}.Release|Any CPU.Build.0 = Release|Any CPU + {4C638FB0-96F1-4AC9-8765-8A6BB322C015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C638FB0-96F1-4AC9-8765-8A6BB322C015}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C638FB0-96F1-4AC9-8765-8A6BB322C015}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C638FB0-96F1-4AC9-8765-8A6BB322C015}.Release|Any CPU.Build.0 = Release|Any CPU + {8BA1E3BC-0F4A-4120-965E-D2709887D05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BA1E3BC-0F4A-4120-965E-D2709887D05E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BA1E3BC-0F4A-4120-965E-D2709887D05E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BA1E3BC-0F4A-4120-965E-D2709887D05E}.Release|Any CPU.Build.0 = Release|Any CPU + {4EDC6262-9BA5-46E7-BB90-E31F499A9F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EDC6262-9BA5-46E7-BB90-E31F499A9F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EDC6262-9BA5-46E7-BB90-E31F499A9F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EDC6262-9BA5-46E7-BB90-E31F499A9F5A}.Release|Any CPU.Build.0 = Release|Any CPU + {1D0B0723-8542-41F4-AF80-6003EAC3F20E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D0B0723-8542-41F4-AF80-6003EAC3F20E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D0B0723-8542-41F4-AF80-6003EAC3F20E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D0B0723-8542-41F4-AF80-6003EAC3F20E}.Release|Any CPU.Build.0 = Release|Any CPU + {87395D1E-7E9B-4099-8093-9AC16D7AE45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87395D1E-7E9B-4099-8093-9AC16D7AE45A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87395D1E-7E9B-4099-8093-9AC16D7AE45A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87395D1E-7E9B-4099-8093-9AC16D7AE45A}.Release|Any CPU.Build.0 = Release|Any CPU + {9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004}.Release|Any CPU.Build.0 = Release|Any CPU + {DD341123-8FD7-4506-AA0D-4280A5112A6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD341123-8FD7-4506-AA0D-4280A5112A6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD341123-8FD7-4506-AA0D-4280A5112A6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD341123-8FD7-4506-AA0D-4280A5112A6F}.Release|Any CPU.Build.0 = Release|Any CPU + {C7767496-1BC5-41AD-98D1-439BAA2ACEAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7767496-1BC5-41AD-98D1-439BAA2ACEAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7767496-1BC5-41AD-98D1-439BAA2ACEAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7767496-1BC5-41AD-98D1-439BAA2ACEAE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6961F352-9908-40B1-A840-A96841CF2031} = {388C55EB-26FF-46AB-9395-549E1A7A99AD} + {EBCE8818-41D9-4089-98F4-A03BAA909210} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {B8004522-7E3D-41B0-B33E-86B2F090D3AA} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {09E89763-37C4-4C21-8D2A-5ACAAC2BB0E4} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {7EE053E7-C2E8-43E7-B103-1D224443A632} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {A2B9B61F-14DC-42AF-B872-93B341BDDCD3} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {476CC12D-74F8-4971-B5E2-FEA84DF52FE1} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {6210AEE2-2164-4DD9-822F-E370336168BB} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {271F09F9-3068-4FB9-A17C-A6105BC7EFD2} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {4C638FB0-96F1-4AC9-8765-8A6BB322C015} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {8BA1E3BC-0F4A-4120-965E-D2709887D05E} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {4EDC6262-9BA5-46E7-BB90-E31F499A9F5A} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {1D0B0723-8542-41F4-AF80-6003EAC3F20E} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {87395D1E-7E9B-4099-8093-9AC16D7AE45A} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {9AE873EE-2FA4-4C0E-9DDB-BB0B3C7A4004} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {DD341123-8FD7-4506-AA0D-4280A5112A6F} = {8A09B442-FB79-4293-BF9B-E34DD3AE70F3} + {C7767496-1BC5-41AD-98D1-439BAA2ACEAE} = {388C55EB-26FF-46AB-9395-549E1A7A99AD} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF1523BC-7F31-4564-8E1B-D2DB4552FFCB} + EndGlobalSection +EndGlobal diff --git a/private-api/app/VotingIrregularities.sln.DotSettings b/src/VotingIrregularities.sln.DotSettings similarity index 78% rename from private-api/app/VotingIrregularities.sln.DotSettings rename to src/VotingIrregularities.sln.DotSettings index 1bd2a7ac..eb0d8a61 100644 --- a/private-api/app/VotingIrregularities.sln.DotSettings +++ b/src/VotingIrregularities.sln.DotSettings @@ -1,2 +1,3 @@  + DTO NG \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Commands/BulkAnswers.cs b/src/api/VoteMonitor.Api.Answer/Commands/BulkAnswers.cs new file mode 100644 index 00000000..3b1f0950 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Commands/BulkAnswers.cs @@ -0,0 +1,19 @@ +using MediatR; +using System.Collections.Generic; +using System.Linq; +using VoteMonitor.Api.Answer.Models; + +namespace VoteMonitor.Api.Answer.Commands +{ + public class BulkAnswers : IRequest + { + public BulkAnswers(IEnumerable raspunsuri) + { + Answers = raspunsuri.ToList(); + } + + public int ObserverId { get; set; } + + public List Answers { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Commands/CompleteazaRaspunsCommand.cs b/src/api/VoteMonitor.Api.Answer/Commands/CompleteazaRaspunsCommand.cs new file mode 100644 index 00000000..afc72032 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Commands/CompleteazaRaspunsCommand.cs @@ -0,0 +1,17 @@ +using MediatR; +using System.Collections.Generic; +using VoteMonitor.Api.Answer.Models; + +namespace VoteMonitor.Api.Answer.Commands +{ + public class CompleteazaRaspunsCommand : IRequest + { + public CompleteazaRaspunsCommand() + { + Answers = new List(); + } + public int ObserverId { get; set; } + public List Answers { get; set; } + + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Controllers/AnswersController.cs b/src/api/VoteMonitor.Api.Answer/Controllers/AnswersController.cs new file mode 100644 index 00000000..b647ab9f --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Controllers/AnswersController.cs @@ -0,0 +1,113 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.Threading.Tasks; +using VoteMonitor.Api.Answer.Commands; +using VoteMonitor.Api.Answer.Models; +using VoteMonitor.Api.Answer.Queries; +using VoteMonitor.Api.Core; + +namespace VoteMonitor.Api.Answer.Controllers +{ + [Route("api/v1/answers")] + public class AnswersController : Controller + { + private readonly IMediator _mediator; + private readonly IConfiguration _configuration; + + public AnswersController(IMediator mediator, IConfiguration configuration) + { + _mediator = mediator; + _configuration = configuration; + } + + /// + /// Returns a list of polling stations where observers from the given NGO have submitted answers + /// to the questions marked as Flagged=Urgent, ordered by ModifiedDate descending + /// + /// Pagination details(default Page=1, PageSize=20) + /// Urgent (Flagged) + /// + [HttpGet] + public async Task> Get(SectionAnswersRequest model) + { + var organizator = this.GetOrganizatorOrDefault(_configuration.GetValue("DefaultOrganizator")); + var idOng = this.GetIdOngOrDefault(_configuration.GetValue("DefaultIdOng")); + + return await _mediator.Send(new AnswersQuery + { + IdONG = idOng, + Organizer = organizator, + Page = model.Page, + PageSize = model.PageSize, + Urgent = model.Urgent, + County = model.County, + PollingStationNumber = model.PollingStationNumber, + ObserverId = model.ObserverId + }); + } + + /// + /// Returns answers given by the specified observer at the specified polling station + /// + [HttpGet("filledIn")] + public async Task>> Get(int idPollingStation, int idObserver) + { + return await _mediator.Send(new FilledInAnswersQuery + { + ObserverId = idObserver, + PollingStationId = idPollingStation + }); + } + + /// + /// Returns the polling station information filled in by the given observer at the given polling station + /// + /// "IdSectieDeVotare" - Id-ul sectiei unde s-au completat raspunsurile + /// "IdObservator" - Id-ul observatorului care a dat raspunsurile + /// + [HttpGet("pollingStationInfo")] + public async Task GetRaspunsuriFormular(ObserverAnswersRequest model) + { + return await _mediator.Send(new FormAnswersQuery + { + ObserverId = model.ObserverId, + PollingStationId = model.PollingStationNumber + }); + } + + + /// + /// Saves the answers to one or more questions, at a given polling station + /// An answer can have multiple options (OptionId) and potentially a free text (Value). + /// + /// Polling station, list of options and the associated text of an option when + /// IsFreeText = true + /// + [HttpPost] + [Authorize("Observer")] + public async Task PostAnswer([FromBody] AnswerModelWrapper answerModel) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + // TODO[DH] use a pipeline instead of separate Send commands + var command = await _mediator.Send(new BulkAnswers(answerModel.Answers)); + + command.ObserverId = this.GetIdObserver(); + + var result = await _mediator.Send(command); + + if (result < 0) + { + return NotFound(); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Handlers/AnswerQueryHandler.cs b/src/api/VoteMonitor.Api.Answer/Handlers/AnswerQueryHandler.cs new file mode 100644 index 00000000..db28cbb0 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Handlers/AnswerQueryHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Answer.Commands; +using VoteMonitor.Api.Answer.Models; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Answer.Handlers +{ + public class AnswerQueryHandler : + IRequestHandler + { + private readonly VoteMonitorContext _context; + + public AnswerQueryHandler(VoteMonitorContext context) + { + _context = context; + } + + public async Task Handle(BulkAnswers message, CancellationToken cancellationToken) + { + // se identifica sectiile in care observatorul a raspuns + var sectii = message.Answers + .Select(a => new { a.PollingStationNumber, a.CountyCode }) + .Distinct() + .ToList(); + + var command = new CompleteazaRaspunsCommand { ObserverId = message.ObserverId }; + + + foreach (var sectie in sectii) + { + var idSectie = (await _context + .PollingStations + .FirstOrDefaultAsync(p => p.County.Code == sectie.CountyCode && p.Number == sectie.PollingStationNumber)) + .Id; + //(sectie.PollingStationNumber, sectie.CountyCode); + + command.Answers.AddRange(message.Answers + .Where(a => a.PollingStationNumber == sectie.PollingStationNumber && a.CountyCode == sectie.CountyCode) + .Select(a => new AnswerDTO + { + QuestionId = a.QuestionId, + PollingSectionId = idSectie, + Options = a.Options, + PollingStationNumber = a.PollingStationNumber, + CountyCode = a.CountyCode + })); + } + + return command; + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Handlers/AnswersQueryHandler.cs b/src/api/VoteMonitor.Api.Answer/Handlers/AnswersQueryHandler.cs new file mode 100644 index 00000000..6eb77d37 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Handlers/AnswersQueryHandler.cs @@ -0,0 +1,111 @@ +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Answer.Models; +using VoteMonitor.Api.Answer.Queries; +using VoteMonitor.Api.Core; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Answer.Handlers +{ + public class AnswersQueryHandler : + IRequestHandler>, + IRequestHandler>>, + IRequestHandler + { + private readonly VoteMonitorContext _context; + private readonly IMapper _mapper; + + public AnswersQueryHandler(VoteMonitorContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(AnswersQuery message, CancellationToken cancellationToken) + { + var query = _context.Answers.Where(a => a.OptionAnswered.Flagged == message.Urgent); + + // Filter by the organizer flag if specified + if (!message.Organizer) + { + query = query.Where(a => a.Observer.IdNgo == message.IdONG); + } + + // Filter by county if specified + if (!string.IsNullOrEmpty(message.County)) + { + query = query.Where(a => a.CountyCode == message.County); + } + + // Filter by polling station if specified + if (message.PollingStationNumber > 0) + { + query = query.Where(a => a.PollingStationNumber == message.PollingStationNumber); + } + + // Filter by polling station if specified + if (message.ObserverId > 0) + { + query = query.Where(a => a.IdObserver == message.ObserverId); + } + + var answerQueryInfosQuery = query.GroupBy(a => new { a.IdPollingStation, a.CountyCode, a.PollingStationNumber, a.IdObserver, ObserverName = a.Observer.Name }) + .Select(x => new VoteMonitorContext.AnswerQueryInfo + { + IdObserver = x.Key.IdObserver, + IdPollingStation = x.Key.IdPollingStation, + PollingStation = x.Key.CountyCode + " " + x.Key.PollingStationNumber.ToString(), + ObserverName = x.Key.ObserverName, + LastModified = x.Max(a => a.LastModified) + }); + + var count = await answerQueryInfosQuery.CountAsync(cancellationToken: cancellationToken); + + var sectiiCuObservatoriPaginat = await answerQueryInfosQuery + .OrderByDescending(aqi => aqi.LastModified) + .Skip((message.Page - 1) * message.PageSize) + .Take(message.PageSize) + .ToListAsync(cancellationToken: cancellationToken); + + return new ApiListResponse + { + Data = sectiiCuObservatoriPaginat.Select(x => _mapper.Map(x)).ToList(), + Page = message.Page, + PageSize = message.PageSize, + TotalItems = count + }; + } + + + public async Task>> Handle(FilledInAnswersQuery message, CancellationToken cancellationToken) + { + var raspunsuri = await _context.Answers + .Include(r => r.OptionAnswered) + .ThenInclude(rd => rd.Question) + .Include(r => r.OptionAnswered) + .ThenInclude(rd => rd.Option) + .Where(r => r.IdObserver == message.ObserverId && r.IdPollingStation == message.PollingStationId) + .ToListAsync(cancellationToken: cancellationToken); + + var intrebari = raspunsuri + .Select(r => r.OptionAnswered.Question) + .ToList(); + + return intrebari.Select(i => _mapper.Map>(i)).ToList(); + } + + public async Task Handle(FormAnswersQuery message, CancellationToken cancellationToken) + { + var raspunsuriFormular = await _context.PollingStationInfos + .FirstOrDefaultAsync(rd => rd.IdObserver == message.ObserverId + && rd.IdPollingStation == message.PollingStationId, cancellationToken: cancellationToken); + + return _mapper.Map(raspunsuriFormular); + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Handlers/FillInAnswerQueryHandler.cs b/src/api/VoteMonitor.Api.Answer/Handlers/FillInAnswerQueryHandler.cs new file mode 100644 index 00000000..22d0eb39 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Handlers/FillInAnswerQueryHandler.cs @@ -0,0 +1,112 @@ +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Answer.Commands; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Answer.Handlers +{ + public class FillInAnswerQueryHandler : IRequestHandler + { + private readonly VoteMonitorContext _context; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public FillInAnswerQueryHandler(VoteMonitorContext context, IMapper mapper, ILogger logger) + { + _context = context; + _mapper = mapper; + _logger = logger; + } + + public async Task Handle(CompleteazaRaspunsCommand message, CancellationToken cancellationToken) + { + try + { + //flat answers + var lastModified = DateTime.UtcNow; + + var raspunsuriNoi = GetFlatListOfAnswers(message, lastModified); + + // stergerea este pe fiecare sectie + var sectii = message.Answers.Select(a => a.PollingSectionId).Distinct().ToList(); + + using (var tran = await _context.Database.BeginTransactionAsync()) + { + foreach (var sectie in sectii) + { + var intrebari = message.Answers.Select(a => a.QuestionId).Distinct().ToList(); + + // delete existing answers for posted questions on this 'sectie' + var todelete = _context.Answers + .Include(a => a.OptionAnswered) + .Where( + a => + a.IdObserver == message.ObserverId && + a.IdPollingStation == sectie) + //.Where(a => intrebari.Contains(a.OptionAnswered.IdQuestion)) + .WhereRaspunsContains(intrebari) + ; + //.Delete(); + _context.Answers.RemoveRange(todelete); + + await _context.SaveChangesAsync(); + } + + _context.Answers.AddRange(raspunsuriNoi); + + var result = await _context.SaveChangesAsync(); + + tran.Commit(); + + return result; + } + } + catch (Exception ex) + { + _logger.LogError(typeof(CompleteazaRaspunsCommand).GetHashCode(), ex, ex.Message); + } + + return await Task.FromResult(-1); + } + + public static List GetFlatListOfAnswers(CompleteazaRaspunsCommand command, DateTime lastModified) + { + var list = command.Answers.Select(a => new + { + flat = a.Options.Select(o => new Entities.Answer + { + IdObserver = command.ObserverId, + IdPollingStation = a.PollingSectionId, + IdOptionToQuestion = o.OptionId, + Value = o.Value, + CountyCode = a.CountyCode, + PollingStationNumber = a.PollingStationNumber, + LastModified = lastModified + }) + }) + .SelectMany(a => a.flat) + .GroupBy(k => k.IdOptionToQuestion, + (g, o) => new Entities.Answer + { + IdObserver = command.ObserverId, + IdPollingStation = o.Last().IdPollingStation, + IdOptionToQuestion = g, + Value = o.Last().Value, + CountyCode = o.Last().CountyCode, + PollingStationNumber = o.Last().PollingStationNumber, + LastModified = lastModified + }) + .Distinct() + .ToList(); + + return list; + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/AnswerDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/AnswerDTO.cs new file mode 100644 index 00000000..60222e34 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/AnswerDTO.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace VoteMonitor.Api.Answer.Models +{ + public class AnswerDTO + { + public int QuestionId { get; set; } + public int PollingSectionId { get; set; } + public string CountyCode { get; set; } + public int PollingStationNumber { get; set; } + public List Options { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/AnswerQueryDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/AnswerQueryDTO.cs new file mode 100644 index 00000000..b59fbf3d --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/AnswerQueryDTO.cs @@ -0,0 +1,10 @@ +namespace VoteMonitor.Api.Answer.Models +{ + public class AnswerQueryDTO + { + public int IdObserver { get; set; } + public int IdPollingStation { get; set; } + public string ObserverName { get; set; } + public string PollingStationName { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/AvailableAnswerDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/AvailableAnswerDTO.cs new file mode 100644 index 00000000..daeb7169 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/AvailableAnswerDTO.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Answer.Models +{ + public class AvailableAnswerDTO + { + public int IdOption { get; set; } + public string Text { get; set; } + public bool IsFreeText { get; set; } + } + + public class FormularProfile : Profile + { + public FormularProfile() + { + CreateMap>() + .ForMember(dest => dest.Answers, c => c.MapFrom(src => src.OptionsToQuestions)) + .ForMember(dest => dest.Id, c => c.MapFrom(src => src.Id)) + .ForMember(dest => dest.Text, c => c.MapFrom(src => src.Text)) + .ForMember(dest => dest.IdQuestionType, c => c.MapFrom(src => src.QuestionType)) + .ForMember(dest => dest.IdQuestion, c => c.MapFrom(src => src.Code)) + .ForMember(dest => dest.FormCode, c => c.MapFrom(src => src.Code)) + ; + + CreateMap() + .ForMember(dest => dest.Text, c => c.MapFrom(src => src.Option.Text)) + .ForMember(dest => dest.IsFreeText, c => c.MapFrom(src => src.Option.IsFreeText)) + .ForMember(dest => dest.IdOption, c => c.MapFrom(src => src.Id)); + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/BulkAnswerModel.cs b/src/api/VoteMonitor.Api.Answer/Models/BulkAnswerModel.cs new file mode 100644 index 00000000..b4909b4a --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/BulkAnswerModel.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace VoteMonitor.Api.Answer.Models +{ + public class AnswerModelWrapper + { + public BulkAnswerModel[] Answers { get; set; } + } + public class SelectedOptionModel + { + public int OptionId { get; set; } + public string Value { get; set; } + } + public class BulkAnswerModel + { + [Required] + public int QuestionId { get; set; } + + [Required(AllowEmptyStrings = false)] + public string CountyCode { get; set; } + + [Required(AllowEmptyStrings = false)] + public int PollingStationNumber { get; set; } + + public List Options { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/FilledInAnswerDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/FilledInAnswerDTO.cs new file mode 100644 index 00000000..9c1be534 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/FilledInAnswerDTO.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using System.Linq; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Answer.Models +{ + public class FilledInAnswerDTO + { + public int IdOption { get; set; } + public string Text { get; set; } + public bool IsFreeText { get; set; } + public string Value { get; set; } + public bool Flagged { get; set; } + } + + public class AnswerProfile : Profile + { + public AnswerProfile() + { + CreateMap>() + .ForMember(dest => dest.Answers, c => c.MapFrom(src => src.OptionsToQuestions)) + .ForMember(dest => dest.Id, c => c.MapFrom(src => src.Id)) + .ForMember(dest => dest.Text, c => c.MapFrom(src => src.Text)) + .ForMember(dest => dest.IdQuestionType, c => c.MapFrom(src => src.QuestionType)) + .ForMember(dest => dest.IdQuestion, c => c.MapFrom(src => src.Code)) + .ForMember(dest => dest.FormCode, c => c.MapFrom(src => src.Code)) + ; + + CreateMap() + .ForMember(dest => dest.Text, c => c.MapFrom(src => src.Option.Text)) + .ForMember(dest => dest.IsFreeText, c => c.MapFrom(src => src.Option.IsFreeText)) + .ForMember(dest => dest.IdOption, c => c.MapFrom(src => src.Id)) + .ForMember(dest => dest.Flagged, c => c.MapFrom(src => src.Flagged)) + .ForMember(dest => dest.Value, c => c.MapFrom(src => src.Answers.First().Value)); + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/PollingStationInfosDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/PollingStationInfosDTO.cs new file mode 100644 index 00000000..e8c172d8 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/PollingStationInfosDTO.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using System; +using VoteMonitor.Entities; +using static VoteMonitor.Entities.VoteMonitorContext; + +namespace VoteMonitor.Api.Answer.Models +{ + public class PollingStationInfosDTO + { + public DateTime LastModified { get; set; } + public bool? UrbanArea { get; set; } + public DateTime? ObserverLeaveTime { get; set; } + public DateTime? ObserverArrivalTime { get; set; } + public bool? IsPollingStationPresidentFemale { get; set; } + } + + public class RaspunsFomularProfile : Profile + { + public RaspunsFomularProfile() + { + CreateMap() + .ForMember(dest => dest.LastModified, o => o.MapFrom(src => src.LastModified)) + .ForMember(dest => dest.UrbanArea, o => o.MapFrom(src => src.UrbanArea)) + .ForMember(dest => dest.ObserverLeaveTime, o => o.MapFrom(src => src.ObserverLeaveTime)) + .ForMember(dest => dest.ObserverArrivalTime, o => o.MapFrom(src => src.ObserverArrivalTime)) + .ForMember(dest => dest.IsPollingStationPresidentFemale, o => o.MapFrom(src => src.IsPollingStationPresidentFemale)) + ; + CreateMap() + .ForMember(dest => dest.IdObserver, o => o.MapFrom(src => src.IdObserver)) + .ForMember(dest => dest.IdPollingStation, o => o.MapFrom(src => src.IdPollingStation)) + .ForMember(dest => dest.ObserverName, o => o.MapFrom(src => src.ObserverName)) + .ForMember(dest => dest.PollingStationName, o => o.MapFrom(src => src.PollingStation)) + ; + } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Models/QuestionDTO.cs b/src/api/VoteMonitor.Api.Answer/Models/QuestionDTO.cs new file mode 100644 index 00000000..501030ce --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/QuestionDTO.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace VoteMonitor.Api.Answer.Models +{ + public class QuestionDTO + where T : class + { + public int Id { get; set; } + public string Text { get; set; } + public int IdQuestionType { get; set; } + public string IdQuestion { get; set; } + public string FormCode { get; set; } + + public IList Answers { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Models/SectionAnswersRequest.cs b/src/api/VoteMonitor.Api.Answer/Models/SectionAnswersRequest.cs new file mode 100644 index 00000000..bedca958 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Models/SectionAnswersRequest.cs @@ -0,0 +1,18 @@ +using VoteMonitor.Api.Core; + +namespace VoteMonitor.Api.Answer.Models +{ + public class SectionAnswersRequest : PagingModel + { + public bool Urgent { get; set; } + public string County { get; set; } + public int PollingStationNumber { get; set; } + public int ObserverId { get; set; } + } + + public class ObserverAnswersRequest + { + public int PollingStationNumber { get; set; } + public int ObserverId { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Answer/Queries/AnswersQuery.cs b/src/api/VoteMonitor.Api.Answer/Queries/AnswersQuery.cs new file mode 100644 index 00000000..72aafcfd --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Queries/AnswersQuery.cs @@ -0,0 +1,16 @@ +using MediatR; +using VoteMonitor.Api.Answer.Models; +using VoteMonitor.Api.Core; + +namespace VoteMonitor.Api.Answer.Queries +{ + public class AnswersQuery : PagingModel, IRequest> + { + public int IdONG { get; set; } + public bool Urgent { get; set; } + public bool Organizer { get; set; } + public string County { get; set; } + public int PollingStationNumber { get; set; } + public int ObserverId { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Queries/FilledInAnswersQuery.cs b/src/api/VoteMonitor.Api.Answer/Queries/FilledInAnswersQuery.cs new file mode 100644 index 00000000..ab272a66 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Queries/FilledInAnswersQuery.cs @@ -0,0 +1,12 @@ +using MediatR; +using System.Collections.Generic; +using VoteMonitor.Api.Answer.Models; + +namespace VoteMonitor.Api.Answer.Queries +{ + public class FilledInAnswersQuery : IRequest>> + { + public int PollingStationId { get; set; } + public int ObserverId { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/Queries/FormAnswersQuery.cs b/src/api/VoteMonitor.Api.Answer/Queries/FormAnswersQuery.cs new file mode 100644 index 00000000..27e22de1 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/Queries/FormAnswersQuery.cs @@ -0,0 +1,11 @@ +using MediatR; +using VoteMonitor.Api.Answer.Models; + +namespace VoteMonitor.Api.Answer.Queries +{ + public class FormAnswersQuery : IRequest + { + public int PollingStationId { get; set; } + public int ObserverId { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Answer/VoteMonitor.Api.Answer.csproj b/src/api/VoteMonitor.Api.Answer/VoteMonitor.Api.Answer.csproj new file mode 100644 index 00000000..6e222a84 --- /dev/null +++ b/src/api/VoteMonitor.Api.Answer/VoteMonitor.Api.Answer.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + diff --git a/private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequest.cs b/src/api/VoteMonitor.Api.Auth/Commands/RegisterDeviceIdRequest.cs similarity index 77% rename from private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequest.cs rename to src/api/VoteMonitor.Api.Auth/Commands/RegisterDeviceIdRequest.cs index 9f701f50..9f10078a 100644 --- a/private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequest.cs +++ b/src/api/VoteMonitor.Api.Auth/Commands/RegisterDeviceIdRequest.cs @@ -1,6 +1,6 @@ using MediatR; -namespace VotingIrregularities.Domain.UserAggregate +namespace VoteMonitor.Api.Auth.Commands { public class RegisterDeviceId : IRequest { diff --git a/src/api/VoteMonitor.Api.Auth/Controllers/Authorization.cs b/src/api/VoteMonitor.Api.Auth/Controllers/Authorization.cs new file mode 100644 index 00000000..83de3fa5 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Controllers/Authorization.cs @@ -0,0 +1,222 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using VoteMonitor.Api.Auth.Commands; +using VoteMonitor.Api.Auth.Models; +using VoteMonitor.Api.Auth.Queries; +using VoteMonitor.Api.Core; +using VoteMonitor.Api.Core.Models; +using VoteMonitor.Api.Core.Options; + +namespace VoteMonitor.Api.Auth.Controllers +{ + /// + [Route("api/v1/access")] + public class Authorization : Controller + { + private readonly JwtIssuerOptions _jwtOptions; + private readonly ILogger _logger; + private readonly IMediator _mediator; + private readonly MobileSecurityOptions _mobileSecurityOptions; + + /// + public Authorization(IOptions jwtOptions, ILogger logger, IMediator mediator, IOptions mobileSecurityOptions) + { + _jwtOptions = jwtOptions.Value; + ThrowIfInvalidOptions(_jwtOptions); + + _logger = logger; + _mediator = mediator; + _mobileSecurityOptions = mobileSecurityOptions.Value; + } + + [HttpPost("authorize")] + [AllowAnonymous] + [ProducesResponseType(typeof(AuthenticationResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task AuthenticateUser([FromBody] AuthenticateUserRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + string token; + if (string.IsNullOrEmpty(request.UniqueId)) + { + var identity = await GetClaimsIdentity(request); + if (identity == null) + { + _logger.LogInformation($"Invalid username ({request.User}) or password ({request.Password})"); + return BadRequest("Invalid credentials"); + } + + token = GetTokenFromIdentity(identity); + } + else + { + var identity = await GetClaimsIdentity(request); + + if (identity == null) + { + _logger.LogInformation($"Invalid Phone ({request.User}) or password ({request.Password})"); + return BadRequest(_mobileSecurityOptions.InvalidCredentialsErrorMessage); + } + + token = GetTokenFromIdentity(identity); + } + + // Serialize and return the response + var response = new AuthenticationResponseModel + { + access_token = token, + expires_in = (int)_jwtOptions.ValidFor.TotalSeconds + }; + + return Ok(response); + } + + /// + /// Test action to get claims + /// + /// + [Authorize] + [HttpPost("test")] + public async Task Test() + { + var claims = User.Claims.Select(c => new + { + c.Type, + c.Value + }); + + return await Task.FromResult(claims); + } + private static void ThrowIfInvalidOptions(JwtIssuerOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.ValidFor <= TimeSpan.Zero) + { + throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); + } + + if (options.SigningCredentials == null) + { + throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); + } + + if (options.JtiGenerator == null) + { + throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); + } + } + /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - + new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) + .TotalSeconds); + + private async Task GetClaimsIdentity(AuthenticateUserRequest request) + { + if (string.IsNullOrEmpty(request.UniqueId)) + { + var userInfo = await _mediator.Send(new NgoAdminApplicationUser + { + Password = request.Password, + UserName = request.User, + UserType = UserType.NgoAdmin + }); + + if (userInfo == null) + { + return null; + } + + // Get the generic claims + the user specific one (the organizer flag) + return new ClaimsIdentity(await GetGenericIdentity(request.User, userInfo.IdNgo.ToString(), UserType.NgoAdmin.ToString()), + new[] + { + new Claim(ClaimsHelper.Organizer, userInfo.Organizer.ToString(), ClaimValueTypes.Boolean) + }); + } + else + { + // verific daca userul exista si daca nu are asociat un alt device, il returneaza din baza + var userInfo = await _mediator.Send(new ObserverApplicationUser + { + Phone = request.User, + Pin = request.Password, + UDID = request.UniqueId + }); + + if (!userInfo.IsAuthenticated) + { + return await Task.FromResult(null); + } + + if (userInfo.FirstAuthentication && _mobileSecurityOptions.LockDevice) + { + await + _mediator.Send(new RegisterDeviceId + { + MobileDeviceId = request.UniqueId, + ObserverId = userInfo.ObserverId + }); + } + + // Get the generic claims + the user specific one (the organizer flag) + return new ClaimsIdentity(await GetGenericIdentity(request.User, userInfo.IdNgo.ToString(), UserType.Observer.ToString()), + new[] + { + new Claim(ClaimsHelper.ObserverIdProperty, userInfo.ObserverId.ToString(), ClaimValueTypes.Boolean) + }); + } + + } + private string GetTokenFromIdentity(ClaimsIdentity identity) + { + // Create the JWT security token and encode it. + var jwt = new JwtSecurityToken( + issuer: _jwtOptions.Issuer, + audience: _jwtOptions.Audience, + claims: identity.Claims, + notBefore: _jwtOptions.NotBefore, + expires: _jwtOptions.Expiration, + signingCredentials: _jwtOptions.SigningCredentials); + + var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); + + return encodedJwt; + } + private async Task GetGenericIdentity(string name, string idNgo, string usertype) + { + return new ClaimsIdentity( + new GenericIdentity(name, ClaimsHelper.GenericIdProvider), + new[] + { + new Claim(JwtRegisteredClaimNames.Sub, name), + new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), + new Claim(JwtRegisteredClaimNames.Iat, + ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), + ClaimValueTypes.Integer64), + // Custom + new Claim(ClaimsHelper.IdNgo, idNgo), + new Claim(ClaimsHelper.UserType, usertype) + }); + } + } +} diff --git a/src/api/VoteMonitor.Api.Auth/Handlers/AdminQueryHandler.cs b/src/api/VoteMonitor.Api.Auth/Handlers/AdminQueryHandler.cs new file mode 100644 index 00000000..3caf3ad0 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Handlers/AdminQueryHandler.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Auth.Models; +using VoteMonitor.Api.Auth.Queries; +using VoteMonitor.Api.Core.Services; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Auth.Handlers +{ + public class AdminQueryHandler : IRequestHandler + { + private readonly VoteMonitorContext _context; + private readonly IHashService _hash; + private readonly IMapper _mapper; + + public AdminQueryHandler(VoteMonitorContext context, IHashService hash, IMapper mapper) + { + _context = context; + _hash = hash; + _mapper = mapper; + } + + public async Task Handle(NgoAdminApplicationUser message, CancellationToken token) + { + var hashValue = _hash.GetHash(message.Password); + + var userinfo = _context.NgoAdmins + .Include(a => a.Ngo) + .Where(a => a.Password == hashValue && + a.Account == message.UserName) + .Select(_mapper.Map) + .FirstOrDefault(); + + return await Task.FromResult(userinfo); + } + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Queries/ObserverAuthenticationQueryHandler.cs b/src/api/VoteMonitor.Api.Auth/Handlers/ObserverAuthenticationQueryHandler.cs similarity index 68% rename from private-api/app/src/VotingIrregularities.Api/Queries/ObserverAuthenticationQueryHandler.cs rename to src/api/VoteMonitor.Api.Auth/Handlers/ObserverAuthenticationQueryHandler.cs index 607b6e36..0cf8f634 100644 --- a/private-api/app/src/VotingIrregularities.Api/Queries/ObserverAuthenticationQueryHandler.cs +++ b/src/api/VoteMonitor.Api.Auth/Handlers/ObserverAuthenticationQueryHandler.cs @@ -2,20 +2,22 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using VotingIrregularities.Api.Models.AccountViewModels; -using VotingIrregularities.Api.Options; -using VotingIrregularities.Api.Services; -using VotingIrregularities.Domain.Models; +using VoteMonitor.Api.Auth.Models; +using VoteMonitor.Api.Auth.Queries; +using VoteMonitor.Api.Core.Options; +using VoteMonitor.Api.Core.Services; +using VoteMonitor.Entities; -namespace VotingIrregularities.Api.Queries +namespace VoteMonitor.Api.Auth.Handlers { /// /// Handles the query regarding the authentication of the observer - checks the phone number and hashed pin against the database /// - public class ObserverAuthenticationQueryHandler : AsyncRequestHandler + public class ObserverAuthenticationQueryHandler : IRequestHandler { - private readonly VotingContext _context; + private readonly VoteMonitorContext _context; private readonly IHashService _hash; private readonly MobileSecurityOptions _mobileSecurityOptions; @@ -25,35 +27,40 @@ public class ObserverAuthenticationQueryHandler : AsyncRequestHandlerThe EntityFramework context /// Implementation of the IHashService to be used to generate the hashes. It can either be `HashService` or `ClearTextService`. /// Options for specifying the DeviceLock feature toggle. If MobileSecurity:LockDevice is enabled (set to `true` in settings), the first login of the observer will store the UniqueDeviceId and only that device will be allowed to login. - public ObserverAuthenticationQueryHandler(VotingContext context, IHashService hash, IOptions mobileSecurityOptions) + public ObserverAuthenticationQueryHandler(VoteMonitorContext context, IHashService hash, IOptions mobileSecurityOptions) { _context = context; _hash = hash; _mobileSecurityOptions = mobileSecurityOptions.Value; } - protected override async Task HandleCore(ApplicationUser message) + public async Task Handle(ObserverApplicationUser message, CancellationToken cancellationToken) { var hashValue = _hash.GetHash(message.Pin); // Check for username and hash - var userQuery = _context.Observers.Where(o => o.Pin == hashValue && o.Phone == message.Phone); + var userQuery = _context.Observers.Where(o => o.Pin == hashValue && o.Phone.Trim() == message.Phone.Trim()); // Only if device lock is enabled verify the DeviceId if (_mobileSecurityOptions.LockDevice) + { userQuery = userQuery.Where(o => string.IsNullOrWhiteSpace(o.MobileDeviceId) || o.MobileDeviceId == message.UDID); + } - var userinfo = await userQuery.FirstOrDefaultAsync(); + var userinfo = await userQuery.FirstOrDefaultAsync(cancellationToken: cancellationToken); if (userinfo == null) + { return new RegisteredObserverModel { IsAuthenticated = false }; + } return new RegisteredObserverModel { ObserverId = userinfo.Id, + IdNgo = userinfo.IdNgo, IsAuthenticated = true, FirstAuthentication = string.IsNullOrWhiteSpace(userinfo.MobileDeviceId) && _mobileSecurityOptions.LockDevice }; diff --git a/private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequestHandler.cs b/src/api/VoteMonitor.Api.Auth/Handlers/RegisterDeviceIdRequestHandler.cs similarity index 51% rename from private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequestHandler.cs rename to src/api/VoteMonitor.Api.Auth/Handlers/RegisterDeviceIdRequestHandler.cs index 28feddce..c102d17a 100644 --- a/private-api/app/src/VotingIrregularities.Domain/UserAggregate/RegisterDeviceIdRequestHandler.cs +++ b/src/api/VoteMonitor.Api.Auth/Handlers/RegisterDeviceIdRequestHandler.cs @@ -2,31 +2,33 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Threading; using System.Threading.Tasks; -using VotingIrregularities.Domain.Models; +using VoteMonitor.Api.Auth.Commands; +using VoteMonitor.Entities; -namespace VotingIrregularities.Domain.UserAggregate +namespace VoteMonitor.Api.Auth.Handlers { - public class RegisterDeviceIdRequestHandler : AsyncRequestHandler + public class RegisterDeviceIdRequestHandler : IRequestHandler { - private readonly VotingContext _context; + private readonly VoteMonitorContext _context; private readonly ILogger _logger; - public RegisterDeviceIdRequestHandler(VotingContext context, ILogger logger) + public RegisterDeviceIdRequestHandler(VoteMonitorContext context, ILogger logger) { _context = context; _logger = logger; } - protected override async Task HandleCore(RegisterDeviceId request) + public async Task Handle(RegisterDeviceId request, CancellationToken cancellationToken) { try { - var observator = await _context.Observers.SingleAsync(a => a.Id == request.ObserverId); + var observator = await _context.Observers.SingleAsync(a => a.Id == request.ObserverId, cancellationToken: cancellationToken); observator.MobileDeviceId = request.MobileDeviceId; observator.DeviceRegisterDate = DateTime.UtcNow; - return await _context.SaveChangesAsync(); + return await _context.SaveChangesAsync(cancellationToken); } catch (Exception ex) { diff --git a/src/api/VoteMonitor.Api.Auth/Models/AuthenticateUserRequest.cs b/src/api/VoteMonitor.Api.Auth/Models/AuthenticateUserRequest.cs new file mode 100644 index 00000000..37856f5a --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Models/AuthenticateUserRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace VoteMonitor.Api.Auth.Models +{ + public class AuthenticateUserRequest + { + [Required] + public string User { get; set; } + [Required] + public string Password { get; set; } + public string UniqueId { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Auth/Models/AuthenticationResponseModel.cs b/src/api/VoteMonitor.Api.Auth/Models/AuthenticationResponseModel.cs new file mode 100644 index 00000000..4a0b3819 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Models/AuthenticationResponseModel.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Auth.Models +{ + public class AuthenticationResponseModel + { + public string access_token { get; set; } + public int expires_in { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Auth/Models/RegisteredObserverModel.cs b/src/api/VoteMonitor.Api.Auth/Models/RegisteredObserverModel.cs new file mode 100644 index 00000000..f0aa3fff --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Models/RegisteredObserverModel.cs @@ -0,0 +1,12 @@ +namespace VoteMonitor.Api.Auth.Models +{ + public class RegisteredObserverModel + { + public bool IsAuthenticated { get; set; } + + public int ObserverId { get; set; } + + public bool FirstAuthentication { get; set; } + public int IdNgo { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Auth/Models/UserInfo.cs b/src/api/VoteMonitor.Api.Auth/Models/UserInfo.cs new file mode 100644 index 00000000..15e83bd2 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Models/UserInfo.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Auth.Models +{ + public class UserInfo + { + public int IdNgo { get; set; } + public bool Organizer { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Auth/Profiles/UserInfoProfile.cs b/src/api/VoteMonitor.Api.Auth/Profiles/UserInfoProfile.cs new file mode 100644 index 00000000..d1e8f3fd --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Profiles/UserInfoProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using VoteMonitor.Api.Auth.Models; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Auth.Profiles +{ + public class UserInfoProfile : Profile + { + public UserInfoProfile() + { + CreateMap() + .ForMember(u => u.Organizer, opt => opt.MapFrom(a => a.Ngo.Organizer)); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Auth/Queries/NgoAdminApplicationUser.cs b/src/api/VoteMonitor.Api.Auth/Queries/NgoAdminApplicationUser.cs new file mode 100644 index 00000000..bc278a92 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/Queries/NgoAdminApplicationUser.cs @@ -0,0 +1,18 @@ +using MediatR; +using System.ComponentModel.DataAnnotations; +using VoteMonitor.Api.Auth.Models; +using VoteMonitor.Api.Core.Models; + +namespace VoteMonitor.Api.Auth.Queries +{ + public class NgoAdminApplicationUser : IRequest + { + [Required(AllowEmptyStrings = false)] + public string UserName { get; set; } + + [Required(AllowEmptyStrings = false)] + public string Password { get; set; } + + public UserType UserType { get; set; } + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/ApplicationUser.cs b/src/api/VoteMonitor.Api.Auth/Queries/ObserverApplicationUser.cs similarity index 65% rename from private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/ApplicationUser.cs rename to src/api/VoteMonitor.Api.Auth/Queries/ObserverApplicationUser.cs index 1dd26a57..455519ce 100644 --- a/private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/ApplicationUser.cs +++ b/src/api/VoteMonitor.Api.Auth/Queries/ObserverApplicationUser.cs @@ -1,12 +1,13 @@ -using System.ComponentModel.DataAnnotations; -using MediatR; +using MediatR; +using System.ComponentModel.DataAnnotations; +using VoteMonitor.Api.Auth.Models; -namespace VotingIrregularities.Api.Models.AccountViewModels +namespace VoteMonitor.Api.Auth.Queries { /// /// Model received from client applications in order to perform the authentication /// - public class ApplicationUser : IRequest + public class ObserverApplicationUser : IRequest { /// /// User's phone number @@ -28,13 +29,4 @@ public class ApplicationUser : IRequest [Required(AllowEmptyStrings = false)] public string UDID { get; set; } } - - public class RegisteredObserverModel - { - public bool IsAuthenticated { get; set; } - - public int ObserverId { get; set; } - - public bool FirstAuthentication { get; set; } - } } diff --git a/src/api/VoteMonitor.Api.Auth/VoteMonitor.Api.Auth.csproj b/src/api/VoteMonitor.Api.Auth/VoteMonitor.Api.Auth.csproj new file mode 100644 index 00000000..3aabe677 --- /dev/null +++ b/src/api/VoteMonitor.Api.Auth/VoteMonitor.Api.Auth.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + diff --git a/src/api/VoteMonitor.Api.Core/ApiListResponse.cs b/src/api/VoteMonitor.Api.Core/ApiListResponse.cs new file mode 100644 index 00000000..7ee7c08f --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/ApiListResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace VoteMonitor.Api.Core +{ + public class ApiListResponse : PagingResponseModel + { + public List Data { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.Core/ApiResponse.cs b/src/api/VoteMonitor.Api.Core/ApiResponse.cs new file mode 100644 index 00000000..4d97b665 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/ApiResponse.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Core +{ + public class ApiResponse + where T : class + { + public T Data { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/Attributes/AllowedExtensions.cs b/src/api/VoteMonitor.Api.Core/Attributes/AllowedExtensions.cs new file mode 100644 index 00000000..e1639e9b --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Attributes/AllowedExtensions.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.IO; +using Microsoft.AspNetCore.Http; +using System.Linq; + +namespace VoteMonitor.Api.Core.Attributes +{ + public class AllowedExtensionsAttribute : ValidationAttribute + { + private readonly string[] _extensions; + private readonly string _validationMessage; + + public AllowedExtensionsAttribute(string[] extensions) : this(extensions, "This extension is not allowed") + { + } + + public AllowedExtensionsAttribute(string[] extensions, string validationMessage) + { + _extensions = extensions; + _validationMessage = validationMessage; + } + + protected override ValidationResult IsValid( + object value, ValidationContext validationContext) + { + if (value is IFormFile file) + { + var extension = Path.GetExtension(file.FileName); + + if (!_extensions.Contains(extension.ToLower())) + { + return new ValidationResult(_validationMessage); + } + + } + + return ValidationResult.Success; + } + + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/ClaimsHelper.cs b/src/api/VoteMonitor.Api.Core/ClaimsHelper.cs new file mode 100644 index 00000000..10ba77bc --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/ClaimsHelper.cs @@ -0,0 +1,14 @@ +namespace VoteMonitor.Api.Core +{ + public static class ClaimsHelper + { + public const string ObserverIdProperty = "ObserverId"; + public const string GenericIdProvider = "Token"; + public const string IdNgo = "IdNgo"; + public const string Organizer = "Organizer"; + public const string UserType = "UserType"; + public static readonly string TOKEN_VALUE = "Token"; + public static readonly string AUTH_HEADER_VALUE = "Authorization"; + + } +} diff --git a/src/api/VoteMonitor.Api.Core/Commands/UploadFileCommand.cs b/src/api/VoteMonitor.Api.Core/Commands/UploadFileCommand.cs new file mode 100644 index 00000000..7984867a --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Commands/UploadFileCommand.cs @@ -0,0 +1,11 @@ +using MediatR; +using Microsoft.AspNetCore.Http; + +namespace VoteMonitor.Api.Core.Commands +{ + public class UploadFileCommand : IRequest + { + public IFormFile File { get; set; } + public UploadType UploadType { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/ControllerExtensions.cs b/src/api/VoteMonitor.Api.Core/ControllerExtensions.cs new file mode 100644 index 00000000..f1fef29d --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/ControllerExtensions.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace VoteMonitor.Api.Core +{ + public static class ControllerExtensions + { + public static readonly int LOWER_OBS_VALUE = 1; + public static readonly int UPPER_OBS_VALUE = 300; + public static readonly string RESET_ERROR_MESSAGE = "Internal server error, please verify that provided id is correct "; + public static readonly string DEVICE_RESET = "device"; + public static readonly string PASSWORD_RESET = "reset-password"; + + public static int GetIdOngOrDefault(this Controller controller, int defaultIdOng) + { + return int.TryParse(controller.User.Claims.FirstOrDefault(a => a.Type == ClaimsHelper.IdNgo)?.Value, out var result) + ? result + : defaultIdOng; + } + + public static int GetIdObserver(this Controller controller) + { + return int.Parse(controller.User.Claims.First(c => c.Type == ClaimsHelper.ObserverIdProperty).Value); + } + public static bool GetOrganizatorOrDefault(this Controller controller, bool defaultOrganizator) + { + return bool.TryParse(controller.User.Claims.FirstOrDefault(a => a.Type == ClaimsHelper.Organizer)?.Value, out var result) + ? result + : defaultOrganizator; + } + + public static bool ValidateGenerateObserversNumber(int number) + { + return (number > LOWER_OBS_VALUE) && (number < UPPER_OBS_VALUE); + } + public static IAsyncResult ResultAsync(this Controller controller, HttpStatusCode statusCode, ModelStateDictionary modelState = null) + { + controller.Response.StatusCode = (int)statusCode; + + if (modelState == null) + { + return Task.FromResult(new StatusCodeResult((int)statusCode)); + } + + return Task.FromResult(controller.BadRequest(modelState)); + } + } +} diff --git a/src/api/VoteMonitor.Api.Core/Extensions/OptionsExtensions.cs b/src/api/VoteMonitor.Api.Core/Extensions/OptionsExtensions.cs new file mode 100644 index 00000000..5bbee893 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Extensions/OptionsExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using VoteMonitor.Api.Core.Options; + +namespace VoteMonitor.Api.Core.Extensions +{ + public static class OptionsExtensions + { + /// + /// At this point this is (I guess) useles; + /// We use the SimpleInjector's container and registering these services in the default container does not benefit us quite a lot.. + /// + /// + /// + /// + public static IServiceCollection ConfigureCustomOptions(this IServiceCollection services, + IConfiguration configuration) + { + services.Configure(configuration.GetSection(nameof(BlobStorageOptions))); + services.Configure(configuration.GetHashOptions()); + services.Configure(configuration.GetSection(nameof(MobileSecurityOptions))); + services.Configure(configuration.GetSection(nameof(FileServiceOptions))); + services.Configure(configuration.GetSection(nameof(FirebaseServiceOptions))); + services.Configure(configuration.GetSection(nameof(DefaultNgoOptions))); + services.Configure(configuration.GetSection(nameof(ApplicationCacheOptions))); + services.Configure(configuration.GetSection(nameof(PollingStationsOptions))); + return services; + } + + public static IConfigurationSection GetHashOptions(this IConfiguration configuration) + { + return configuration.GetSection(nameof(HashOptions)); + } + } +} diff --git a/src/api/VoteMonitor.Api.Core/Handlers/UploadFileHandler.cs b/src/api/VoteMonitor.Api.Core/Handlers/UploadFileHandler.cs new file mode 100644 index 00000000..c5acb7c3 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Handlers/UploadFileHandler.cs @@ -0,0 +1,36 @@ +using MediatR; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Core.Commands; +using VoteMonitor.Api.Core.Services; + +namespace VoteMonitor.Api.Core.Handlers +{ + public class UploadFileHandler : IRequestHandler + { + private readonly IFileService _fileService; + + public UploadFileHandler(IFileService fileService) + { + _fileService = fileService; + } + + /// + /// Uploads a file in azure blob storage + /// + /// The url of the blob + public async Task Handle(UploadFileCommand message, CancellationToken cancellationToken) + { + if (message.File != null) + { + return await _fileService.UploadFromStreamAsync(message.File.OpenReadStream(), + message.File.ContentType, + Path.GetExtension(message.File.FileName), + message.UploadType); + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/ListExtensions.cs b/src/api/VoteMonitor.Api.Core/ListExtensions.cs new file mode 100644 index 00000000..2d36c34e --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/ListExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace VoteMonitor.Api.Core +{ + public static class ListExtensions + { + public static List Paginate(this List unPagedList, int page, int pageSize) + { + return unPagedList + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + } +} diff --git a/src/api/VoteMonitor.Api.Core/Models/CountyPollingStationLimit.cs b/src/api/VoteMonitor.Api.Core/Models/CountyPollingStationLimit.cs new file mode 100644 index 00000000..26e68795 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Models/CountyPollingStationLimit.cs @@ -0,0 +1,12 @@ +namespace VoteMonitor.Api.Location.Models +{ + public class CountyPollingStationLimit + { + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public int Limit { get; set; } + public bool Diaspora { get; set; } + public int Order { get; set; } + } +} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/JwtIssuerOptions.cs b/src/api/VoteMonitor.Api.Core/Models/JwtIssuerOptions.cs similarity index 96% rename from private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/JwtIssuerOptions.cs rename to src/api/VoteMonitor.Api.Core/Models/JwtIssuerOptions.cs index 2fa711a4..a91a0724 100644 --- a/private-api/app/src/VotingIrregularities.Api/Models/AccountViewModels/JwtIssuerOptions.cs +++ b/src/api/VoteMonitor.Api.Core/Models/JwtIssuerOptions.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Microsoft.IdentityModel.Tokens; +using System; using System.Threading.Tasks; -using Microsoft.IdentityModel.Tokens; -namespace VotingIrregularities.Api.Models.AccountViewModels +namespace VoteMonitor.Api.Core.Models { public class JwtIssuerOptions { diff --git a/src/api/VoteMonitor.Api.Core/Models/UserType.cs b/src/api/VoteMonitor.Api.Core/Models/UserType.cs new file mode 100644 index 00000000..a9181fa8 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Models/UserType.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Core.Models +{ + public enum UserType + { + Observer, + NgoAdmin + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/Options/ApplicationCacheOptions.cs b/src/api/VoteMonitor.Api.Core/Options/ApplicationCacheOptions.cs new file mode 100644 index 00000000..beff8513 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Options/ApplicationCacheOptions.cs @@ -0,0 +1,20 @@ +namespace VoteMonitor.Api.Core.Options +{ + public class ApplicationCacheOptions + { + public int Hours { get; set; } + + public int Minutes { get; set; } = 30; + + public int Seconds { get; set; } + + public string Implementation { get; set; } = ApplicationCacheImplementationType.NoCache.ToString(); + } + + public enum ApplicationCacheImplementationType + { + NoCache, + MemoryDistributedCache, + RedisCache + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Options/BlobStorageOptions.cs b/src/api/VoteMonitor.Api.Core/Options/BlobStorageOptions.cs similarity index 94% rename from private-api/app/src/VotingIrregularities.Api/Options/BlobStorageOptions.cs rename to src/api/VoteMonitor.Api.Core/Options/BlobStorageOptions.cs index 8159dfe7..02c28748 100644 --- a/private-api/app/src/VotingIrregularities.Api/Options/BlobStorageOptions.cs +++ b/src/api/VoteMonitor.Api.Core/Options/BlobStorageOptions.cs @@ -1,4 +1,4 @@ -namespace VotingIrregularities.Api.Models +namespace VoteMonitor.Api.Core.Options { /// /// Manages the details about the blob storage being used diff --git a/src/api/VoteMonitor.Api.Core/Options/DefaultNgoOptions.cs b/src/api/VoteMonitor.Api.Core/Options/DefaultNgoOptions.cs new file mode 100644 index 00000000..03fb14fe --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Options/DefaultNgoOptions.cs @@ -0,0 +1,7 @@ +namespace VoteMonitor.Api.Core.Options +{ + public class DefaultNgoOptions + { + public int DefaultNgoId { get; set; } + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Options/FileServiceOptions.cs b/src/api/VoteMonitor.Api.Core/Options/FileServiceOptions.cs similarity index 77% rename from private-api/app/src/VotingIrregularities.Api/Options/FileServiceOptions.cs rename to src/api/VoteMonitor.Api.Core/Options/FileServiceOptions.cs index f4383eb2..8e404c74 100644 --- a/private-api/app/src/VotingIrregularities.Api/Options/FileServiceOptions.cs +++ b/src/api/VoteMonitor.Api.Core/Options/FileServiceOptions.cs @@ -1,4 +1,6 @@ -namespace VotingIrregularities.Api.Options +using System.Collections.Generic; + +namespace VoteMonitor.Api.Core.Options { /// /// Options for defining the FileService implementation @@ -14,6 +16,6 @@ public class FileServiceOptions /// Only relevand when `Type`=`LocalFileService`. /// This will be a relative path (`\notes`). Make sure you configure your container persistent storage on this path /// - public string StoragePath { get; set; } + public Dictionary StoragePaths { get; set; } } } \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/Options/FirebaseServiceOptions.cs b/src/api/VoteMonitor.Api.Core/Options/FirebaseServiceOptions.cs new file mode 100644 index 00000000..854862ff --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Options/FirebaseServiceOptions.cs @@ -0,0 +1,7 @@ +namespace VoteMonitor.Api.Core.Options +{ + public class FirebaseServiceOptions + { + public string ServerKey { get; set; } + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Options/HashOptions.cs b/src/api/VoteMonitor.Api.Core/Options/HashOptions.cs similarity index 93% rename from private-api/app/src/VotingIrregularities.Api/Options/HashOptions.cs rename to src/api/VoteMonitor.Api.Core/Options/HashOptions.cs index 1871d12d..ca158148 100644 --- a/private-api/app/src/VotingIrregularities.Api/Options/HashOptions.cs +++ b/src/api/VoteMonitor.Api.Core/Options/HashOptions.cs @@ -1,4 +1,4 @@ -namespace VotingIrregularities.Api.Models +namespace VoteMonitor.Api.Core.Options { public class HashOptions { diff --git a/private-api/app/src/VotingIrregularities.Api/Options/MobileSecurityOptions.cs b/src/api/VoteMonitor.Api.Core/Options/MobileSecurityOptions.cs similarity index 78% rename from private-api/app/src/VotingIrregularities.Api/Options/MobileSecurityOptions.cs rename to src/api/VoteMonitor.Api.Core/Options/MobileSecurityOptions.cs index 5592b774..eca1990d 100644 --- a/private-api/app/src/VotingIrregularities.Api/Options/MobileSecurityOptions.cs +++ b/src/api/VoteMonitor.Api.Core/Options/MobileSecurityOptions.cs @@ -1,4 +1,4 @@ -namespace VotingIrregularities.Api.Options +namespace VoteMonitor.Api.Core.Options { public class MobileSecurityOptions { diff --git a/src/api/VoteMonitor.Api.Core/Options/PollingStationsOptions.cs b/src/api/VoteMonitor.Api.Core/Options/PollingStationsOptions.cs new file mode 100644 index 00000000..58e9da32 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Options/PollingStationsOptions.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Core.Options +{ + public class PollingStationsOptions + { + public bool OverrideDefaultSorting { get; set; } + public string CodeOfFirstToDisplayCounty { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/PagingDefaultsConstants.cs b/src/api/VoteMonitor.Api.Core/PagingDefaultsConstants.cs new file mode 100644 index 00000000..7ffc448a --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/PagingDefaultsConstants.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Core +{ + public static class PagingDefaultsConstants + { + public const int DEFAULT_PAGE_SIZE = 20; + public const int DEFAULT_PAGE = 1; + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/PagingModel.cs b/src/api/VoteMonitor.Api.Core/PagingModel.cs new file mode 100644 index 00000000..99e8a9c3 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/PagingModel.cs @@ -0,0 +1,20 @@ +namespace VoteMonitor.Api.Core +{ + public class PagingModel + { + protected int _page; + protected int _pageSize; + + public int Page + { + get => _page < 1 ? PagingDefaultsConstants.DEFAULT_PAGE : _page; + set => _page = value < 1 ? PagingDefaultsConstants.DEFAULT_PAGE : value; + } + + public int PageSize + { + get => _pageSize < 1 ? PagingDefaultsConstants.DEFAULT_PAGE_SIZE : _pageSize; + set => _pageSize = value < 1 ? PagingDefaultsConstants.DEFAULT_PAGE_SIZE : value; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Core/PagingResponseModel.cs b/src/api/VoteMonitor.Api.Core/PagingResponseModel.cs new file mode 100644 index 00000000..fab8d000 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/PagingResponseModel.cs @@ -0,0 +1,18 @@ +namespace VoteMonitor.Api.Core +{ + public class PagingResponseModel : PagingModel + { + protected int _totalItems; + + public int TotalItems + { + get { return _totalItems; } + set { _totalItems = value; } + } + + public int TotalPages + { + get { return 1 + (_totalItems - 1) / _pageSize; } + } + } +} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Services/BlobService.cs b/src/api/VoteMonitor.Api.Core/Services/BlobService.cs similarity index 80% rename from private-api/app/src/VotingIrregularities.Api/Services/BlobService.cs rename to src/api/VoteMonitor.Api.Core/Services/BlobService.cs index 92f60053..abb3606c 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/BlobService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/BlobService.cs @@ -5,9 +5,9 @@ using System; using System.IO; using System.Threading.Tasks; -using VotingIrregularities.Api.Models; +using VoteMonitor.Api.Core.Options; -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { /// public class BlobService : IFileService @@ -29,24 +29,32 @@ public BlobService(IOptions storageOptions) /// /// Uploads a file from a stream in azure blob storage /// - public async Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension) + public async Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension, UploadType uploadType) { // Get a reference to the container. var container = _client.GetContainerReference(_storageOptions.Value.Container); - // Create the container if it doesn't already exist. - await container.CreateIfNotExistsAsync(); - // Retrieve reference to a blob. var blockBlob = container.GetBlockBlobReference(Guid.NewGuid().ToString("N") + extension); // Create or overwrite the previous created blob with contents from stream. - await blockBlob.UploadFromStreamAsync(sourceStream); - blockBlob.Properties.ContentType = mimeType; + + await blockBlob.UploadFromStreamAsync(sourceStream, sourceStream.Length); + + await blockBlob.SetPropertiesAsync(); return blockBlob.Uri.ToString(); } + + public async Task Initialize() + { + // Get a reference to the container. + var container = _client.GetContainerReference(_storageOptions.Value.Container); + + // Create the container if it doesn't already exist. + await container.CreateIfNotExistsAsync(); + } } } diff --git a/private-api/app/src/VotingIrregularities.Api/Services/CacheService.cs b/src/api/VoteMonitor.Api.Core/Services/CacheService.cs similarity index 70% rename from private-api/app/src/VotingIrregularities.Api/Services/CacheService.cs rename to src/api/VoteMonitor.Api.Core/Services/CacheService.cs index 02f83db4..5efcf0b6 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/CacheService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/CacheService.cs @@ -1,10 +1,10 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using System; +using System.Threading.Tasks; -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { /// public class CacheService : ICacheService @@ -18,12 +18,15 @@ public CacheService(IDistributedCache cache, ILogger logger) _logger = logger; } - public async Task GetOrSaveDataInCacheAsync(CacheObjectsName name, Func> source, DistributedCacheEntryOptions options = null) + public async Task GetOrSaveDataInCacheAsync(string name, Func> source, + DistributedCacheEntryOptions options = null) { var obj = await GetObjectSafeAsync(name); if (obj != null) + { return obj; + } var result = await source(); @@ -32,7 +35,7 @@ public async Task GetOrSaveDataInCacheAsync(CacheObjectsName name, Func GetObjectSafeAsync(CacheObjectsName name) + public async Task GetObjectSafeAsync(string name) { var result = default(T); @@ -53,23 +56,26 @@ public async Task GetObjectSafeAsync(CacheObjectsName name) } catch (Exception exception) { - _logger.LogError(GetHashCode(),exception,exception.Message); + _logger.LogError(GetHashCode(), exception, exception.Message); } return result; } - public async Task SaveObjectSafeAsync(CacheObjectsName name, object value, DistributedCacheEntryOptions options = null) + public async Task SaveObjectSafeAsync(string name, object value, DistributedCacheEntryOptions options = null) { try { var obj = JsonConvert.SerializeObject(value); if (options != null) + { await _cache.SetAsync(name.ToString(), GetBytes(obj), options); + } else + { await _cache.SetAsync(name.ToString(), GetBytes(obj)); - + } } catch (Exception exception) { @@ -83,6 +89,7 @@ private static byte[] GetBytes(string str) System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length); return bytes; } + private static string GetString(byte[] bytes) { var chars = new char[bytes.Length / sizeof(char)]; @@ -91,22 +98,4 @@ private static string GetString(byte[] bytes) } } - /// - /// Enum for forms' names in cache - /// - public enum CacheObjectsName - { - /// - /// First form - /// - FormularA, - /// - /// Second form - /// - FormularB, - /// - /// this is becoming redundant - /// - FormularC - } } diff --git a/private-api/app/src/VotingIrregularities.Api/Services/ClearTextService.cs b/src/api/VoteMonitor.Api.Core/Services/ClearTextService.cs similarity index 78% rename from private-api/app/src/VotingIrregularities.Api/Services/ClearTextService.cs rename to src/api/VoteMonitor.Api.Core/Services/ClearTextService.cs index 49514da8..89bdab4f 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/ClearTextService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/ClearTextService.cs @@ -1,4 +1,4 @@ -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { public class ClearTextService : IHashService { diff --git a/src/api/VoteMonitor.Api.Core/Services/FirebaseService.cs b/src/api/VoteMonitor.Api.Core/Services/FirebaseService.cs new file mode 100644 index 00000000..23cf05db --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Services/FirebaseService.cs @@ -0,0 +1,56 @@ +using FirebaseAdmin; +using FirebaseAdmin.Messaging; +using Google.Apis.Auth.OAuth2; +using System.Collections.Generic; +using System.Linq; + +namespace VoteMonitor.Api.Core.Services +{ + public class FirebaseService : IFirebaseService + { + public int SendAsync(string from, string title, string message, List recipients) + { + if (FirebaseApp.DefaultInstance == null) + { + FirebaseApp.Create(new AppOptions() + { + Credential = GoogleCredential.GetApplicationDefault(), + }); + } + + int successCount = 0; + + if (recipients == null) + { + return successCount; + } + + while (recipients.Any()) + { + var registrationTokens = recipients.Take(100).ToList().AsReadOnly(); + recipients = recipients.Skip(100).ToList(); + + var message2 = new MulticastMessage + { + Tokens = registrationTokens, + Data = new Dictionary + { + { "title", title }, + { "body", message }, + }, + Notification = new Notification + { + Title = title, + Body = message + }, + }; + + var response = FirebaseMessaging.DefaultInstance.SendMulticastAsync(message2); + response.Wait(); + successCount += response.Result.SuccessCount; + } + + return successCount; + } + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/HashService.cs b/src/api/VoteMonitor.Api.Core/Services/HashService.cs similarity index 85% rename from private-api/app/src/VotingIrregularities.Api/Services/HashService.cs rename to src/api/VoteMonitor.Api.Core/Services/HashService.cs index 376a11ad..34070535 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/HashService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/HashService.cs @@ -1,10 +1,10 @@ -using System; +using Microsoft.Extensions.Options; +using System; using System.Security.Cryptography; using System.Text; -using Microsoft.Extensions.Options; -using VotingIrregularities.Api.Models; +using VoteMonitor.Api.Core.Options; -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { /// public class HashService : IHashService diff --git a/src/api/VoteMonitor.Api.Core/Services/ICacheService.cs b/src/api/VoteMonitor.Api.Core/Services/ICacheService.cs new file mode 100644 index 00000000..a570acf0 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Services/ICacheService.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Caching.Distributed; +using System; +using System.Threading.Tasks; + +namespace VoteMonitor.Api.Core.Services +{ + /// + /// Interface for the caching service to be used. + /// + public interface ICacheService + { + Task GetOrSaveDataInCacheAsync(string name, Func> source, DistributedCacheEntryOptions options = null); + Task GetObjectSafeAsync(string name); + Task SaveObjectSafeAsync(string name, object value, DistributedCacheEntryOptions options = null); + + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/IFileService.cs b/src/api/VoteMonitor.Api.Core/Services/IFileService.cs similarity index 67% rename from private-api/app/src/VotingIrregularities.Api/Services/IFileService.cs rename to src/api/VoteMonitor.Api.Core/Services/IFileService.cs index e54e294c..39502772 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/IFileService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/IFileService.cs @@ -1,7 +1,7 @@ using System.IO; using System.Threading.Tasks; -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { /// /// Interface for the file service to be used @@ -15,6 +15,12 @@ public interface IFileService /// /// /// the reference to the resource just uploaded - Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension); + Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension, UploadType uploadType); + + /// + /// Initialize the file system before first usage + /// + /// + Task Initialize(); } } diff --git a/src/api/VoteMonitor.Api.Core/Services/IFirebaseService.cs b/src/api/VoteMonitor.Api.Core/Services/IFirebaseService.cs new file mode 100644 index 00000000..466aa9d3 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Services/IFirebaseService.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace VoteMonitor.Api.Core.Services +{ + public interface IFirebaseService + { + int SendAsync(string from, string title, string message, List recipients); + } +} diff --git a/private-api/app/src/VotingIrregularities.Api/Services/IHashService.cs b/src/api/VoteMonitor.Api.Core/Services/IHashService.cs similarity index 73% rename from private-api/app/src/VotingIrregularities.Api/Services/IHashService.cs rename to src/api/VoteMonitor.Api.Core/Services/IHashService.cs index f4712497..a9ad2e60 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/IHashService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/IHashService.cs @@ -1,4 +1,4 @@ -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { public interface IHashService { diff --git a/src/api/VoteMonitor.Api.Core/Services/IPollingStationService.cs b/src/api/VoteMonitor.Api.Core/Services/IPollingStationService.cs new file mode 100644 index 00000000..af881626 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Services/IPollingStationService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using VoteMonitor.Api.Location.Models; + +namespace VoteMonitor.Api.Location.Services +{ + public interface IPollingStationService + { + Task GetPollingStationByCountyCode(int pollingStationNumber, string countyCode); + Task GetPollingStationByCountyId(int pollingStationNumber, int countyId); + Task> GetPollingStationsAssignmentsForAllCounties(bool? diaspora); + } +} \ No newline at end of file diff --git a/private-api/app/src/VotingIrregularities.Api/Services/LocalFileService.cs b/src/api/VoteMonitor.Api.Core/Services/LocalFileService.cs similarity index 74% rename from private-api/app/src/VotingIrregularities.Api/Services/LocalFileService.cs rename to src/api/VoteMonitor.Api.Core/Services/LocalFileService.cs index a37fd239..251e83f2 100644 --- a/private-api/app/src/VotingIrregularities.Api/Services/LocalFileService.cs +++ b/src/api/VoteMonitor.Api.Core/Services/LocalFileService.cs @@ -2,9 +2,9 @@ using System; using System.IO; using System.Threading.Tasks; -using VotingIrregularities.Api.Options; +using VoteMonitor.Api.Core.Options; -namespace VotingIrregularities.Api.Services +namespace VoteMonitor.Api.Core.Services { /// /// This will be used just for development purposes @@ -21,11 +21,11 @@ public LocalFileService(IOptions options) { _localFileOptions = options.Value; } - public Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension) + public Task UploadFromStreamAsync(Stream sourceStream, string mimeType, string extension, UploadType uploadType) { // set name - var localFile = _localFileOptions.StoragePath + "\\" + Guid.NewGuid().ToString("N") + extension; - + var localFile = _localFileOptions.StoragePaths[uploadType.ToString()] + "\\" + Guid.NewGuid().ToString("N") + extension; + // save to local path using (var fileStream = File.Create(localFile)) { @@ -36,5 +36,10 @@ public Task UploadFromStreamAsync(Stream sourceStream, string mimeType, // return relative path return Task.FromResult(localFile); } + + public Task Initialize() + { + return null; + } } } diff --git a/src/api/VoteMonitor.Api.Core/Services/NoCacheService.cs b/src/api/VoteMonitor.Api.Core/Services/NoCacheService.cs new file mode 100644 index 00000000..318b3d4d --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/Services/NoCacheService.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Caching.Distributed; +using System; +using System.Threading.Tasks; + +namespace VoteMonitor.Api.Core.Services +{ + public class NoCacheService : ICacheService + { + public async Task GetOrSaveDataInCacheAsync(string name, Func> source, + DistributedCacheEntryOptions options = null) + { + return await source(); + } + + public Task GetObjectSafeAsync(string name) => throw new NotImplementedException(); + + public Task SaveObjectSafeAsync(string name, object value, + DistributedCacheEntryOptions options = null) => throw new NotImplementedException(); + } +} diff --git a/src/api/VoteMonitor.Api.Core/UploadType.cs b/src/api/VoteMonitor.Api.Core/UploadType.cs new file mode 100644 index 00000000..9db79012 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/UploadType.cs @@ -0,0 +1,8 @@ +namespace VoteMonitor.Api.Core +{ + public enum UploadType + { + Notes, + Observers + } +} diff --git a/src/api/VoteMonitor.Api.Core/VoteMonitor.Api.Core.csproj b/src/api/VoteMonitor.Api.Core/VoteMonitor.Api.Core.csproj new file mode 100644 index 00000000..88440594 --- /dev/null +++ b/src/api/VoteMonitor.Api.Core/VoteMonitor.Api.Core.csproj @@ -0,0 +1,16 @@ + + + netcoreapp3.1 + + + + + + + + + + + + + diff --git a/src/api/VoteMonitor.Api.County/Commands/CreateOrUpdateCounties.cs b/src/api/VoteMonitor.Api.County/Commands/CreateOrUpdateCounties.cs new file mode 100644 index 00000000..ae15c731 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Commands/CreateOrUpdateCounties.cs @@ -0,0 +1,16 @@ +using CSharpFunctionalExtensions; +using MediatR; +using Microsoft.AspNetCore.Http; + +namespace VoteMonitor.Api.County.Commands +{ + public class CreateOrUpdateCounties: IRequest + { + public IFormFile File { get; } + + public CreateOrUpdateCounties(IFormFile file) + { + File = file; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Commands/UpdateCounty.cs b/src/api/VoteMonitor.Api.County/Commands/UpdateCounty.cs new file mode 100644 index 00000000..b63ed9ee --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Commands/UpdateCounty.cs @@ -0,0 +1,24 @@ +using CSharpFunctionalExtensions; +using MediatR; +using VoteMonitor.Api.County.Models; + +namespace VoteMonitor.Api.County.Commands +{ + public class UpdateCounty : IRequest + { + public CountyModel County { get; } + + public UpdateCounty(int countyId, UpdateCountyModel county) + { + County = new CountyModel + { + Id = countyId, + Name = county.Name, + Code = county.Code, + Diaspora = county.Diaspora, + Order = county.Order, + NumberOfPollingStations = county.NumberOfPollingStations + }; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Controllers/CountyController.cs b/src/api/VoteMonitor.Api.County/Controllers/CountyController.cs new file mode 100644 index 00000000..0e663aac --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Controllers/CountyController.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.IO; +using System.Threading.Tasks; +using VoteMonitor.Api.County.Models; +using CsvHelper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using VoteMonitor.Api.County.Commands; +using VoteMonitor.Api.County.Queries; + +namespace VoteMonitor.Api.County.Controllers +{ + [Route("api/v1/county")] + public class CountyController : Controller + { + private readonly IMediator _mediator; + public CountyController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet] + [Route("csvFormat")] + [Authorize("Organizer")] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task ExportToCsvAsync() + { + var dataResult = await _mediator.Send(new GetCountiesForExport()); + + if (dataResult.IsFailure) + { + return BadRequest(dataResult.Error); + } + + using (var mem = new MemoryStream()) + using (var writer = new StreamWriter(mem)) + using (var csvWriter = new CsvWriter(writer)) + { + csvWriter.Configuration.HasHeaderRecord = true; + csvWriter.Configuration.AutoMap(); + + csvWriter.WriteRecords(dataResult.Value); + + writer.Flush(); + return File(mem.ToArray(), "application/octet-stream", "counties.csv"); + } + } + + [HttpPost] + [Route("import")] + [Authorize("Organizer")] + public async Task ImportAsync(CountiesUploadModel request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var response = await _mediator.Send(new CreateOrUpdateCounties(request.CsvFile)); + if (response.IsSuccess) + { + return Ok(); + } + + return BadRequest(new ErrorModel { Message = response.Error }); + } + + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task GetAllCountiesAsync() + { + var response = await _mediator.Send(new GetAllCounties()); + if (response.IsSuccess) + { + return Ok(response.Value); + } + + return BadRequest(new ErrorModel { Message = response.Error }); + } + + [HttpGet("{countyId}")] + [Authorize("NgoAdmin")] + [ProducesResponseType(typeof(CountyModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task GetCountyAsync(int countyId) + { + var response = await _mediator.Send(new GetCounty(countyId)); + if (response.IsSuccess) + { + return Ok(response.Value); + } + + return BadRequest(new ErrorModel { Message = response.Error }); + } + + [HttpPost("{countyId}")] + [Authorize("Organizer")] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task UpdateCountyAsync(int countyId, [FromBody] UpdateCountyModel county) + { + var response = await _mediator.Send(new UpdateCounty(countyId, county)); + if (response.IsSuccess) + { + return Ok(); + } + + return BadRequest(new ErrorModel { Message = response.Error }); + } + } +} diff --git a/src/api/VoteMonitor.Api.County/Handlers/CountiesCommandHandler.cs b/src/api/VoteMonitor.Api.County/Handlers/CountiesCommandHandler.cs new file mode 100644 index 00000000..5cf8bf91 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Handlers/CountiesCommandHandler.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using CSharpFunctionalExtensions; +using CsvHelper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using VoteMonitor.Api.County.Commands; +using VoteMonitor.Api.County.Models; +using VoteMonitor.Api.County.Queries; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.County.Handlers +{ + public class CountiesCommandHandler : IRequestHandler>>, + IRequestHandler, + IRequestHandler>>, + IRequestHandler>, + IRequestHandler + + { + private readonly VoteMonitorContext _context; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public CountiesCommandHandler(VoteMonitorContext context, ILogger logger, IMapper mapper) + { + _context = context; + _logger = logger; + _mapper = mapper; + } + + public async Task>> Handle(GetCountiesForExport request, CancellationToken cancellationToken) + { + return await Result.Try(async () => + { + return await _context.Counties + .OrderBy(c => c.Order) + .Select(c => _mapper.Map(c)) + .ToListAsync(cancellationToken); + }, ex => + { + _logger.LogError("Error retrieving counties", ex); + return "Cannot retrieve counties."; + }); + } + + public async Task Handle(CreateOrUpdateCounties request, CancellationToken cancellationToken) + { + var result = await ReadFromCsv(request) + .Ensure(x => x != null && x.Count > 0, "No counties to add or update") + .Bind(x => ValidateData(x)) + .Tap(async x => await InsertOrUpdateCounties(x, cancellationToken)); + + return result; + } + + private async Task InsertOrUpdateCounties(List counties, CancellationToken cancellationToken) + { + var countiesIdUpdated = new List(); + var countiesDictionary = counties.ToDictionary(c => c.Id, y => y); + + try + { + using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken); + foreach (var county in _context.Counties) + { + if (countiesDictionary.TryGetValue(county.Id, out var csvModel)) + { + county.Code = csvModel.Code; + county.Name = csvModel.Name; + county.NumberOfPollingStations = csvModel.NumberOfPollingStations; + county.Diaspora = csvModel.Diaspora; + county.Order = csvModel.Order; + + countiesIdUpdated.Add(county.Id); + } + } + + foreach (var id in countiesDictionary.Keys.Except(countiesIdUpdated)) + { + var csvModel = countiesDictionary[id]; + + var newCounty = new Entities.County + { + Id = csvModel.Id, + Code = csvModel.Code, + Name = csvModel.Name, + NumberOfPollingStations = csvModel.NumberOfPollingStations, + Diaspora = csvModel.Diaspora, + Order = csvModel.Order + }; + + _context.Counties.Add(newCounty); + } + + await _context.SaveChangesAsync(cancellationToken); + + transaction.Commit(); + } + catch (Exception exception) + { + _logger.LogError("Cannot add/update counties", exception); + return Result.Failure("Cannot add/update counties"); + } + + return Result.Ok(); + } + + private Result> ValidateData(List counties) + { + if (counties.Count != counties.Select(x => x.Id).Distinct().Count()) + { + return Result.Failure>("Duplicated id in csv found"); + } + + var invalidCounty = counties.FirstOrDefault(x => + x == null + || string.IsNullOrEmpty(x.Code) + || string.IsNullOrEmpty(x.Name) + || x.Name.Length > 100 + || x.Code.Length > 20); + + if (invalidCounty == null) + { + + return Result.Ok(counties); + } + + return Result.Failure>($"Invalid county entry found: {JsonConvert.SerializeObject(invalidCounty)}"); + } + + private Result> ReadFromCsv(CreateOrUpdateCounties request) + { + List counties; + + try + { + using var reader = new StreamReader(request.File.OpenReadStream()); + using var csv = new CsvReader(reader); + counties = csv.GetRecords() + .ToList(); + } + catch (Exception e) + { + _logger.LogError("Unable to read csv file", e); + return Result.Failure>("Cannot read csv file provided"); + } + + return Result.Ok(counties); + } + + public async Task>> Handle(GetAllCounties request, CancellationToken cancellationToken) + { + List counties; + + try + { + counties = await _context.Counties + .OrderBy(c => c.Order) + .Select(x => _mapper.Map(x)) + .ToListAsync(cancellationToken); + } + catch (Exception e) + { + _logger.LogError("Unable to load all counties", e); + return Result.Failure>("Unable to load all counties"); + } + + return Result.Ok(counties); + } + + public async Task> Handle(GetCounty request, CancellationToken cancellationToken) + { + try + { + var county = await _context.Counties.FirstOrDefaultAsync(x => x.Id == request.CountyId, cancellationToken); + if (county == null) + { + return Result.Failure($"Could not find county with id = {request.CountyId}"); + } + + var countyModel = _mapper.Map(county); + + return Result.Ok(countyModel); + } + catch (Exception e) + { + _logger.LogError($"Unable to load county {request.CountyId}", e); + return Result.Failure($"Unable to load county {request.CountyId}"); + } + } + + public async Task Handle(UpdateCounty request, CancellationToken cancellationToken) + { + try + { + var county = await _context.Counties.FirstOrDefaultAsync(x => x.Id == request.County.Id, cancellationToken); + if (county == null) + { + return Result.Failure($"Could not find county with id = {request.County.Id}"); + } + + county.Code = request.County.Code; + county.Name = request.County.Name; + county.NumberOfPollingStations = request.County.NumberOfPollingStations; + county.Diaspora = request.County.Diaspora; + county.Order = request.County.Order; + + await _context.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } + catch (Exception e) + { + _logger.LogError($"Unable to update county {request.County.Id}", e); + return Result.Failure($"Unable to update county {request.County.Id}"); + } + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Mappers/CountyMapping.cs b/src/api/VoteMonitor.Api.County/Mappers/CountyMapping.cs new file mode 100644 index 00000000..b81508a2 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Mappers/CountyMapping.cs @@ -0,0 +1,27 @@ +using AutoMapper; +using VoteMonitor.Api.County.Models; + +namespace VoteMonitor.Api.County.Mappers +{ + public class CountyMapping : Profile + { + public CountyMapping() + { + _ = CreateMap() + .ForMember(dest => dest.Id, x => x.MapFrom(src => src.Id)) + .ForMember(dest => dest.Code, x => x.MapFrom(src => src.Code)) + .ForMember(dest => dest.Diaspora, x => x.MapFrom(src => src.Diaspora)) + .ForMember(dest => dest.Name, x => x.MapFrom(src => src.Name)) + .ForMember(dest => dest.NumberOfPollingStations, x => x.MapFrom(src => src.NumberOfPollingStations)) + .ForMember(dest => dest.Order, x => x.MapFrom(src => src.Order)); + + _ = CreateMap() + .ForMember(dest => dest.Id, x => x.MapFrom(src => src.Id)) + .ForMember(dest => dest.Code, x => x.MapFrom(src => src.Code)) + .ForMember(dest => dest.Diaspora, x => x.MapFrom(src => src.Diaspora)) + .ForMember(dest => dest.Name, x => x.MapFrom(src => src.Name)) + .ForMember(dest => dest.NumberOfPollingStations, x => x.MapFrom(src => src.NumberOfPollingStations)) + .ForMember(dest => dest.Order, x => x.MapFrom(src => src.Order)); + } + } +} diff --git a/src/api/VoteMonitor.Api.County/Models/CountiesUploadModel.cs b/src/api/VoteMonitor.Api.County/Models/CountiesUploadModel.cs new file mode 100644 index 00000000..a298f4a0 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Models/CountiesUploadModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; +using VoteMonitor.Api.Core.Attributes; + +namespace VoteMonitor.Api.County.Models +{ + public class CountiesUploadModel + { + [Required(ErrorMessage = "Please select a file.")] + [DataType(DataType.Upload)] + [AllowedExtensions(new string[] { ".csv" })] + public IFormFile CsvFile { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Models/CountyCsvModel.cs b/src/api/VoteMonitor.Api.County/Models/CountyCsvModel.cs new file mode 100644 index 00000000..8117579e --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Models/CountyCsvModel.cs @@ -0,0 +1,14 @@ +using CsvHelper.Configuration.Attributes; + +namespace VoteMonitor.Api.County.Models +{ + public class CountyCsvModel + { + [Index(2)] public string Name { get; set; } + [Index(1)] public string Code { get; set; } + [Index(3)] public int NumberOfPollingStations { get; set; } + [Index(0)] public int Id { get; set; } + [Index(4)] public bool Diaspora { get; set; } + [Index(5)] public int Order { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Models/CountyModel.cs b/src/api/VoteMonitor.Api.County/Models/CountyModel.cs new file mode 100644 index 00000000..b29be7b0 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Models/CountyModel.cs @@ -0,0 +1,12 @@ +namespace VoteMonitor.Api.County.Models +{ + public class CountyModel + { + public string Name { get; set; } + public string Code { get; set; } + public int NumberOfPollingStations { get; set; } + public int Id { get; set; } + public bool Diaspora { get; set; } + public int Order { get; set; } + } +} diff --git a/src/api/VoteMonitor.Api.County/Models/ErrorModel.cs b/src/api/VoteMonitor.Api.County/Models/ErrorModel.cs new file mode 100644 index 00000000..2acaf296 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Models/ErrorModel.cs @@ -0,0 +1,7 @@ +namespace VoteMonitor.Api.County.Models +{ + public class ErrorModel + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Models/UpdateCountyModel.cs b/src/api/VoteMonitor.Api.County/Models/UpdateCountyModel.cs new file mode 100644 index 00000000..c1d51772 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Models/UpdateCountyModel.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace VoteMonitor.Api.County.Models +{ + public class UpdateCountyModel + { + [Required] + [StringLength(100)] + public string Name { get; set; } + + [Required] + [StringLength(20)] + public string Code { get; set; } + + public int NumberOfPollingStations { get; set; } + public bool Diaspora { get; set; } + public int Order { get; set; } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Queries/GetAllCounties.cs b/src/api/VoteMonitor.Api.County/Queries/GetAllCounties.cs new file mode 100644 index 00000000..b056ddac --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Queries/GetAllCounties.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using CSharpFunctionalExtensions; +using MediatR; +using VoteMonitor.Api.County.Models; + +namespace VoteMonitor.Api.County.Queries +{ + public class GetAllCounties : IRequest>> + { + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.County/Queries/GetCountiesForExport.cs b/src/api/VoteMonitor.Api.County/Queries/GetCountiesForExport.cs new file mode 100644 index 00000000..ab591742 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Queries/GetCountiesForExport.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using CSharpFunctionalExtensions; +using MediatR; +using VoteMonitor.Api.County.Models; + +namespace VoteMonitor.Api.County.Queries +{ + public class GetCountiesForExport : IRequest>> + { + } +} diff --git a/src/api/VoteMonitor.Api.County/Queries/GetCounty.cs b/src/api/VoteMonitor.Api.County/Queries/GetCounty.cs new file mode 100644 index 00000000..73b330a8 --- /dev/null +++ b/src/api/VoteMonitor.Api.County/Queries/GetCounty.cs @@ -0,0 +1,16 @@ +using CSharpFunctionalExtensions; +using MediatR; +using VoteMonitor.Api.County.Models; + +namespace VoteMonitor.Api.County.Queries +{ + public class GetCounty : IRequest> + { + public GetCounty(int countyId) + { + CountyId = countyId; + } + + public int CountyId { get; } + } +} diff --git a/src/api/VoteMonitor.Api.County/VoteMonitor.Api.County.csproj b/src/api/VoteMonitor.Api.County/VoteMonitor.Api.County.csproj new file mode 100644 index 00000000..8269b4fc --- /dev/null +++ b/src/api/VoteMonitor.Api.County/VoteMonitor.Api.County.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + diff --git a/src/api/VoteMonitor.Api.DataExport/Controllers/DataExportController.cs b/src/api/VoteMonitor.Api.DataExport/Controllers/DataExportController.cs new file mode 100644 index 00000000..96b43620 --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/Controllers/DataExportController.cs @@ -0,0 +1,53 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using VoteMonitor.Api.DataExport.Queries; + +namespace VoteMonitor.Api.DataExport.Controllers +{ + [Route("api/v1/export")] + public class DataExportController : Microsoft.AspNetCore.Mvc.Controller + { + private readonly IMediator _mediator; + + public DataExportController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Exports all data (which data?) into a excel file + /// + /// + [HttpGet("all")] + [Authorize("NgoAdmin")] + public async Task GetMyData(int? idNgo, int? idObserver, int? pollingStationNumber, string county, DateTime? from, DateTime? to) + { + var filter = new GetDataForExport + { + NgoId = idNgo, + ObserverId = idObserver, + PollingStationNumber = pollingStationNumber, + County = county, + From = from, + To = to + }; + + var data = await _mediator.Send(filter); + var excelFileBytes = await _mediator.Send(new GenerateExcelFile(data)); + + if (excelFileBytes == null || excelFileBytes.Length == 0) + { + return NotFound(); + } + + return File( + fileContents: excelFileBytes, + contentType: Utility.EXCEL_MEDIA_TYPE, + fileDownloadName: "data.xlsx" + ); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/ExcelExporter.cs b/src/api/VoteMonitor.Api.DataExport/ExcelExporter.cs new file mode 100644 index 00000000..e6ed2a42 --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/ExcelExporter.cs @@ -0,0 +1,191 @@ +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.IO; +using System.Text.RegularExpressions; + +namespace VoteMonitor.Api.DataExport +{ + public interface IExcelGenerator + { + byte[] Export(List exportData, string fileName, + bool appendDateTimeInFileName = false, string sheetName = Utility.DEFAULT_SHEET_NAME); + } + + public class Utility + { + public const string DEFAULT_SHEET_NAME = "Sheet1"; + public const string DEFAULT_FILE_DATETIME = "yyyyMMdd_HHmm"; + public const string DATETIME_FORMAT = "dd/MM/yyyy hh:mm:ss"; + public const string EXCEL_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + public const string DISPOSITION_TYPE_ATTACHMENT = "attachment"; + + + #region DataType available for Excel Export + public const string STRING = "string"; + public const string INT32 = "int32"; + public const string DOUBLE = "double"; + public const string DATETIME = "datetime"; + #endregion + } + public class ExcelGenerator : ExcelGeneratorBase + { + public ExcelGenerator() + { + _headers = new List(); + _type = new List(); + } + + public sealed override void WriteData(List exportData) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T)); + DataTable table = new DataTable(); + + #region Reading property name to generate cell header + foreach (PropertyDescriptor prop in properties) + { + var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + _type.Add(type.Name); + table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + string name = Regex.Replace(prop.Name, "([A-Z])", " $1").Trim(); //space seperated name by caps for header + _headers.Add(name); + } + #endregion + + #region Generating Datatable from List + foreach (T item in exportData) + { + DataRow row = table.NewRow(); + foreach (PropertyDescriptor prop in properties) + { + row[prop.Name] = prop.GetValue(item) ?? DBNull.Value; + } + + table.Rows.Add(row); + } + #endregion + + #region Generating SheetRow based on datatype + IRow sheetRow = null; + + for (int i = 0; i < table.Rows.Count; i++) + { + sheetRow = _sheet.CreateRow(i + 1); + for (int j = 0; j < table.Columns.Count; j++) + { + // TODO: Below commented code is for Wrapping and Alignment of cell + // Row1.CellStyle = CellCentertTopAlignment; + // Row1.CellStyle.WrapText = true; + // ICellStyle CellCentertTopAlignment = _workbook.CreateCellStyle(); + // CellCentertTopAlignment = _workbook.CreateCellStyle(); + // CellCentertTopAlignment.Alignment = HorizontalAlignment.Center; + + ICell Row1 = sheetRow.CreateCell(j); + string cellvalue = Convert.ToString(table.Rows[i][j]); + + // TODO: move it to switch case + + if (string.IsNullOrWhiteSpace(cellvalue)) + { + Row1.SetCellValue(string.Empty); + } + else if (_type[j].ToLower() == Utility.STRING) + { + Row1.SetCellValue(cellvalue); + } + else if (_type[j].ToLower() == Utility.INT32) + { + Row1.SetCellValue(Convert.ToInt32(table.Rows[i][j])); + } + else if (_type[j].ToLower() == Utility.DOUBLE) + { + Row1.SetCellValue(Convert.ToDouble(table.Rows[i][j])); + } + else if (_type[j].ToLower() == Utility.DATETIME) + { + Row1.SetCellValue(Convert.ToDateTime + (table.Rows[i][j]).ToString(Utility.DATETIME_FORMAT)); + } + else + { + Row1.SetCellValue(string.Empty); + } + } + } + #endregion + } + } + public abstract class ExcelGeneratorBase : IExcelGenerator + { + protected string _sheetName; + protected string _fileName; + protected List _headers; + protected List _type; + protected IWorkbook _workbook; + protected ISheet _sheet; + + /// + /// Common Code for the Export + /// It creates Workbook, Sheet, Generate Header Cells and returns HttpResponseMessage + /// + /// Generic Class Type + /// Data to be exported + /// Export File Name + /// Specify if filename should contain when file was generated + /// First Sheet Name + /// + public byte[] Export(List exportData, string fileName, + bool appendDateTimeInFileName = false, + string sheetName = Utility.DEFAULT_SHEET_NAME) + { + _sheetName = sheetName; + + _fileName = appendDateTimeInFileName + ? $"{fileName}_{DateTime.Now.ToString(Utility.DEFAULT_FILE_DATETIME)}" + : fileName; + + #region Generation of Workbook, Sheet and General Configuration + _workbook = new XSSFWorkbook(); + _sheet = _workbook.CreateSheet(_sheetName); + + var headerStyle = _workbook.CreateCellStyle(); + var headerFont = _workbook.CreateFont(); + headerFont.IsBold = true; + headerStyle.SetFont(headerFont); + #endregion + + WriteData(exportData); + + #region Generating Header Cells + var header = _sheet.CreateRow(0); + for (var i = 0; i < _headers.Count; i++) + { + var cell = header.CreateCell(i); + cell.SetCellValue(_headers[i]); + cell.CellStyle = headerStyle; + // It's heavy, it slows down your Excel if you have large data + _sheet.AutoSizeColumn(i); + } + #endregion + + #region Generating and Returning Stream for Excel + using (var memoryStream = new MemoryStream()) + { + _workbook.Write(memoryStream); + + return memoryStream.ToArray(); + } + #endregion + } + + /// + /// Generic Definition to handle all types of List + /// Overrride this function to provide your own implementation + /// + /// + public abstract void WriteData(List exportData); + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/Handlers/DataExportQueryHandler.cs b/src/api/VoteMonitor.Api.DataExport/Handlers/DataExportQueryHandler.cs new file mode 100644 index 00000000..08ed5294 --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/Handlers/DataExportQueryHandler.cs @@ -0,0 +1,127 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using Microsoft.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.DataExport.Queries; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.DataExport.Handlers +{ + public class DataExportQueryHandler : IRequestHandler> + { + private readonly VoteMonitorContext _context; + private readonly ILogger _logger; + + public DataExportQueryHandler(VoteMonitorContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task> Handle(GetDataForExport request, CancellationToken cancellationToken) + { + //var exportData = await _context.Answers + // .Where(a => a.IdObserver > 10) + // .Where(a => a.LastModified >= new DateTime(2019, 11, 08, 6, 0, 0)) + // .Where(a => a.Observer.IdNgo != 1) + // .Where(a => a.OptionAnswered != null && a.OptionAnswered.Question != null) + // .SelectMany(a => a.OptionAnswered.Question.Notes.DefaultIfEmpty(), (a, note) => new ExportModel + // { + // ObserverPhone = a.Observer.Phone, + // IdNgo = a.Observer.IdNgo, + // FormCode = a.OptionAnswered.Question.FormSection.Form.Code, + // QuestionText = a.OptionAnswered.Question.Text, + // OptionText = a.OptionAnswered.Option.Text, + // AnswerFreeText = a.Value, + // NoteText = note.Text, + // NoteAttachmentPath = note.AttachementPath, + // LastModified = a.LastModified, + // CountyCode = a.CountyCode, + // PollingStationNumber = a.PollingStationNumber + // }) + // .ToListAsync(cancellationToken); + + var query = @" SELECT + NEWID() as Id, + obs.Phone as [ObserverPhone], + obs.IdNgo, + f.Code as FormCode, + q.Text as QuestionText, + o.Text as [OptionText], + a.[Value] as [AnswerFreeText], + n.Text as NoteText, + n.AttachementPath as [NoteAttachmentPath], + a.LastModified, + a.CountyCode, + a.PollingStationNumber + FROM + (Answers a + INNER JOIN Observers obs + ON a.IdObserver = obs.Id + INNER JOIN OptionsToQuestions oq + ON a.IdOptionToQuestion = oq.Id + INNER JOIN Options o + ON oq.IdOption = o.Id + INNER JOIN Questions q + ON oq.IdQuestion = q.Id + INNER JOIN FormSections fs + ON q.IdSection = fs.Id + INNER JOIN Forms f + ON fs.IdForm = f.Id) + LEFT JOIN Notes n + ON n.IdQuestion = q.Id AND n.IdObserver = obs.Id AND n.IdPollingStation = a.IdPollingStation + WHERE + a.LastModified >= @from + AND obs.IsTestObserver = 0 + + "; + + var parameters = new List + { + new SqlParameter("@from", request.From ?? new DateTime(2019, 11, 08, 6, 0, 0)), + + }; + + if (request.ApplyFilters) + { + if (request.To.HasValue) + { + query += " AND a.LastModified <= @to "; + parameters.Add(new SqlParameter("@to", request.To ?? DateTime.Now.AddDays(2))); + } + + if (request.ObserverId.HasValue) + { + query += " AND obs.Id = @ObserverId "; + parameters.Add(new SqlParameter("@ObserverId", request.ObserverId)); + } + + if (request.NgoId.HasValue) + { + query += " AND obs.IdNgo = @IdNgo "; + parameters.Add(new SqlParameter("@IdNgo", request.NgoId)); + } + + if (!string.IsNullOrEmpty(request.County)) + { + query += " AND a.CountyCode = @County "; + parameters.Add(new SqlParameter("@County", request.County)); + } + + if (request.PollingStationNumber.HasValue) + { + query += " AND a.PollingStationNumber = @PollingStationNumber "; + parameters.Add(new SqlParameter("@PollingStationNumber", request.PollingStationNumber)); + } + } + + var exportData = _context.ExportModels.FromSqlRaw(query, parameters.ToArray()); + + return await exportData.ToListAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/Handlers/ExcelGeneratorQueryHandler.cs b/src/api/VoteMonitor.Api.DataExport/Handlers/ExcelGeneratorQueryHandler.cs new file mode 100644 index 00000000..6af534fe --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/Handlers/ExcelGeneratorQueryHandler.cs @@ -0,0 +1,24 @@ +using MediatR; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.DataExport.Queries; + +namespace VoteMonitor.Api.DataExport.Handlers +{ + public class ExcelGeneratorQueryHandler : IRequestHandler + { + private readonly IExcelGenerator _excelGenerator; + + public ExcelGeneratorQueryHandler() + { + _excelGenerator = new ExcelGenerator(); + } + + public Task Handle(GenerateExcelFile request, CancellationToken cancellationToken) + { + var fileContents = _excelGenerator.Export(request.Data, "myData"); + + return Task.FromResult(fileContents); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/Queries/GenerateExcelFile.cs b/src/api/VoteMonitor.Api.DataExport/Queries/GenerateExcelFile.cs new file mode 100644 index 00000000..bcf71425 --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/Queries/GenerateExcelFile.cs @@ -0,0 +1,16 @@ +using MediatR; +using System.Collections.Generic; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.DataExport.Queries +{ + public class GenerateExcelFile : IRequest + { + public List Data { get; } + + public GenerateExcelFile(List data) + { + Data = data; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/Queries/GetDataForExport.cs b/src/api/VoteMonitor.Api.DataExport/Queries/GetDataForExport.cs new file mode 100644 index 00000000..25dbe74f --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/Queries/GetDataForExport.cs @@ -0,0 +1,20 @@ +using MediatR; +using System; +using System.Collections.Generic; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.DataExport.Queries +{ + public class GetDataForExport : IRequest> + { + public int? NgoId { get; set; } + public int? ObserverId { get; set; } + public int? PollingStationNumber { get; set; } + public string County { get; set; } + public DateTime? From { get; set; } + public DateTime? To { get; set; } + + public bool ApplyFilters => NgoId.HasValue || ObserverId.HasValue || PollingStationNumber.HasValue || + From.HasValue || To.HasValue || !string.IsNullOrEmpty(County); + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.DataExport/VoteMonitor.Api.DataExport.csproj b/src/api/VoteMonitor.Api.DataExport/VoteMonitor.Api.DataExport.csproj new file mode 100644 index 00000000..986ff013 --- /dev/null +++ b/src/api/VoteMonitor.Api.DataExport/VoteMonitor.Api.DataExport.csproj @@ -0,0 +1,21 @@ + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + diff --git a/src/api/VoteMonitor.Api.Form/Controllers/FormController.cs b/src/api/VoteMonitor.Api.Form/Controllers/FormController.cs new file mode 100644 index 00000000..c77759e0 --- /dev/null +++ b/src/api/VoteMonitor.Api.Form/Controllers/FormController.cs @@ -0,0 +1,103 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Collections.Generic; +using System.Threading.Tasks; +using VoteMonitor.Api.Core.Options; +using VoteMonitor.Api.Form.Models; +using VoteMonitor.Api.Form.Queries; + +namespace VoteMonitor.Api.Form.Controllers +{ + /// + /// + /// Ruta Formular ofera suport pentru toate operatiile legate de formularele completate de observatori + /// + + [Route("api/v1/form")] + public class FormController : Controller + { + private readonly ApplicationCacheOptions _cacheOptions; + private readonly IMediator _mediator; + + public FormController(IMediator mediator, IOptions cacheOptions) + { + _cacheOptions = cacheOptions.Value; + _mediator = mediator; + } + + [HttpPost] + [Authorize("Organizer")] + public async Task AddForm([FromBody]FormDTO newForm) + { + FormDTO result = await _mediator.Send(new AddFormQuery { Form = newForm }); + return result.Id; + } + /// + /// Returneaza versiunea tuturor formularelor sub forma unui array. + /// Daca versiunea returnata difera de cea din aplicatie, atunci trebuie incarcat formularul din nou + /// + /// + [HttpGet("versions")] + [Produces(typeof(Dictionary))] + public async Task GetFormVersions() + { + var formsAsDict = new Dictionary(); + (await _mediator.Send(new FormVersionQuery(null))).ForEach(form => formsAsDict.Add(form.Code, form.CurrentVersion)); + + return Ok(new { Versions = formsAsDict }); + } + + /// + /// Returns an array of forms + /// + /// + [HttpGet] + public async Task GetFormsAsync(bool? diaspora) + => Ok(new FormVersionsModel { FormVersions = await _mediator.Send(new FormVersionQuery(diaspora)) }); + + /// + /// Se interogheaza ultima versiunea a formularului pentru observatori si se primeste definitia lui. + /// In definitia unui formular nu intra intrebarile standard (ora sosirii, etc). + /// Acestea se considera implicite pe fiecare formular. + /// + /// Id-ul formularului pentru care trebuie preluata definitia + /// + [HttpGet("{formId}")] + public async Task> GetFormAsync(int formId) + { + var result = await _mediator.Send(new FormQuestionQuery + { + FormId = formId, + CacheHours = _cacheOptions.Hours, + CacheMinutes = _cacheOptions.Minutes, + CacheSeconds = _cacheOptions.Seconds + }); + + return result; + } + + [HttpDelete] + [Authorize("Organizer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteForm(int formId) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var formDeleted = await _mediator.Send(new DeleteFormCommand { FormId = formId }); + + if (!formDeleted) + { + return BadRequest("The form could not be deleted. Make sure the form exists and it doesn't already have saved answers."); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Form/Controllers/OptionController.cs b/src/api/VoteMonitor.Api.Form/Controllers/OptionController.cs new file mode 100644 index 00000000..9473bb5b --- /dev/null +++ b/src/api/VoteMonitor.Api.Form/Controllers/OptionController.cs @@ -0,0 +1,97 @@ +using AutoMapper; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using VoteMonitor.Api.Core; +using VoteMonitor.Api.Form.Models; +using VoteMonitor.Api.Form.Models.Options; +using VoteMonitor.Api.Form.Queries; + +namespace VoteMonitor.Api.Form.Controllers +{ + [Route("api/v1/option")] + public class OptionController : Controller + { + private readonly IMediator _mediator; + private readonly IMapper _mapper; + + public OptionController(IMediator mediator, IMapper mapper) + { + _mediator = mediator; + _mapper = mapper; + } + + [HttpGet] + [Authorize] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task> GetAll() + { + var options = await _mediator.Send(new FetchAllOptionsCommand()); + var mappedResult = options.Select(dto => _mapper.Map(dto)).ToList(); + + return mappedResult; + } + + [HttpGet("{id}")] + [Authorize] + [ProducesResponseType(typeof(OptionModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + public async Task GetByOptionId([FromRoute] int id) + { + var optionDto = await _mediator.Send(new GetOptionByIdCommand(id)); + var result = _mapper.Map(optionDto); + return result; + } + + [HttpPost("create")] + [Authorize("NgoAdmin")] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreateOptionModel model) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var dto = _mapper.Map(model); + var optionDto = await _mediator.Send(new AddOptionCommand(dto)); + + var result = _mapper.Map(optionDto); + + return Ok(result); + + } + + [HttpPut("update")] + [Authorize("NgoAdmin")] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + public async Task Update([FromBody] OptionModel model) + { + if (!ModelState.IsValid) + { + return this.ResultAsync(HttpStatusCode.BadRequest, ModelState); + } + + var dto = _mapper.Map(model); + var result = await _mediator.Send(new UpdateOptionCommand(dto)); + + + return this.ResultAsync(result < 0 ? HttpStatusCode.NotFound : HttpStatusCode.OK); + } + + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Form/Controllers/QuestionController.cs b/src/api/VoteMonitor.Api.Form/Controllers/QuestionController.cs new file mode 100644 index 00000000..f4ad8b12 --- /dev/null +++ b/src/api/VoteMonitor.Api.Form/Controllers/QuestionController.cs @@ -0,0 +1,39 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using VoteMonitor.Api.Form.Models; + +namespace VoteMonitor.Api.Form.Controllers +{ + /// + /// + /// CRUD operations on Questions + /// + + [Route("api/v1/question")] + public class QuestionController : Controller + { + private IMediator _mediator; + + public QuestionController(IMediator mediator) + { + _mediator = mediator; + } + [HttpGet("all")] + public Task> GetAll() + { + return null; + } + [HttpPost] + public Task NewQuestion() + { + return null; + } + [HttpDelete] + public Task DeleteQuestion(int id) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/api/VoteMonitor.Api.Form/Handlers/OptionQueriesHandler.cs b/src/api/VoteMonitor.Api.Form/Handlers/OptionQueriesHandler.cs new file mode 100644 index 00000000..ede825ef --- /dev/null +++ b/src/api/VoteMonitor.Api.Form/Handlers/OptionQueriesHandler.cs @@ -0,0 +1,83 @@ +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VoteMonitor.Api.Form.Models; +using VoteMonitor.Api.Form.Queries; +using VoteMonitor.Entities; + +namespace VoteMonitor.Api.Form.Handlers +{ + public class OptionQueriesHandler : IRequestHandler>, + IRequestHandler, + IRequestHandler, + IRequestHandler + + { + private readonly VoteMonitorContext _context; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public OptionQueriesHandler(VoteMonitorContext context, IMapper mapper, ILogger logger) + { + _context = context; + _mapper = mapper; + _logger = logger; + } + + public async Task> Handle(FetchAllOptionsCommand request, CancellationToken cancellationToken) + { + return await _context.Options.Select(x => _mapper.Map(x)).ToListAsync(cancellationToken); + } + + public Task Handle(GetOptionByIdCommand request, CancellationToken cancellationToken) + { + var option = _context.Options.FirstOrDefault(c => c.Id == request.OptionId); + + var optionDto = _mapper.Map(option); + + return Task.FromResult(optionDto); + } + + public async Task Handle(AddOptionCommand request, CancellationToken cancellationToken) + { + var optionEntity = _mapper.Map