diff --git a/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs b/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs index b31555a..650edcb 100644 --- a/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs +++ b/src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -13,5 +14,9 @@ namespace EventHub.Organizations Task GetProfileAsync(string name); Task> GetMyOrganizationsAsync(); + + Task IsOrganizationOwnerAsync(Guid organizationId); + + Task UpdateAsync(Guid id, UpdateOrganizationDto input); } } diff --git a/src/EventHub.Application.Contracts/Organizations/UpdateOrganizationDto.cs b/src/EventHub.Application.Contracts/Organizations/UpdateOrganizationDto.cs new file mode 100644 index 0000000..32516a0 --- /dev/null +++ b/src/EventHub.Application.Contracts/Organizations/UpdateOrganizationDto.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventHub.Organizations +{ + public class UpdateOrganizationDto + { + [Required] + [StringLength(OrganizationConsts.MaxDisplayNameLength, MinimumLength = OrganizationConsts.MinDisplayNameLength)] + public string DisplayName { get; set; } + + [Required] + [StringLength(OrganizationConsts.MaxDescriptionNameLength, MinimumLength = OrganizationConsts.MinDescriptionNameLength)] + public string Description { get; set; } + + public string Website { get; set; } + + public string TwitterUsername { get; set; } + + public string GitHubUsername { get; set; } + + public string FacebookUsername { get; set; } + + public string InstagramUsername { get; set; } + + public string MediumUsername { get; set; } + } +} \ No newline at end of file diff --git a/src/EventHub.Application/Organizations/OrganizationAppService.cs b/src/EventHub.Application/Organizations/OrganizationAppService.cs index 2536cb4..f447d95 100644 --- a/src/EventHub.Application/Organizations/OrganizationAppService.cs +++ b/src/EventHub.Application/Organizations/OrganizationAppService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; using Volo.Abp.Users; @@ -67,5 +68,33 @@ namespace EventHub.Organizations ObjectMapper.Map, List>(organizations) ); } + + public async Task IsOrganizationOwnerAsync(Guid organizationId) + { + return CurrentUser.Id.HasValue && await _organizationRepository.AnyAsync(x => x.Id == organizationId && x.OwnerUserId == CurrentUser.Id.Value); + } + + [Authorize] + public async Task UpdateAsync(Guid id, UpdateOrganizationDto input) + { + var organization = await _organizationRepository.GetAsync(id); + + if (organization.OwnerUserId != CurrentUser.GetId()) + { + throw new BusinessException(EventHubErrorCodes.NotAuthorizedToUpdateOrganizationProfile) + .WithData("OrganizationName", organization.DisplayName); + } + + organization.SetDisplayName(input.DisplayName); + organization.SetDescription(input.Description); + organization.Website = input.Website; + organization.TwitterUsername = input.TwitterUsername; + organization.GitHubUsername = input.GitHubUsername; + organization.InstagramUsername = input.InstagramUsername; + organization.FacebookUsername = input.FacebookUsername; + organization.MediumUsername = input.MediumUsername; + + await _organizationRepository.UpdateAsync(organization); + } } } diff --git a/src/EventHub.Domain.Shared/EventHubErrorCodes.cs b/src/EventHub.Domain.Shared/EventHubErrorCodes.cs index 7923307..3d1603a 100644 --- a/src/EventHub.Domain.Shared/EventHubErrorCodes.cs +++ b/src/EventHub.Domain.Shared/EventHubErrorCodes.cs @@ -6,5 +6,6 @@ public const string NotAuthorizedToCreateEventInThisOrganization = "EventHub:NotAuthorizedToCreateEventInThisOrganization"; public const string EventEndTimeCantBeEarlierThanStartTime = "EventHub:EventEndTimeCantBeEarlierThanStartTime"; public const string CantRegisterOrUnregisterForAPastEvent = "EventHub:CantRegisterOrUnregisterForAPastEvent"; + public const string NotAuthorizedToUpdateOrganizationProfile = "EventHub:NotAuthorizedToUpdateOrganizationProfile"; } } diff --git a/src/EventHub.Domain.Shared/Localization/EventHub/en.json b/src/EventHub.Domain.Shared/Localization/EventHub/en.json index 18fb75d..25f2ca7 100644 --- a/src/EventHub.Domain.Shared/Localization/EventHub/en.json +++ b/src/EventHub.Domain.Shared/Localization/EventHub/en.json @@ -42,6 +42,11 @@ "OrganizationJoinSuccessMessage": "See you at the events.", "OrganizationMembershipLeaveMessage": "You left your organization membership.", "LeaveOrganization": "Leave this organization", - "JoinOrganization": "Join the organization" + "JoinOrganization": "Join the organization", + "Edit": "Edit", + "EditOrganization": "Edit Organization", + "PersonalWebsite": "Personal Website", + "SocialMedia": "Social Media", + "EventHub:NotAuthorizedToUpdateOrganizationProfile": "You are not authorized to update the \"{OrganizationName}\" organization." } } diff --git a/src/EventHub.Web/EventHubWebAutoMapperProfile.cs b/src/EventHub.Web/EventHubWebAutoMapperProfile.cs index a82e04f..d0e9a0b 100644 --- a/src/EventHub.Web/EventHubWebAutoMapperProfile.cs +++ b/src/EventHub.Web/EventHubWebAutoMapperProfile.cs @@ -1,6 +1,7 @@ using AutoMapper; using EventHub.Events; using EventHub.Organizations; +using EventHub.Web.Pages.Organizations; namespace EventHub.Web { @@ -8,8 +9,10 @@ namespace EventHub.Web { public EventHubWebAutoMapperProfile() { - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } } diff --git a/src/EventHub.Web/Pages/Organizations/Edit.cshtml b/src/EventHub.Web/Pages/Organizations/Edit.cshtml new file mode 100644 index 0000000..5eb4651 --- /dev/null +++ b/src/EventHub.Web/Pages/Organizations/Edit.cshtml @@ -0,0 +1,10 @@ +@page "/organization/edit/{name}" +@inject IHtmlLocalizer L +@using EventHub.Localization +@using Microsoft.AspNetCore.Mvc.Localization +@model EventHub.Web.Pages.Organizations.EditPageModel + +
+

@L["EditOrganization"]

+ +
\ 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 new file mode 100644 index 0000000..a91a454 --- /dev/null +++ b/src/EventHub.Web/Pages/Organizations/Edit.cshtml.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using EventHub.Organizations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace EventHub.Web.Pages.Organizations +{ + [Authorize] + public class EditPageModel : EventHubPageModel + { + [BindProperty(SupportsGet = true)] + public string Name { get; set; } + + [BindProperty] + public EditOrganizationViewModel Organization { get; set; } + + private readonly IOrganizationAppService _organizationAppService; + + public EditPageModel(IOrganizationAppService organizationAppService) + { + _organizationAppService = organizationAppService; + } + + public async Task OnGetAsync() + { + await GetOrganizationAsync(); + } + + public async Task OnPostAsync() + { + try + { + ValidateModel(); + + var input = ObjectMapper.Map(Organization); + await _organizationAppService.UpdateAsync(Organization.Id, input); + + return RedirectToPage("./Profile", new { name = Name }); + } + catch (Exception exception) + { + ShowAlert(exception); + return Page(); + } + } + + private async Task GetOrganizationAsync() + { + var organizationProfileDto = await _organizationAppService.GetProfileAsync(Name); + Organization = ObjectMapper.Map(organizationProfileDto); + } + } + + public class EditOrganizationViewModel + { + [HiddenInput] + public Guid Id { get; set; } + + [Required] + [StringLength(OrganizationConsts.MaxDisplayNameLength, MinimumLength = OrganizationConsts.MinDisplayNameLength)] + public string DisplayName { get; set; } + + [Required] + [StringLength(OrganizationConsts.MaxDescriptionNameLength, MinimumLength = OrganizationConsts.MinDescriptionNameLength)] + public string Description { get; set; } + + public string Website { get; set; } + + public string TwitterUsername { get; set; } + + public string GitHubUsername { get; set; } + + public string FacebookUsername { get; set; } + + public string InstagramUsername { get; set; } + + public string MediumUsername { get; set; } + } +} \ No newline at end of file diff --git a/src/EventHub.Web/Pages/Organizations/Profile.cshtml b/src/EventHub.Web/Pages/Organizations/Profile.cshtml index 817437e..5a001d4 100644 --- a/src/EventHub.Web/Pages/Organizations/Profile.cshtml +++ b/src/EventHub.Web/Pages/Organizations/Profile.cshtml @@ -15,10 +15,83 @@ @Model.Organization.DisplayName -

@Model.Organization.DisplayName

+ @if (Model.IsOrganizationOwner) + { +
+
+

@Model.Organization.DisplayName

+
+ +
+ } + else + { +

@Model.Organization.DisplayName

+ }
@Model.Organization.Description + + @if (!Model.Organization.Website.IsNullOrWhiteSpace() || !Model.Organization.TwitterUsername.IsNullOrWhiteSpace() || + !Model.Organization.GitHubUsername.IsNullOrWhiteSpace() || !Model.Organization.InstagramUsername.IsNullOrWhiteSpace() || + !Model.Organization.FacebookUsername.IsNullOrWhiteSpace() || !Model.Organization.MediumUsername.IsNullOrWhiteSpace()) + { +
+ @if (!Model.Organization.Website.IsNullOrWhiteSpace()) + { +
+ @L["PersonalWebsite"] +
+ @if (Model.Organization.Website != null) + { + + @Model.Organization.Website + + } +
+
+ } + @if (!Model.Organization.TwitterUsername.IsNullOrWhiteSpace() || !Model.Organization.GitHubUsername.IsNullOrWhiteSpace() || + !Model.Organization.InstagramUsername.IsNullOrWhiteSpace() || !Model.Organization.FacebookUsername.IsNullOrWhiteSpace() || + !Model.Organization.MediumUsername.IsNullOrWhiteSpace()) + { +
+ @L["SocialMedia"]
+ @if (!Model.Organization.TwitterUsername.IsNullOrWhiteSpace()) + { + + + + } + @if (!Model.Organization.GitHubUsername.IsNullOrWhiteSpace()) + { + + + + } + @if (!Model.Organization.InstagramUsername.IsNullOrWhiteSpace()) + { + + + + } + @if (!Model.Organization.FacebookUsername.IsNullOrWhiteSpace()) + { + + + + } + @if (!Model.Organization.MediumUsername.IsNullOrWhiteSpace()) + { + + + + } +
+ } + }
diff --git a/src/EventHub.Web/Pages/Organizations/Profile.cshtml.cs b/src/EventHub.Web/Pages/Organizations/Profile.cshtml.cs index 73572dc..d7578a3 100644 --- a/src/EventHub.Web/Pages/Organizations/Profile.cshtml.cs +++ b/src/EventHub.Web/Pages/Organizations/Profile.cshtml.cs @@ -18,6 +18,8 @@ namespace EventHub.Web.Pages.Organizations public IReadOnlyList PastEvents { get; private set; } public long PastEventTotalCount { get; private set; } + public bool IsOrganizationOwner { get; private set; } + private readonly IEventAppService _eventAppService; private readonly IOrganizationAppService _organizationAppService; @@ -34,6 +36,8 @@ namespace EventHub.Web.Pages.Organizations await GetProfileAsync(); await GetUpcomingEventsAsync(); await GetPastEventsAsync(); + + IsOrganizationOwner = await _organizationAppService.IsOrganizationOwnerAsync(Organization.Id); } private async Task GetProfileAsync() diff --git a/test/EventHub.Application.Tests/Organizations/OrganizationAppServiceTests.cs b/test/EventHub.Application.Tests/Organizations/OrganizationAppServiceTests.cs index 3e2f5f4..607ded8 100644 --- a/test/EventHub.Application.Tests/Organizations/OrganizationAppServiceTests.cs +++ b/test/EventHub.Application.Tests/Organizations/OrganizationAppServiceTests.cs @@ -1,7 +1,12 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; using Shouldly; using Volo.Abp; using Volo.Abp.Application.Dtos; +using Volo.Abp.Authorization; +using Volo.Abp.Users; using Xunit; namespace EventHub.Organizations @@ -10,16 +15,25 @@ namespace EventHub.Organizations { private readonly IOrganizationAppService _organizationAppService; private readonly EventHubTestData _testData; - + private ICurrentUser _currentUser; + public OrganizationAppServiceTests() { _organizationAppService = GetRequiredService(); _testData = GetRequiredService(); } + + protected override void AfterAddApplication(IServiceCollection services) + { + _currentUser = Substitute.For(); + services.AddSingleton(_currentUser); + } [Fact] public async Task Should_Create_A_Valid_Organization() { + Login(_testData.UserAdminId); + await _organizationAppService.CreateAsync( new CreateOrganizationDto { @@ -38,6 +52,8 @@ namespace EventHub.Organizations [Fact] public async Task Should_Not_Create_Organization_With_Existing_Name() { + Login(_testData.UserAdminId); + var exception = await Assert.ThrowsAsync(() => _organizationAppService.CreateAsync( new CreateOrganizationDto @@ -68,5 +84,51 @@ namespace EventHub.Organizations result.Id.ShouldBe(_testData.OrganizationVolosoftId); result.Name.ShouldBe(_testData.OrganizationVolosoftName); } + + [Fact] + public async Task Should_Update_Organization_If_User_Is_Owner() + { + Login(_testData.UserAdminId); + + await _organizationAppService.UpdateAsync( + _testData.OrganizationVolosoftId, + new UpdateOrganizationDto + { + DisplayName = "VOLOSOFT", + Description = "Test description text that is valid and long enough for updating!", + Website = "https://volosoft.com/", + MediumUsername = "volosoft" + }); + + var organization = await GetOrganizationOrNullAsync(_testData.OrganizationVolosoftName); + organization.ShouldNotBeNull(); + organization.DisplayName.ShouldBe("VOLOSOFT"); + organization.Description.ShouldBe("Test description text that is valid and long enough for updating!"); + } + + [Fact] + public async Task Should_Not_Update_Organization_If_User_Not_Owner() + { + Login(_testData.UserAdminId); + + var exception = await Assert.ThrowsAsync(() => + _organizationAppService.UpdateAsync( + _testData.OrganizationDotnetEuropeId, + new UpdateOrganizationDto + { + DisplayName = "Dotnet Europe (DE)", + Description = "Test description text that is valid and long enough for updating!", + Website = "https://dotnet-europe.com/" + }) + ); + + exception.ShouldNotBeNull(); + } + + private void Login(Guid userId) + { + _currentUser.Id.Returns(userId); + _currentUser.IsAuthenticated.Returns(true); + } } }