From 5b619f370c80c19dfc0a648ba0fc365b1e6f9775 Mon Sep 17 00:00:00 2001 From: EngincanV Date: Mon, 8 Feb 2021 15:22:17 +0300 Subject: [PATCH] Add organization profile section into organization edit page --- .../Organizations/IOrganizationAppService.cs | 2 + .../Organizations/OrganizationInListDto.cs | 2 + .../Organizations/OrganizationProfileDto.cs | 2 + .../Organizations/OrganizationAppService.cs | 56 ++++++++- .../Organizations/OrganizationContst.cs | 2 + .../OrganizationProfilePictureContainer.cs | 10 ++ .../Controllers/OrganizationController.cs | 39 ++++++ .../Helpers/AllowedExtensionsAttribute.cs | 38 ++++++ .../Helpers/MaxFileSizeAttribute.cs | 36 ++++++ .../Pages/Organizations/Edit.cshtml | 53 +++++++- .../Pages/Organizations/Edit.cshtml.cs | 14 +++ src/EventHub.Web/Pages/Organizations/Edit.js | 113 ++++++++++++++++++ .../Pages/Organizations/Index.cshtml | 9 +- .../Pages/Organizations/Profile.cshtml | 13 +- 14 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 src/EventHub.Domain/Organizations/OrganizationProfilePictureContainer.cs create mode 100644 src/EventHub.Web/Controllers/OrganizationController.cs create mode 100644 src/EventHub.Web/Helpers/AllowedExtensionsAttribute.cs create mode 100644 src/EventHub.Web/Helpers/MaxFileSizeAttribute.cs create mode 100644 src/EventHub.Web/Pages/Organizations/Edit.js diff --git a/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs b/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs index 650edcb..7ce5686 100644 --- a/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs +++ b/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs @@ -18,5 +18,7 @@ namespace EventHub.Organizations Task IsOrganizationOwnerAsync(Guid organizationId); Task UpdateAsync(Guid id, UpdateOrganizationDto input); + + Task SaveProfilePictureAsync(Guid id, byte[] bytes); } } diff --git a/src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs b/src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs index 16f6fd3..f064065 100644 --- a/src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs +++ b/src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs @@ -10,5 +10,7 @@ namespace EventHub.Organizations public string DisplayName { get; set; } public string Description { get; set; } + + public byte[] ProfilePictureContent { get; set; } } } diff --git a/src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs b/src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs index d65412a..1b92778 100644 --- a/src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs +++ b/src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs @@ -22,5 +22,7 @@ namespace EventHub.Organizations public string InstagramUsername { get; set; } public string MediumUsername { get; set; } + + public byte[] ProfilePictureContent { get; set; } } } diff --git a/src/EventHub.Application/Organizations/OrganizationAppService.cs b/src/EventHub.Application/Organizations/OrganizationAppService.cs index f447d95..eafbc0f 100644 --- a/src/EventHub.Application/Organizations/OrganizationAppService.cs +++ b/src/EventHub.Application/Organizations/OrganizationAppService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Volo.Abp; using Volo.Abp.Application.Dtos; +using Volo.Abp.BlobStoring; using Volo.Abp.Domain.Repositories; using Volo.Abp.Users; @@ -14,13 +15,17 @@ namespace EventHub.Organizations { private readonly IRepository _organizationRepository; private readonly OrganizationManager _organizationManager; + private readonly IBlobContainer _organizationBlobContainer; public OrganizationAppService( IRepository organizationRepository, - OrganizationManager organizationManager) + OrganizationManager organizationManager, + IBlobContainer organizationBlobContainer + ) { _organizationRepository = organizationRepository; _organizationManager = organizationManager; + _organizationBlobContainer = organizationBlobContainer; } [Authorize] @@ -45,16 +50,27 @@ namespace EventHub.Organizations nameof(Organization.DisplayName) ); + var organizationDto = ObjectMapper.Map, List>(organizations); + + foreach (var organization in organizationDto) + { + organization.ProfilePictureContent = await GetProfilePictureAsync(organization.Id); + } + return new PagedResultDto( organizationCount, - ObjectMapper.Map, List>(organizations) + organizationDto ); } public async Task GetProfileAsync(string name) { var organization = await _organizationRepository.GetAsync(o => o.Name == name); - return ObjectMapper.Map(organization); + var organizationProfileDto = ObjectMapper.Map(organization); + + organizationProfileDto.ProfilePictureContent = await GetProfilePictureAsync(organizationProfileDto.Id); + + return organizationProfileDto; } [Authorize] @@ -64,9 +80,14 @@ namespace EventHub.Organizations var query = (await _organizationRepository.GetQueryableAsync()).Where(o => o.OwnerUserId == currentUserId); var organizations = await AsyncExecuter.ToListAsync(query); - return new ListResultDto( - ObjectMapper.Map, List>(organizations) - ); + var organizationDto = ObjectMapper.Map, List>(organizations); + + foreach (var organization in organizationDto) + { + organization.ProfilePictureContent = await GetProfilePictureAsync(organization.Id); + } + + return new ListResultDto(organizationDto); } public async Task IsOrganizationOwnerAsync(Guid organizationId) @@ -96,5 +117,28 @@ namespace EventHub.Organizations await _organizationRepository.UpdateAsync(organization); } + + [Authorize] + public async Task SaveProfilePictureAsync(Guid id, byte[] bytes) + { + var organization = await _organizationRepository.GetAsync(x => x.Id == id); + + if (organization.OwnerUserId != CurrentUser.GetId()) + { + throw new BusinessException(EventHubErrorCodes.NotAuthorizedToUpdateOrganizationProfile) + .WithData("Name", organization.DisplayName); + } + + var blobName = id.ToString(); + + await _organizationBlobContainer.SaveAsync(blobName, bytes, overrideExisting: true); + } + + private async Task GetProfilePictureAsync(Guid id) + { + var blobName = id.ToString(); + + return await _organizationBlobContainer.GetAllBytesOrNullAsync(blobName); + } } } diff --git a/src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs b/src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs index a8fca4a..2348d71 100644 --- a/src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs +++ b/src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs @@ -10,5 +10,7 @@ public const int MinDescriptionNameLength = 50; public const int MaxDescriptionNameLength = 1000; + + public const int MaxProfilePictureFileSize = 1048576; } } diff --git a/src/EventHub.Domain/Organizations/OrganizationProfilePictureContainer.cs b/src/EventHub.Domain/Organizations/OrganizationProfilePictureContainer.cs new file mode 100644 index 0000000..133dc48 --- /dev/null +++ b/src/EventHub.Domain/Organizations/OrganizationProfilePictureContainer.cs @@ -0,0 +1,10 @@ +using Volo.Abp.BlobStoring; + +namespace EventHub.Organizations +{ + [BlobContainerName("organization-profile-pictures")] + public class OrganizationProfilePictureContainer + { + + } +} \ No newline at end of file diff --git a/src/EventHub.Web/Controllers/OrganizationController.cs b/src/EventHub.Web/Controllers/OrganizationController.cs new file mode 100644 index 0000000..4b5ce93 --- /dev/null +++ b/src/EventHub.Web/Controllers/OrganizationController.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Threading.Tasks; +using EventHub.Organizations; +using EventHub.Web.Pages.Organizations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace EventHub.Web.Controllers +{ + [Route("api/organization")] + public class OrganizationController : AbpController + { + private readonly IOrganizationAppService _organizationAppService; + + public OrganizationController(IOrganizationAppService organizationAppService) + { + _organizationAppService = organizationAppService; + } + + [HttpPost] + [Authorize] + public async Task SaveProfilePicture([FromForm] OrganizationProfilePictureInput input) + { + byte[] profilePictureContent = new byte[] {}; + + if (input.ProfilePictureFile != null && input.ProfilePictureFile.Length > 0) + { + using (var memoryStream = new MemoryStream()) + { + await input.ProfilePictureFile.CopyToAsync(memoryStream); + profilePictureContent = memoryStream.ToArray(); + } + } + + await _organizationAppService.SaveProfilePictureAsync(input.OrganizationId, profilePictureContent); + } + } +} \ No newline at end of file diff --git a/src/EventHub.Web/Helpers/AllowedExtensionsAttribute.cs b/src/EventHub.Web/Helpers/AllowedExtensionsAttribute.cs new file mode 100644 index 0000000..cab348b --- /dev/null +++ b/src/EventHub.Web/Helpers/AllowedExtensionsAttribute.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace EventHub.Web.Helpers +{ + public class AllowedExtensionsAttribute : ValidationAttribute + { + private readonly string[] _allowedExtensions; + + public AllowedExtensionsAttribute(string[] allowedExtensions) + { + _allowedExtensions = allowedExtensions; + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value == null) + { + return ValidationResult.Success; + } + + var file = value as IFormFile; + var extension = Path.GetExtension(file.FileName); + + if (file.Length > 0 && file != null) + { + if (!_allowedExtensions.Contains(extension.ToLower())) + { + return new ValidationResult("This file extension is not allowed. Allowed extensions: png, jpg, jpeg"); + } + } + + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/src/EventHub.Web/Helpers/MaxFileSizeAttribute.cs b/src/EventHub.Web/Helpers/MaxFileSizeAttribute.cs new file mode 100644 index 0000000..8c115f0 --- /dev/null +++ b/src/EventHub.Web/Helpers/MaxFileSizeAttribute.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; + +namespace EventHub.Web.Helpers +{ + public class MaxFileSizeAttribute : ValidationAttribute + { + private readonly int _maxFileSize; + + public MaxFileSizeAttribute(int maxFileSize) + { + _maxFileSize = maxFileSize; + } + + protected override ValidationResult IsValid( + object value, ValidationContext validationContext) + { + if (value == null) + { + return ValidationResult.Success; + } + + var file = value as IFormFile; + + if (file.Length > 0) + { + if (file.Length > _maxFileSize) + { + return new ValidationResult("Maximum file size: 1MB"); + } + } + + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/src/EventHub.Web/Pages/Organizations/Edit.cshtml b/src/EventHub.Web/Pages/Organizations/Edit.cshtml index 5eb4651..7d22d5f 100644 --- a/src/EventHub.Web/Pages/Organizations/Edit.cshtml +++ b/src/EventHub.Web/Pages/Organizations/Edit.cshtml @@ -4,7 +4,58 @@ @using Microsoft.AspNetCore.Mvc.Localization @model EventHub.Web.Pages.Organizations.EditPageModel +@section scripts { + + + +} +

@L["EditOrganization"]

- + + +
+
+ +
+
+
+ + + +
+ +
+ + + Profile picture info +
+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/src/EventHub.Web/Pages/Organizations/Edit.cshtml.cs b/src/EventHub.Web/Pages/Organizations/Edit.cshtml.cs index a91a454..fb7d634 100644 --- a/src/EventHub.Web/Pages/Organizations/Edit.cshtml.cs +++ b/src/EventHub.Web/Pages/Organizations/Edit.cshtml.cs @@ -2,7 +2,9 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using EventHub.Organizations; +using EventHub.Web.Helpers; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace EventHub.Web.Pages.Organizations @@ -78,4 +80,16 @@ namespace EventHub.Web.Pages.Organizations public string MediumUsername { get; set; } } + + public class OrganizationProfilePictureInput + { + [Required] + public Guid OrganizationId { get; set; } + + [Required] + [DataType(DataType.Upload)] + [MaxFileSize(OrganizationConsts.MaxProfilePictureFileSize)] + [AllowedExtensions(new string[] { ".jpg", ".png", ".jpeg" })] + public IFormFile ProfilePictureFile { get; set; } + } } \ No newline at end of file diff --git a/src/EventHub.Web/Pages/Organizations/Edit.js b/src/EventHub.Web/Pages/Organizations/Edit.js new file mode 100644 index 0000000..684f78d --- /dev/null +++ b/src/EventHub.Web/Pages/Organizations/Edit.js @@ -0,0 +1,113 @@ +$(function () { + var l = abp.localization.getResource('EventHub'); + + var fileInput = document.getElementById("organization_id"); + var file; + + fileInput.addEventListener('change', function () { + var showModal = true; + + file = fileInput.files[0]; + + if(file == undefined) { + $("#choose-cover-image").html("Choose a cover image..."); + return; + } + else { + $("#choose-cover-image").html(file.name); + } + + var permittedExtensions = ["jpg", "jpeg", "png"] + var fileExtension = $(this).val().split('.').pop(); + + if (permittedExtensions.indexOf(fileExtension) === -1) { + showModal = false; + abp.message.error('This Extension Is Not Allowed') + .then(() => { + $("#choose-cover-image").html("Choose a cover image..."); + file = null; + }); + } + //1mb + else if(file.size > 1024 * 1024) { + showModal = false; + abp.message.error('The File Is Too Large') + .then(() => { + $("#choose-cover-image").html("Choose a cover image..."); + file = null; + }); + } + + var img = new Image(); + img.onload = function () { + var imageError = true; + var sizes = { + width: this.width, + height: this.height + }; + + URL.revokeObjectURL(this.src); + + if(showModal && imageError) { + readURL(file); + $("#picturePreviewModal").modal('show'); + } + } + + var objectURL = URL.createObjectURL(file); + img.src = objectURL; + }); + + function readURL(input) { + if (input) { + var reader = new FileReader(); + + reader.onload = function (e) { + $('#img-upload').attr('src', e.target.result); + } + + reader.readAsDataURL(input); + } + } + + $("form#EditOrganizationProfileForm").submit(function (e) { + e.preventDefault(); + + var id = document.getElementById("my-id").value; + + var formData = new FormData(); + formData.append("OrganizationId", id); + formData.append("ProfilePictureFile", file); + + $.ajax({ + xhr: function() { + var xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + percentComplete = parseInt(percentComplete * 100); + if(percentComplete !== 100){ + $('#btnSubmit').prop( "disabled", true ); + } + } + }, false); + + return xhr; + }, + url: `/api/organization`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: function(data){ + abp.message + .success("Organization Profile Picture successfully added.") + .then(data => window.location.href = "/"); + }, + error: function (data) { + abp.message.error(data.responseJSON.error.message); + } + }); + + }); +}); \ No newline at end of file diff --git a/src/EventHub.Web/Pages/Organizations/Index.cshtml b/src/EventHub.Web/Pages/Organizations/Index.cshtml index e8a58d3..3fddf3b 100644 --- a/src/EventHub.Web/Pages/Organizations/Index.cshtml +++ b/src/EventHub.Web/Pages/Organizations/Index.cshtml @@ -9,7 +9,14 @@ { - @organization.DisplayName + @if (organization.ProfilePictureContent == null) + { + @organization.DisplayName + } + else + { + @organization.DisplayName + } @organization.DisplayName diff --git a/src/EventHub.Web/Pages/Organizations/Profile.cshtml b/src/EventHub.Web/Pages/Organizations/Profile.cshtml index 5a001d4..adfeb70 100644 --- a/src/EventHub.Web/Pages/Organizations/Profile.cshtml +++ b/src/EventHub.Web/Pages/Organizations/Profile.cshtml @@ -12,7 +12,14 @@ - @Model.Organization.DisplayName + @if (Model.Organization.ProfilePictureContent == null) + { + @Model.Organization.DisplayName + } + else + { + @Model.Organization.DisplayName + } @if (Model.IsOrganizationOwner) @@ -53,8 +60,8 @@ } - @if (!Model.Organization.TwitterUsername.IsNullOrWhiteSpace() || !Model.Organization.GitHubUsername.IsNullOrWhiteSpace() || - !Model.Organization.InstagramUsername.IsNullOrWhiteSpace() || !Model.Organization.FacebookUsername.IsNullOrWhiteSpace() || + @if (!Model.Organization.TwitterUsername.IsNullOrWhiteSpace() || !Model.Organization.GitHubUsername.IsNullOrWhiteSpace() || + !Model.Organization.InstagramUsername.IsNullOrWhiteSpace() || !Model.Organization.FacebookUsername.IsNullOrWhiteSpace() || !Model.Organization.MediumUsername.IsNullOrWhiteSpace()) {