Browse Source

Merge branch 'dev' into maliming/claims

pull/5315/head
maliming 6 years ago
parent
commit
a2f5abe41d
  1. 39
      .github/workflows/angular.yml
  2. 2
      .github/workflows/build-and-test.yml
  3. 1
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  4. 1
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  5. 18
      docs/en/Authorization.md
  6. 18
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md
  7. 447
      docs/en/Features.md
  8. 16
      docs/en/Index.md
  9. 2
      docs/en/Modules/Feature-Management.md
  10. 2
      docs/en/Modules/Permission-Management.md
  11. 3
      docs/en/Multi-Tenancy.md
  12. 77
      docs/en/UI/Angular/Component-Replacement.md
  13. 3
      docs/en/UI/Angular/Features.md
  14. 12
      docs/en/UI/Angular/Permission-Management-Component-Replacement.md
  15. 4
      docs/en/docs-nav.json
  16. BIN
      docs/en/images/features-action.png
  17. BIN
      docs/en/images/features-modal.png
  18. 86
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs
  19. 7
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs
  20. 9
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs
  21. 4
      framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs
  22. 8
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpRemoteCallException.cs
  23. 6
      framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorInfo.cs
  24. 46
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/luxon/abp.luxon.js
  25. 4
      framework/test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj
  26. 22
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs
  27. 2
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs
  28. 10
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs
  29. 11
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs
  30. 10
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Localization/HttpClientTestResource.cs
  31. 6
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Localization/en.json
  32. 2
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
  33. 6
      global.json
  34. 4
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json
  35. 4
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json
  36. 3
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs
  37. 31
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs
  38. 59
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs
  39. 17
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml
  40. 31
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js
  41. 56
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs
  42. 39
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml
  43. 19
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
  44. 73
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml
  45. 72
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml.cs
  46. 47
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.js
  47. 1
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  48. 48
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/AccountProfileManagementPageContributor.cs
  49. 9
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/IProfileManagementPageContributor.cs
  50. 19
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageCreationContext.cs
  51. 39
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageGroup.cs
  52. 14
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageOptions.cs
  53. 7
      modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs
  54. 3
      modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj
  55. 4
      modules/cms-kit/host/Volo.CmsKit.IdentityServer/CmsKitIdentityServerModule.cs
  56. 1
      modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj
  57. 1
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs
  58. 1235
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.Designer.cs
  59. 39
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.cs
  60. 2429
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs
  61. 9
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml
  62. 37
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/css/star-rating-svg.css
  63. 1
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/js/jquery.star-rating-svg.min.js
  64. 3
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs
  65. 18
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/RatingsFeature.cs
  66. 6
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json
  67. 6
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json
  68. 15
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Ratings/RatingConsts.cs
  69. 25
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/IRatingRepository.cs
  70. 57
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/Rating.cs
  71. 9
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/RatingWithStarCountQueryResultItem.cs
  72. 17
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs
  73. 2
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs
  74. 57
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Ratings/EfCoreRatingRepository.cs
  75. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContext.cs
  76. 6
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContextExtensions.cs
  77. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbModule.cs
  78. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/ICmsKitMongoDbContext.cs
  79. 58
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Ratings/MongoRatingRepository.cs
  80. 30
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo.CmsKit.Public.Application.Contracts.csproj
  81. 13
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/CreateUpdateRatingInput.cs
  82. 19
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/IRatingPublicAppService.cs
  83. 19
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingDto.cs
  84. 11
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingWithStarCountDto.cs
  85. 32
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj
  86. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs
  87. 104
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Ratings/RatingPublicAppService.cs
  88. 32
      modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo.CmsKit.Public.HttpApi.csproj
  89. 51
      modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Ratings/RatingPublicController.cs
  90. 6
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs
  91. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml
  92. 46
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/Default.cshtml
  93. 16
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingScriptBundleContributor.cs
  94. 16
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingStyleBundleContributor.cs
  95. 75
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingViewComponent.cs
  96. 44
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.css
  97. 87
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.js
  98. 137
      modules/cms-kit/test/Volo.CmsKit.Application.Tests/Ratings/RatingPublicAppService_Tests.cs
  99. 9
      modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/Ratings/RatingRepository_Tests.cs
  100. 11
      modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/Ratings/RatingRepository_Tests.cs

39
.github/workflows/angular.yml

@ -9,8 +9,41 @@ jobs:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/cache@v2
with: with:
node-version: '12.x' path: 'npm/ng-packs/node_modules'
- run: yarn && yarn ci key: ${{ runner.os }}-${{ hashFiles('npm/ng-packs/yarn.lock') }}
- uses: actions/cache@v2
with:
path: 'templates/app/angular/node_modules'
key: ${{ runner.os }}-${{ hashFiles('templates/app/angular/yarn.lock') }}
- name: Install packages
run: yarn install
working-directory: npm/ng-packs
- name: Run lint
run: yarn ng lint
working-directory: npm/ng-packs
- name: Run prepare workspace
run: yarn prepare:workspace
working-directory: npm/ng-packs
- name: Run test
run: yarn ci:test
working-directory: npm/ng-packs
- name: Build dev-app
run: yarn build --prod
working-directory: npm/ng-packs working-directory: npm/ng-packs
- name: Install packages of app template
run: yarn install
working-directory: templates/app/angular
- name: Build app template
run: yarn build --prod
working-directory: templates/app/angular

2
.github/workflows/build-and-test.yml

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-dotnet@master - uses: actions/setup-dotnet@master
with: with:
dotnet-version: 3.1.100 dotnet-version: 3.1.102
- name: Build All - name: Build All
run: .\build-all.ps1 run: .\build-all.ps1

1
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -26,6 +26,7 @@
"ContributionGuide": "Contribution Guide", "ContributionGuide": "Contribution Guide",
"Blog": "Blog", "Blog": "Blog",
"Commercial": "Commercial", "Commercial": "Commercial",
"MyAccount": "My account",
"SeeDocuments": "See Documents" "SeeDocuments": "See Documents"
} }
} }

1
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -78,7 +78,6 @@
"Biography": "Biography", "Biography": "Biography",
"HasNoPublishedArticlesYet": "has no published articles yet", "HasNoPublishedArticlesYet": "has no published articles yet",
"Author": "Author", "Author": "Author",
"MyAccount": "My account",
"LatestGithubAnnouncements": "Latest Github Announcements", "LatestGithubAnnouncements": "Latest Github Announcements",
"SeeAllAnnouncements": "See All Announcements", "SeeAllAnnouncements": "See All Announcements",
"LatestBlogPost": "Latest Blog Post", "LatestBlogPost": "Latest Blog Post",

18
docs/en/Authorization.md

@ -80,6 +80,8 @@ namespace Acme.BookStore.Permissions
> ABP automatically discovers this class. No additional configuration required! > ABP automatically discovers this class. No additional configuration required!
> You typically define this class inside the `Application.Contracts` project of your [application](Startup-Templates/Application.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with.
In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group. In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group.
When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role: When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role:
@ -276,14 +278,24 @@ public async Task CreateAsync(CreateAuthorDto input)
> Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check. > Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check.
### Check a Permission in JavaScript ## Check a Permission in JavaScript
You may need to check a policy/permission on the client side.
### MVC UI
You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example: For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API.
**Example: Check if a given permission has been granted for the current user**
```js ```js
abp.auth.isGranted('MyPermissionName'); abp.auth.isGranted('MyPermissionName');
``` ```
### Angular UI
See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI.
## Permission Management ## Permission Management
Permission management is normally done by an admin user using the permission management modal: Permission management is normally done by an admin user using the permission management modal:
@ -374,7 +386,7 @@ Configure<AbpPermissionOptions>(options =>
### Permission Store ### Permission Store
`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. Permission management module implements it. See the [permission management module documentation](Modules/Permission-Management.md) for more information `IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](Modules/Permission-Management.md) for more information
### AlwaysAllowAuthorizationService ### AlwaysAllowAuthorizationService

18
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md

@ -24,7 +24,7 @@ Then add a string property called `Title`, as an example property.
ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property. ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property.
To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`. To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`.
![create-appuserdto](create-appuserdto.jpg) ![create-appuserdto](create-appuserdto.jpg)
@ -40,13 +40,13 @@ CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
### Define the Navigation Property ### Define the Navigation Property
Get back to ABP Suite, open the **Navigation Properties** tab of the ABP Suite, click the **Add Navigation Property** button. Browse `AppUser.cs` in `*.Domain\Users` folder. Then choose the `Name` item as display property. Browse `AppUserDto.cs` in `*.Contracts\Users` folder. Choose `Users` from Collection Names dropdown. Get back to ABP Suite, open the **Navigation Properties** tab of the ABP Suite, click the **Add Navigation Property** button. Browse `AppUser.cs` in `*.Domain\Users` folder. Then choose the `Name` item as display property. Browse `AppUserDto.cs` in `*.Contracts\Users` folder. Choose `Users` from Collection Names dropdown.
![add-user-navigation](add-user-navigation.jpg) ![add-user-navigation](add-user-navigation.jpg)
### Generate the Code! ### Generate the Code!
That's it! Click **Save and generate** button to create your page. You'll see the following page if everything goes well. That's it! Click **Save and generate** button to create your page. You'll see the following page if everything goes well.
![final-page](final-page.jpg) ![final-page](final-page.jpg)
@ -54,7 +54,7 @@ This is the new page that has been created by the ABP Suite. It can perform the
**Picking Users from Look Up Table** **Picking Users from Look Up Table**
We used dropdown element to select a user from the user list. If you have a lot of users, then it's good to pick a user from a look up table. A look up table is a modal window that lets you filter data and pick one. To do this, get back to Suite and click **Edit** button of user navigation which is set as `AppUserId` name. Choose "Modal" from the "UI Pick Type" field. Then click **Save and generate** button to recreate your page. We used dropdown element to select a user from the user list. If you have a lot of users, then it's good to pick a user from a look up table. A look up table is a modal window that lets you filter data and pick one. To do this, get back to Suite and click **Edit** button of user navigation which is set as `AppUserId` name. Choose "Modal" from the "UI Pick Type" field. Then click **Save and generate** button to recreate your page.
![ui-pick-type-modal](ui-pick-type-modal.jpg) ![ui-pick-type-modal](ui-pick-type-modal.jpg)
@ -64,18 +64,18 @@ After successful code generation, you'll see the the user can be picked from use
## About the ABP Commercial RC ## About the ABP Commercial RC
This example has been implemented with **ABP Commercial 3.1.0-rc.3**. This is a RC version. If you want to install the CLI and Suite RC version follow the next steps: This example has been implemented with **ABP Commercial 3.1.0**. If you have not installed the ABP CLI and ABP Suite, follow the next steps:
1- Uninstall the current version of the CLI and install the specific RC version: 1- Uninstall the current version of the CLI and install:
```bash ```bash
dotnet tool uninstall --global Volo.Abp.Cli && dotnet tool install --global Volo.Abp.Cli --version 3.1.0-rc.3 dotnet tool install --global Volo.Abp.Cli --version 3.1.0
``` ```
2- Uninstall the current version of the Suite and install the specific RC version: 2- Uninstall the current version of the Suite and install:
```bash ```bash
dotnet tool uninstall --global Volo.Abp.Suite && dotnet tool install -g Volo.Abp.Suite --version 3.1.0-rc.3 --add-source https://nuget.abp.io/<YOUR-API-KEY>/v3/index.json dotnet tool uninstall --global Volo.Abp.Suite && dotnet tool install -g Volo.Abp.Suite --version 3.1.0 --add-source https://nuget.abp.io/<YOUR-API-KEY>/v3/index.json
``` ```
Don't forget to replace the `<YOUR-API-KEY>` with your own key! Don't forget to replace the `<YOUR-API-KEY>` with your own key!

447
docs/en/Features.md

@ -1,3 +1,448 @@
# Features # Features
TODO ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**.
The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature.
Feature system was originally designed to control the tenant features in a **[multi-tenant](Multi-Tenancy.md)** application. However, it is **extensible** and capable of determining the features by any condition.
> The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md).
## Checking for the Features
Before explaining to define features, let's see how to check a feature value in your application code.
### RequiresFeature Attribute
`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is `true` (enabled) or not. It is a useful shortcut for the `boolean` features.
**Example: Check if the "PDF Reporting" feature enabled**
```csharp
public class ReportingAppService : ApplicationService, IReportingAppService
{
[RequiresFeature("MyApp.PdfReporting")]
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
//TODO...
}
}
```
* `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) is thrown and a proper response is returned to the client side.
* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the methods of that class require the given feature.
* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if any of the features enabled. Use `RequiresAll` option, like `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to check all of the features to be enabled.
* Multiple usage of `[RequiresFeature]` attribute is supported for a method or class. ABP check checks all of them in that case.
> Feature name can be any arbitrary string. It should be unique for a feature.
#### About the Interception
ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class (application services, controllers...) that is injected from the [dependency injection](Dependency-Injection.md).
However, there are **some rules should be followed** in order to make it working;
* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted.
> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP Framework uses the action/page filters to implement the feature checking in this case.
### IFeatureChecker Service
`IFeatureChecker` allows to check a feature in your application code.
#### IsEnabledAsync
Returns `true` if the given feature is enabled. So, you can conditionally execute your business flow.
**Example: Check if the "PDF Reporting" feature enabled**
```csharp
public class ReportingAppService : ApplicationService, IReportingAppService
{
private readonly IFeatureChecker _featureChecker;
public ReportingAppService(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting"))
{
//TODO...
}
else
{
//TODO...
}
}
}
```
`IsEnabledAsync` has overloads to check multiple features in one method call.
#### GetOrNullAsync
Gets the current value for a feature. This method returns a `string`, so you store any kind of value inside it, by converting to or from `string`.
**Example: Check the maximum product count allowed**
```csharp
public class ProductController : AbpController
{
private readonly IFeatureChecker _featureChecker;
public ProductController(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<IActionResult> Create(CreateProductModel model)
{
var currentProductCount = await GetCurrentProductCountFromDatabase();
//GET THE FEATURE VALUE
var maxProductCountLimit =
await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount");
if (currentProductCount >= Convert.ToInt32(maxProductCountLimit))
{
throw new BusinessException(
"MyApp:ReachToMaxProductCountLimit",
$"You can not create more than {maxProductCountLimit} products!"
);
}
//TODO: Create the product in the database...
}
private async Task<int> GetCurrentProductCountFromDatabase()
{
throw new System.NotImplementedException();
}
}
```
This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application.
Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method:
```csharp
var maxProductCountLimit = await _featureChecker.GetAsync<int>("MyApp.MaxProductCount");
```
#### Extension Methods
There are some useful extension methods for the `IFeatureChecker` interface;
* `Task<T> GetAsync<T>(string name, T defaultValue = default)`: Used to get a value of a feature with the given type `T`. Allows to specify a `defaultValue` that is returned when the feature value is `null`.
* `CheckEnabledAsync(string name)`: Checks if given feature is enabled. Throws an `AbpAuthorizationException` if the feature was not `true` (enabled).
## Defining the Features
A feature should be defined to be able to check it.
### FeatureDefinitionProvider
Create a class inheriting the `FeatureDefinitionProvider` to define permissions.
**Example: Defining permissions**
```csharp
using Volo.Abp.Features;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false");
myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10");
}
}
}
```
> ABP automatically discovers this class and registers the features. No additional configuration required.
> This class is generally created in the `Application.Contracts` project of your solution.
* In the `Define` method, you first need to add a **feature group** for your application/module or get an existing group then add **features** to this group.
* First feature, named `MyApp.PdfReporting`, is a `boolean` feature with `false` as the default value.
* Second feature, named `MyApp.MaxProductCount`, is a numeric feature with `10` as the default value.
Default value is used if there is no other value set for the current user/tenant.
### Other Feature Properties
While these minimal definitions are enough to make the feature system working, you can specify the **optional properties** for the features;
* `DisplayName`: A localizable string that will be used to show the feature name on the user interface.
* `Description`: A longer localizable text to describe the feature.
* `ValueType`: Type of the feature value. Can be a class implementing the `IStringValueType`. Built-in types:
* `ToggleStringValueType`: Used to define `true`/`false`, `on`/`off`, `enabled`/`disabled` style features. A checkbox is shown on the UI.
* `FreeTextStringValueType`: Used to define free text values. A textbox is shown on the UI.
* `SelectionStringValueType`: Used to force the value to be selected from a list. A dropdown list is shown on the UI.
* `IsVisibleToClients` (default: `true`): Set false to hide the value of this feature from clients (browsers). Sharing the value with the clients helps them to conditionally show/hide/change the UI parts based on the feature value.
* `Properties`: A dictionary to set/get arbitrary key-value pairs related to this feature. This can be a point for customization.
So, based on these descriptions, it would be better to define these features as shown below:
```csharp
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
myGroup.AddFeature(
"MyApp.MaxProductCount",
defaultValue: "10",
displayName: LocalizableString
.Create<FeaturesDemoResource>("MaxProductCount"),
valueType: new FreeTextStringValueType(
new NumericValueValidator(0, 1000000))
);
}
}
}
```
* `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) for details about the localization system.
* First feature is set to `ToggleStringValueType`, while the second one is set to `FreeTextStringValueType` with a numeric validator that allows to the values from `0` to `1,000,000`.
Remember to define the localization the keys in your localization file:
````json
"PdfReporting": "PDF Reporting",
"MaxProductCount": "Maximum number of products"
````
See the [localization document](Localization.md) for details about the localization system.
### Feature Management Modal
The [application startup template](Startup-Templates/Application.md) comes with the [tenant management](Modules/Tenant-Management.md) and the [feature management](Modules/Feature-Management.md) modules pre-installed.
Whenever you define a new feature, it will be available on the **feature management modal**. To open this modal, navigate to the **tenant management page** and select the `Features` action for a tenant (create a new tenant if there is no tenant yet):
![features-action](images/features-action.png)
This action opens a modal to manage the feature values for the selected tenant:
![features-modal](images/features-modal.png)
So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application.
See the *Feature Management* section below to learn more about managing the features.
### Child Features
A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled.
**Example: Defining child features**
```csharp
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
var reportingFeature = myGroup.AddFeature(
"MyApp.Reporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("Reporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.ExcelReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("ExcelReporting"),
valueType: new ToggleStringValueType()
);
}
}
}
```
The example above defines a *Reporting* feature with two children: *PDF Reporting* and *Excel Reporting*.
### Changing Features Definitions of a Depended Module
A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
**Example: Manipulate an existing feature definition**
```csharp
var someGroup = context.GetGroupOrNull("SomeModule");
var feature = someGroup.Features.FirstOrDefault(f => f.Name == "SomeFeature");
if (feature != null)
{
feature.Description = ...
feature.CreateChild(...);
}
```
## Check a Feature in the Client Side
A feature value is available at the client side too, unless you set `IsVisibleToClients` to `false` on the feature definition. The feature values are exposed from the [Application Configuration API](API/Application-Configuration.md) and usable via some services on the UI.
### ASP.NET Core MVC / Razor Pages UI
Use `abp.features` API to get the feature values.
**Example: Get feature values in the JavaScript code**
````js
var isEnabled = abp.features.values["MyApp.ExcelReporting"] === "true";
var count = abp.features.values["MyApp.MaxProductCount"];
````
### Angular UI
See the [features](Features.md) document for the Angular UI.
## Feature Management
Feature management is normally done by an admin user using the feature management modal:
![features-modal](images/features-modal.png)
This modal is available on the related entities, like tenants in a multi-tenant application. To open it, navigate to the **Tenant Management** page (for a multi-tenant application), click to the **Actions** button left to the Tenant and select the **Features** action.
If you need to manage features by code, inject the `IFeatureManager` service.
**Example: Enable PDF reporting for a tenant**
```csharp
public class MyService : ITransientDependency
{
private readonly IFeatureManager _featureManager;
public MyService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task EnablePdfReporting(Guid tenantId)
{
await _featureManager.SetForTenantAsync(
tenantId,
"MyApp.PdfReporting",
true.ToString()
);
}
}
```
`IFeatureManager` is defined by the Feature Management module. It comes pre-installed with the application startup template. See the [feature management module documentation](Modules/Feature-Management.md) for more information.
## Advanced Topics
### Feature Value Providers
Feature system is extensible. Any class derived from `FeatureValueProvider` (or implements `IFeatureValueProvider`) can contribute to the feature system. A value provider is responsible to **obtain the current value** of a given feature.
Feature value providers are **executed one by one**. If one of them return a non-null value, then this feature value is used and the other providers are not executed.
There are three pre-defined value providers, executed by the given order:
* `TenantFeatureValueProvider` tries to get if the feature value is explicitly set for the **current tenant**.
* `EditionFeatureValueProvider` tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (`ICurrentPrincipalAccessor`) with the claim name `editionid` (a constant defined as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](Modules/Tenant-Management.md) module. You can implement it yourself or consider to use the [SaaS module](https://commercial.abp.io/modules/Volo.Saas) of the ABP Commercial.
* `DefaultValueFeatureValueProvider` gets the default value of the feature.
You can write your own provider by inheriting the `FeatureValueProvider`.
**Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value**
```csharp
using System.Threading.Tasks;
using Volo.Abp.Features;
using Volo.Abp.Security.Claims;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class SystemAdminFeatureValueProvider : FeatureValueProvider
{
public override string Name => "SA";
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;
public SystemAdminFeatureValueProvider(
IFeatureStore featureStore,
ICurrentPrincipalAccessor currentPrincipalAccessor)
: base(featureStore)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
}
public override Task<string> GetOrNullAsync(FeatureDefinition feature)
{
if (feature.ValueType is ToggleStringValueType &&
_currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin")
{
return Task.FromResult("true");
}
return null;
}
}
}
```
If a provider returns `null`, then the next provider is executed.
Once a provider is defined, it should be added to the `AbpFeatureOptions` as shown below:
```csharp
Configure<AbpFeatureOptions>(options =>
{
options.ValueProviders.Add<SystemAdminFeatureValueProvider>();
});
```
Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) class.
### Feature Store
`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information

16
docs/en/Index.md

@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation.
## Getting Started ## Getting Started
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) tutorial. The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide.
Then you can continue with the [web application development tutorial](Tutorials/Part-1.md). ## Tutorials / Articles
### Web Application Development
[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework.
### ABP Community Articles
See also the [ABP Community](https://community.abp.io/) articles.
## Samples
See the [sample projects](Samples/Index.md) built with the ABP Framework.
## Source Code ## Source Code

2
docs/en/Modules/Feature-Management.md

@ -1,3 +1,5 @@
# Feature Management Module # Feature Management Module
> This module implements the `IFeatureStore` to store and manage feature values in a database. See the [Features System document](../Features.md) to understand the features first.
TODO TODO

2
docs/en/Modules/Permission-Management.md

@ -1,3 +1,5 @@
# Permission Management Module # Permission Management Module
This module implements the `IPermissionStore` to store and manage feature values in a database. See the [Authorization document](../Authorization.md) to understand the authorization and permission systems first.
TODO TODO

3
docs/en/Multi-Tenancy.md

@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.com");
options.AddDomainTenantResolver("{0}.com"); options.AddDomainTenantResolver("{0}.com");
```` ````
## See Also
* [Features](Features.md)

77
docs/en/UI/Angular/Component-Replacement.md

@ -8,27 +8,22 @@ The reason that you **can replace** but **cannot customize** default ABP compone
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) ) {
{ this.replaceableComponents.add({
// dispatched the AddReplaceableComponent action component: YourNewRoleComponent,
this.store.dispatch( key: eIdentityComponents.Roles,
new AddReplaceableComponent({ });
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
}),
);
} }
} }
``` ```
@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store, // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) { ) {
// dispatched the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch( component: MyApplicationLayoutComponent,
new AddReplaceableComponent({ key: eThemeBasicComponents.ApplicationLayout,
component: MyApplicationLayoutComponent, });
key: eThemeBasicComponents.ApplicationLayout,
}),
);
} }
} }
``` ```
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. > If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
### Layout Components ### Layout Components
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: LogoComponent, component: LogoComponent,
key: eThemeBasicComponents.Logo, key: eThemeBasicComponents.Logo,
}), });
);
} }
} }
``` ```
@ -166,7 +153,7 @@ yarn ng generate component routes --entryComponent
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following: Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js ```js
import { ABP, ReplaceableComponents } from '@abp/ng.core'; import { ABP } from '@abp/ng.core';
import { import {
Component, Component,
HostBinding, HostBinding,
@ -309,26 +296,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: RoutesComponent, component: RoutesComponent,
key: eThemeBasicComponents.Routes, key: eThemeBasicComponents.Routes,
}), });
);
} }
} }
``` ```
@ -511,26 +494,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: NavItemsComponent, component: NavItemsComponent,
key: eThemeBasicComponents.NavItems, key: eThemeBasicComponents.NavItems,
}), });
);
} }
} }
``` ```

3
docs/en/UI/Angular/Features.md

@ -0,0 +1,3 @@
# Angular UI: Features
> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system.

12
docs/en/UI/Angular/Permission-Management-Component-Replacement.md

@ -473,24 +473,20 @@ Open the generated `permission-management.component.html` in `src/app/permission
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component'; import { PermissionManagementComponent } from './permission-management/permission-management.component';
//... //...
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(private store: Store) {} // injected store constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
// added dispatching the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: PermissionManagementComponent, component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement, key: ePermissionManagementComponents.PermissionManagement,
}) });
);
} }
} }
``` ```

4
docs/en/docs-nav.json

@ -170,6 +170,10 @@
"text": "Settings", "text": "Settings",
"path": "Settings.md" "path": "Settings.md"
}, },
{
"text": "Features",
"path": "Features.md"
},
{ {
"text": "Data Filtering", "text": "Data Filtering",
"path": "Data-Filtering.md" "path": "Data-Filtering.md"

BIN
docs/en/images/features-action.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/images/features-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

86
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers;
using Volo.Abp.Guids;
using Volo.Abp.VirtualFileSystem; using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.TagHelpers namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.TagHelpers
@ -17,10 +18,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
public string Title { get; set; } public string Title { get; set; }
private readonly IVirtualFileProvider _virtualFileProvider; private readonly IVirtualFileProvider _virtualFileProvider;
private readonly IGuidGenerator _guidGenerator;
public AbpComponentDemoSectionTagHelper(IVirtualFileProvider virtualFileProvider) public AbpComponentDemoSectionTagHelper(
IVirtualFileProvider virtualFileProvider,
IGuidGenerator guidGenerator)
{ {
_virtualFileProvider = virtualFileProvider; _virtualFileProvider = virtualFileProvider;
_guidGenerator = guidGenerator;
} }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
@ -29,34 +34,69 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
var content = await output.GetChildContentAsync(); var content = await output.GetChildContentAsync();
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section\">"); var previewId = _guidGenerator.Create();
output.PreContent.AppendHtml($"<h2>{Title}</h2>"); var codeBlockId = _guidGenerator.Create();
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section-body\">");
var codeBlockTabId = _guidGenerator.Create();
var tagHelperCodeBlockId = _guidGenerator.Create();
var bootstrapCodeBlockId = _guidGenerator.Create();
output.PreContent.AppendHtml("<div class=\"card\">");
output.PreContent.AppendHtml("<div class=\"card-header\">");
output.PreContent.AppendHtml("<div class=\"row\">");
output.PreContent.AppendHtml($"<div class=\"col\"><h4 class=\"card-title my-1\">{Title}</h4></div>");
output.PreContent.AppendHtml("<div class=\"col-auto\">");
output.PreContent.AppendHtml("<nav>");
output.PreContent.AppendHtml("<div class=\"nav nav-pills\" role=\"tablist\">");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link active\" id=\"nav-preview-tab-{previewId}\" data-toggle=\"tab\" href=\"#nav-preview-{previewId}\" role=\"tab\" aria-controls=\"nav-preview-{previewId}\" aria-selected=\"true\">Preview</a>");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link\" id=\"nav-code-tab-{codeBlockId}\" data-toggle=\"tab\" href=\"#nav-code-{codeBlockId}\" role=\"tab\" aria-controls=\"nav-code-{codeBlockId}\" aria-selected=\"false\">Code</a>");
output.PreContent.AppendHtml("</div>");
output.PreContent.AppendHtml("</nav>");
output.PreContent.AppendHtml("</div>"); // col-auto
output.PreContent.AppendHtml("</div>"); // row
output.PreContent.AppendHtml("</div>"); // card-header
output.PreContent.AppendHtml("<div class=\"card-body\">");
output.PreContent.AppendHtml($"<div class=\"tab-content\" id=\"nav-preview-tabContent-{previewId}\">");
output.PreContent.AppendHtml($"<div class=\"tab-pane fade show active\" id=\"nav-preview-{previewId}\" role=\"tabpanel\" aria-labelledby=\"nav-preview-tab-{previewId}\">");
/* component rendering here */ /* component rendering here */
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section-body
AppendRawSource(output);
AppendBootstrapSource(output, content);
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section
}
private static void AppendBootstrapSource(TagHelperOutput output, TagHelperContent content) output.PostContent.AppendHtml("</div>"); // tab-pane
{ output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"nav-code-{codeBlockId}\" role=\"tabpanel\" aria-labelledby=\"nav-code-tab-{codeBlockId}\">");
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-bs-source\">");
output.PostContent.AppendHtml("<h3>Bootstrap</h3>"); /* CodeBlock tabs */
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(content.GetContent()); output.PostContent.AppendHtml($"<ul class=\"nav nav-tabs\" id=\"code-block-tab-{codeBlockTabId}\" role=\"tablist\">");
output.PostContent.AppendHtml("<li class=\"nav-item\">");
output.PostContent.AppendHtml($"<a class=\"nav-link active\" id=\"tag-helper-tab-{tagHelperCodeBlockId}\" data-toggle=\"pill\" href=\"#tag-helper-{tagHelperCodeBlockId}\" role=\"tab\" aria-controls=\"tag-helper-{tagHelperCodeBlockId}\" aria-selected=\"true\">Abp Tag Helper</a>");
output.PostContent.AppendHtml("</li>");
output.PostContent.AppendHtml("<li class=\"nav-item\">");
output.PostContent.AppendHtml($"<a class=\"nav-link\" id=\"bootstrap-tab-{bootstrapCodeBlockId}\" data-toggle=\"pill\" href=\"#bootstrap-{bootstrapCodeBlockId}\" role=\"tab\" aria-controls=\"bootstrap-{tagHelperCodeBlockId}\" aria-selected=\"true\">Bootstrap</a>");
output.PostContent.AppendHtml("</li>");
output.PostContent.AppendHtml("</ul>");
output.PostContent.AppendHtml($"<div class=\"tab-content\" id=\"code-block-tabContent-{codeBlockTabId}\">");
output.PostContent.AppendHtml($"<div class=\"tab-pane fade show active\" id=\"tag-helper-{tagHelperCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"tag-helper-tab-{tagHelperCodeBlockId}\">");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml("</code>");
output.PostContent.AppendHtml("</pre>"); output.PostContent.AppendHtml("</pre>");
output.PostContent.AppendHtml("</div>"); output.PostContent.AppendHtml("</div>");
}
output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"bootstrap-{bootstrapCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"bootstrap-tab-{bootstrapCodeBlockId}\">");
private void AppendRawSource(TagHelperOutput output) output.PostContent.AppendHtml("<pre class=\"p-4\">");
{ output.PostContent.AppendHtml("<code>");
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-raw-source\">"); output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("<h3>ABP Tag Helpers</h3>"); output.PostContent.AppendHtml("</code>");
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml("</pre>"); output.PostContent.AppendHtml("</pre>");
output.PostContent.AppendHtml("</div>"); output.PostContent.AppendHtml("</div>");
output.PostContent.AppendHtml("</div>"); // tab-content
output.PostContent.AppendHtml("</div>"); // tab-pane
output.PostContent.AppendHtml("</div>"); // tab-content
output.PostContent.AppendHtml("</div>"); // card-body
output.PostContent.AppendHtml("</div>"); // card
} }
private string GetRawDemoSource() private string GetRawDemoSource()

7
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs

@ -72,7 +72,7 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
} }
var errorInfo = new RemoteServiceErrorInfo(); var errorInfo = new RemoteServiceErrorInfo();
if (exception is IUserFriendlyException) if (exception is IUserFriendlyException)
{ {
errorInfo.Message = exception.Message; errorInfo.Message = exception.Message;
@ -101,6 +101,8 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
errorInfo.Message = L["InternalServerErrorMessage"]; errorInfo.Message = L["InternalServerErrorMessage"];
} }
errorInfo.Data = exception.Data;
return errorInfo; return errorInfo;
} }
@ -190,7 +192,6 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
return exception; return exception;
} }
protected virtual RemoteServiceErrorInfo CreateDetailedErrorInfoFromException(Exception exception) protected virtual RemoteServiceErrorInfo CreateDetailedErrorInfoFromException(Exception exception)
{ {
var detailBuilder = new StringBuilder(); var detailBuilder = new StringBuilder();
@ -213,7 +214,7 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
detailBuilder.AppendLine(exception.GetType().Name + ": " + exception.Message); detailBuilder.AppendLine(exception.GetType().Name + ": " + exception.Message);
//Additional info for UserFriendlyException //Additional info for UserFriendlyException
if (exception is IUserFriendlyException && if (exception is IUserFriendlyException &&
exception is IHasErrorDetails) exception is IHasErrorDetails)
{ {
var details = ((IHasErrorDetails) exception).Details; var details = ((IHasErrorDetails) exception).Details;

9
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs

@ -62,6 +62,11 @@ namespace Volo.Abp.Features
return FeatureDefinitions.GetOrDefault(name); return FeatureDefinitions.GetOrDefault(name);
} }
public IReadOnlyList<FeatureGroupDefinition> GetGroups()
{
return FeatureGroupDefinitions.Values.ToImmutableList();
}
protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions() protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions()
{ {
var features = new Dictionary<string, FeatureDefinition>(); var features = new Dictionary<string, FeatureDefinition>();
@ -78,7 +83,7 @@ namespace Volo.Abp.Features
} }
protected virtual void AddFeatureToDictionaryRecursively( protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features, Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature) FeatureDefinition feature)
{ {
if (features.ContainsKey(feature.Name)) if (features.ContainsKey(feature.Name))
@ -114,4 +119,4 @@ namespace Volo.Abp.Features
return context.Groups; return context.Groups;
} }
} }
} }

4
framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs

@ -11,5 +11,7 @@ namespace Volo.Abp.Features
IReadOnlyList<FeatureDefinition> GetAll(); IReadOnlyList<FeatureDefinition> GetAll();
FeatureDefinition GetOrNull(string name); FeatureDefinition GetOrNull(string name);
IReadOnlyList<FeatureGroupDefinition> GetGroups();
} }
} }

8
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpRemoteCallException.cs

@ -28,6 +28,14 @@ namespace Volo.Abp.Http.Client
: base(error.Message) : base(error.Message)
{ {
Error = error; Error = error;
if (error.Data != null)
{
foreach (var dataKey in error.Data.Keys)
{
Data[dataKey] = error.Data[dataKey];
}
}
} }
} }
} }

6
framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorInfo.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
namespace Volo.Abp.Http namespace Volo.Abp.Http
{ {
@ -23,6 +25,8 @@ namespace Volo.Abp.Http
/// </summary> /// </summary>
public string Details { get; set; } public string Details { get; set; }
public IDictionary Data { get; set; }
/// <summary> /// <summary>
/// Validation errors if exists. /// Validation errors if exists.
/// </summary> /// </summary>
@ -49,4 +53,4 @@ namespace Volo.Abp.Http
Code = code; Code = code;
} }
} }
} }

46
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/luxon/abp.luxon.js

@ -0,0 +1,46 @@
var abp = abp || {};
(function () {
if (!luxon) {
throw "abp/luxon library requires the luxon library included to the page!";
}
/* TIMING *************************************************/
abp.timing = abp.timing || {};
var setObjectValue = function (obj, property, value) {
if (typeof property === "string") {
property = property.split('.');
}
if (property.length > 1) {
var p = property.shift();
setObjectValue(obj[p], property, value);
} else {
obj[property[0]] = value;
}
}
var getObjectValue = function (obj, property) {
return property.split('.').reduce((a, v) => a[v], obj)
}
abp.timing.convertFieldsToIsoDate = function (form, fields) {
for (var field of fields) {
var dateTime = luxon.DateTime
.fromFormat(
getObjectValue(form, field),
abp.localization.currentCulture.dateTimeFormat.shortDatePattern,
{locale: abp.localization.currentCulture.cultureName}
);
if (!dateTime.invalid) {
setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss"))
}
}
return form;
}
})(jQuery);

4
framework/test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj

@ -14,4 +14,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Volo\Abp\Http\Localization\en.json" />
</ItemGroup>
</Project> </Project>

22
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs

@ -2,8 +2,12 @@
using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Http.Client; using Volo.Abp.Http.Client;
using Volo.Abp.Http.DynamicProxying; using Volo.Abp.Http.DynamicProxying;
using Volo.Abp.Http.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.TestApp; using Volo.Abp.TestApp;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Http namespace Volo.Abp.Http
{ {
@ -22,6 +26,24 @@ namespace Volo.Abp.Http
{ {
options.RemoteServices.Default = new RemoteServiceConfiguration("/"); options.RemoteServices.Default = new RemoteServiceConfiguration("/");
}); });
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpHttpClientTestModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<HttpClientTestResource>("en")
.AddVirtualJson("/Volo/Abp/Http/Localization");
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("Volo.Abp.Http.DynamicProxying", typeof(HttpClientTestResource));
});
} }
} }
} }

2
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs

@ -9,6 +9,8 @@ namespace Volo.Abp.Http.DynamicProxying
Task GetException1Async(); Task GetException1Async();
Task GetException2Async();
Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1); Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1);
Task<string> PostValueWithHeaderAndQueryStringAsync(string headerValue, string qsValue); Task<string> PostValueWithHeaderAndQueryStringAsync(string headerValue, string qsValue);

10
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs

@ -27,6 +27,14 @@ namespace Volo.Abp.Http.DynamicProxying
throw new UserFriendlyException("This is an error message!"); throw new UserFriendlyException("This is an error message!");
} }
[HttpGet]
[Route("get-exception2")]
public Task GetException2Async()
{
throw new BusinessException("Volo.Abp.Http.DynamicProxying:10001")
.WithData("0","TEST");
}
[HttpGet] [HttpGet]
[Route("get-with-datetime-parameter")] [Route("get-with-datetime-parameter")]
public Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1) public Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1)
@ -133,4 +141,4 @@ namespace Volo.Abp.Http.DynamicProxying
[FromQuery] [FromQuery]
public DateTime FirstReleaseDate { get; set; } public DateTime FirstReleaseDate { get; set; }
} }
} }

11
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs

@ -1,8 +1,10 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Shouldly; using Shouldly;
using Volo.Abp.Http.Client; using Volo.Abp.Http.Client;
using Volo.Abp.Http.Localization;
using Volo.Abp.Localization; using Volo.Abp.Localization;
using Xunit; using Xunit;
@ -32,6 +34,13 @@ namespace Volo.Abp.Http.DynamicProxying
exception.Error.Message.ShouldBe("This is an error message!"); exception.Error.Message.ShouldBe("This is an error message!");
} }
[Fact]
public async Task GetException2Async()
{
var exception = await Assert.ThrowsAsync<AbpRemoteCallException>(async () => await _controller.GetException2Async());
exception.Error.Message.ShouldBe("Business exception with data: TEST");
}
[Fact] [Fact]
public async Task GetWithDateTimeParameterAsync() public async Task GetWithDateTimeParameterAsync()
{ {
@ -151,4 +160,4 @@ namespace Volo.Abp.Http.DynamicProxying
} }
} }
} }

10
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Localization/HttpClientTestResource.cs

@ -0,0 +1,10 @@
using Volo.Abp.Localization;
namespace Volo.Abp.Http.Localization
{
[LocalizationResourceName("HttpClientTest")]
public class HttpClientTestResource
{
}
}

6
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Localization/en.json

@ -0,0 +1,6 @@
{
"culture": "en",
"texts": {
"Volo.Abp.Http.DynamicProxying:10001": "Business exception with data: {0}"
}
}

2
framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs

@ -2,9 +2,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Volo.Abp.TestApp.Testing; using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering namespace Volo.Abp.MongoDB.DataFiltering
{ {
[Collection(MongoTestCollection.Name)]
public class HardDelete_Tests : HardDelete_Tests<AbpMongoDbTestModule> public class HardDelete_Tests : HardDelete_Tests<AbpMongoDbTestModule>
{ {
} }

6
global.json

@ -0,0 +1,6 @@
{
"sdk": {
"version": "3.1.102",
"rollForward": "latestFeature"
}
}

4
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json

@ -53,6 +53,8 @@
"ResetPassword_Information": "Please enter your new password.", "ResetPassword_Information": "Please enter your new password.",
"YourPasswordIsSuccessfullyReset": "Your password is successfully reset.", "YourPasswordIsSuccessfullyReset": "Your password is successfully reset.",
"GoToTheApplication": "Go to the application", "GoToTheApplication": "Go to the application",
"BackToLogin": "Back to login" "BackToLogin": "Back to login",
"ProfileTab:Password": "Change password",
"ProfileTab:PersonalInfo": "Personal info"
} }
} }

4
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json

@ -52,6 +52,8 @@
"ResetPassword_Information": "Lütfen yeni şifrenizi belirleyin.", "ResetPassword_Information": "Lütfen yeni şifrenizi belirleyin.",
"YourPasswordIsSuccessfullyReset": "Şifreniz başarıyla sıfırlandı.", "YourPasswordIsSuccessfullyReset": "Şifreniz başarıyla sıfırlandı.",
"GoToTheApplication": "Uygulamaya git", "GoToTheApplication": "Uygulamaya git",
"BackToLogin": "Girişe dön" "BackToLogin": "Girişe dön",
"ProfileTab:Password": "Şifre değiştir",
"ProfileTab:PersonalInfo": "Kişisel bilgiler"
} }
} }

3
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs

@ -1,6 +1,7 @@
using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using AutoMapper; using AutoMapper;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
namespace Volo.Abp.Account.Web namespace Volo.Abp.Account.Web
{ {
@ -8,7 +9,7 @@ namespace Volo.Abp.Account.Web
{ {
public AbpAccountWebAutoMapperProfile() public AbpAccountWebAutoMapperProfile()
{ {
CreateMap<ProfileDto, PersonalSettingsInfoModel>(); CreateMap<ProfileDto, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel>();
} }
} }
} }

31
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs

@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account.Localization; using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
@ -50,16 +53,38 @@ namespace Volo.Abp.Account.Web
options.Contributors.Add(new AccountModuleToolbarContributor()); options.Contributors.Add(new AccountModuleToolbarContributor());
}); });
ConfigureProfileManagementPage();
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true);
});
}
private void ConfigureProfileManagementPage()
{
Configure<RazorPagesOptions>(options => Configure<RazorPagesOptions>(options =>
{ {
options.Conventions.AuthorizePage("/Account/Manage"); options.Conventions.AuthorizePage("/Account/Manage");
}); });
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>(); Configure<ProfileManagementPageOptions>(options =>
Configure<AbpAutoMapperOptions>(options =>
{ {
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true); options.Contributors.Add(new AccountProfileManagementPageContributor());
}); });
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles
.Configure(typeof(ManageModel).FullName,
configuration =>
{
configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Password/Default.js");
configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js");
});
});
} }
} }
} }

59
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs

@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Auditing;
using Volo.Abp.Identity;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password
{
public class AccountProfilePasswordManagementGroupViewComponent : AbpViewComponent
{
private readonly IProfileAppService _profileAppService;
public AccountProfilePasswordManagementGroupViewComponent(
IProfileAppService profileAppService)
{
_profileAppService = profileAppService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var user = await _profileAppService.GetAsync();
var model = new ChangePasswordInfoModel
{
HideOldPasswordInput = !user.HasPassword
};
return View("~/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml", model);
}
public class ChangePasswordInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:CurrentPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string CurrentPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPasswordConfirm")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPasswordConfirm { get; set; }
public bool HideOldPasswordInput { get; set; }
}
}
}

17
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml

@ -0,0 +1,17 @@
@using Volo.Abp.Account.Localization
@using Volo.Abp.Users
@using Microsoft.AspNetCore.Mvc.Localization
@inject IHtmlLocalizer<AccountResource> L
@inject ICurrentUser CurrentUser
@model Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password.AccountProfilePasswordManagementGroupViewComponent.ChangePasswordInfoModel
<h4>@L["ChangePassword"]</h4><hr/>
<form id="ChangePasswordForm">
@if (!Model.HideOldPasswordInput)
{
<abp-input asp-for="CurrentPassword"/>
}
<abp-input asp-for="NewPassword"/>
<abp-input asp-for="NewPasswordConfirm"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>

31
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js

@ -0,0 +1,31 @@
(function ($) {
$(function () {
var l = abp.localization.getResource("AbpAccount");
$('#ChangePasswordForm').submit(function (e) {
e.preventDefault();
if (!$('#ChangePasswordForm').valid()) {
return false;
}
var input = $('#ChangePasswordForm').serializeFormToObject();
if (
input.newPassword != input.newPasswordConfirm ||
input.newPassword == ''
) {
abp.message.error(l('NewPasswordConfirmFailed'));
return;
}
if (input.currentPassword && input.currentPassword == ''){
return;
}
volo.abp.identity.profile.changePassword(input).then(function (result) {
abp.message.success(l('PasswordChanged'));
});
});
});
})(jQuery);

56
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs

@ -0,0 +1,56 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Identity;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo
{
public class AccountProfilePersonalInfoManagementGroupViewComponent : AbpViewComponent
{
private readonly IProfileAppService _profileAppService;
public AccountProfilePersonalInfoManagementGroupViewComponent(
IProfileAppService profileAppService)
{
_profileAppService = profileAppService;
ObjectMapperContext = typeof(AbpAccountWebModule);
}
public async Task<IViewComponentResult> InvokeAsync()
{
var user = await _profileAppService.GetAsync();
var model = ObjectMapper.Map<ProfileDto, PersonalInfoModel>(user);
return View("~/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml", model);
}
public class PersonalInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))]
[Display(Name = "DisplayName:UserName")]
public string UserName { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))]
[Display(Name = "DisplayName:Email")]
public string Email { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))]
[Display(Name = "DisplayName:Name")]
public string Name { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxSurnameLength))]
[Display(Name = "DisplayName:Surname")]
public string Surname { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))]
[Display(Name = "DisplayName:PhoneNumber")]
public string PhoneNumber { get; set; }
}
}
}

39
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml

@ -0,0 +1,39 @@
@using Volo.Abp.Account.Localization
@using Volo.Abp.Users
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@using Volo.Abp.Identity.Settings
@using Volo.Abp.Settings
@inject IHtmlLocalizer<AccountResource> L
@inject ICurrentUser CurrentUser
@inject ISettingProvider SettingManager
@inject IThemeManager ThemeManager
@model Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo.AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel
@{
var isUserNameUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsUserNameUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
var isEmailUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsEmailUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
}
<h4>@L["PersonalSettings"]</h4><hr/>
<form method="post" id="PersonalSettingsForm">
<abp-input asp-for="UserName" readonly="!isUserNameUpdateEnabled"/>
<abp-row>
<abp-column size-md="_6">
<abp-input asp-for="Name"/>
</abp-column>
<abp-column size-md="_6">
<abp-input asp-for="Surname"/>
</abp-column>
</abp-row>
<abp-input asp-for="Email" readonly="!isEmailUpdateEnabled"/>
<abp-input asp-for="PhoneNumber"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>

19
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js

@ -0,0 +1,19 @@
(function ($) {
$(function () {
var l = abp.localization.getResource("AbpAccount");
$('#PersonalSettingsForm').submit(function (e) {
e.preventDefault();
if (!$('#PersonalSettingsForm').valid()) {
return false;
}
var input = $('#PersonalSettingsForm').serializeFormToObject();
volo.abp.identity.profile.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved'));
});
});
});
})(jQuery);

73
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml

@ -2,66 +2,33 @@
@using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Account.Localization @using Volo.Abp.Account.Localization
@using Volo.Abp.Account.Web.Pages.Account @using Volo.Abp.Account.Web.Pages.Account
@using Volo.Abp.Identity.Settings
@using Volo.Abp.Settings
@using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Abp.AspNetCore.Mvc.UI.Theming
@inject ISettingProvider SettingManager
@inject IThemeManager ThemeManager @inject IThemeManager ThemeManager
@inject IHtmlLocalizer<AccountResource> L @inject IHtmlLocalizer<AccountResource> L
@model ManageModel @model ManageModel
@{ @{
Layout = ThemeManager.CurrentTheme.GetApplicationLayout(); Layout = ThemeManager.CurrentTheme.GetApplicationLayout();
var isUserNameUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsUserNameUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
var isEmailUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsEmailUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
} }
@section scripts { @section scripts {
<abp-script-bundle name="@typeof(ManageModel).FullName"> <abp-script-bundle name="@typeof(ManageModel).FullName"/>
<abp-script src="/Pages/Account/Manage.js" />
</abp-script-bundle>
} }
<abp-card>
<abp-card-body>
<abp-tabs tab-style="PillVertical">
@if (!Model.DisablePasswordChange)
{
<abp-tab title="@L["ChangePassword"].Value">
<h4>@L["ChangePassword"].Value</h4><hr/>
<form id="ChangePasswordForm">
@if (!Model.HideOldPasswordInput)
{
<abp-input asp-for="ChangePasswordInfoModel.CurrentPassword"/>
}
<abp-input asp-for="ChangePasswordInfoModel.NewPassword"/>
<abp-input asp-for="ChangePasswordInfoModel.NewPasswordConfirm"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>
</abp-tab>
}
<abp-tab title="@L["PersonalSettings"].Value">
<h4>@L["PersonalSettings"].Value</h4><hr/>
<form method="post" id="PersonalSettingsForm">
<abp-input asp-for="PersonalSettingsInfoModel.UserName" readonly="!isUserNameUpdateEnabled"/>
<abp-row>
<abp-column size-md="_6">
<abp-input asp-for="PersonalSettingsInfoModel.Name"/>
</abp-column>
<abp-column size-md="_6">
<abp-input asp-for="PersonalSettingsInfoModel.Surname"/>
</abp-column>
</abp-row>
<abp-input asp-for="PersonalSettingsInfoModel.Email" readonly="!isEmailUpdateEnabled"/> <div id="ProfileManagementWrapper">
<abp-card>
<abp-input asp-for="PersonalSettingsInfoModel.PhoneNumber"/> <abp-card-body>
<abp-tabs tab-style="PillVertical" vertical-header-size="_3">
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/> @foreach (var group in Model.ProfileManagementPageCreationContext.Groups)
</form> {
</abp-tab> <abp-tab title="@group.DisplayName">
</abp-tabs> <h2>@group.DisplayName</h2>
</abp-card-body> <hr class="my-4" />
</abp-card> @await Component.InvokeAsync(group.ComponentType, new
{
parameter = group.Parameter
})
</abp-tab>
}
</abp-tabs>
</abp-card-body>
</abp-card>
</div>

72
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml.cs

@ -2,35 +2,31 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.Validation; using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account namespace Volo.Abp.Account.Web.Pages.Account
{ {
public class ManageModel : AccountPageModel public class ManageModel : AccountPageModel
{ {
public ChangePasswordInfoModel ChangePasswordInfoModel { get; set; } public ProfileManagementPageCreationContext ProfileManagementPageCreationContext { get; private set; }
public PersonalSettingsInfoModel PersonalSettingsInfoModel { get; set; } protected ProfileManagementPageOptions Options { get; }
public bool DisablePasswordChange { get; set; } public ManageModel(IOptions<ProfileManagementPageOptions> options)
public bool HideOldPasswordInput { get; set; }
protected IProfileAppService ProfileAppService { get; }
public ManageModel(IProfileAppService profileAppService)
{ {
ProfileAppService = profileAppService; Options = options.Value;
} }
public virtual async Task<IActionResult> OnGetAsync() public virtual async Task<IActionResult> OnGetAsync()
{ {
var user = await ProfileAppService.GetAsync(); ProfileManagementPageCreationContext = new ProfileManagementPageCreationContext(ServiceProvider);
PersonalSettingsInfoModel = ObjectMapper.Map<ProfileDto, PersonalSettingsInfoModel>(user);
DisablePasswordChange = user.IsExternal; foreach (var contributor in Options.Contributors)
HideOldPasswordInput = !user.HasPassword; {
await contributor.ConfigureAsync(ProfileManagementPageCreationContext);
}
return Page(); return Page();
} }
@ -40,50 +36,4 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Task.FromResult<IActionResult>(Page()); return Task.FromResult<IActionResult>(Page());
} }
} }
public class ChangePasswordInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:CurrentPassword")]
[DataType(DataType.Password)]
public string CurrentPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPassword")]
[DataType(DataType.Password)]
public string NewPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPasswordConfirm")]
[DataType(DataType.Password)]
public string NewPasswordConfirm { get; set; }
}
public class PersonalSettingsInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))]
[Display(Name = "DisplayName:UserName")]
public string UserName { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))]
[Display(Name = "DisplayName:Email")]
public string Email { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))]
[Display(Name = "DisplayName:Name")]
public string Name { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxSurnameLength))]
[Display(Name = "DisplayName:Surname")]
public string Surname { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))]
[Display(Name = "DisplayName:PhoneNumber")]
public string PhoneNumber { get; set; }
}
} }

47
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.js

@ -1,47 +0,0 @@
(function ($) {
var l = abp.localization.getResource('AbpAccount');
var _profileService = volo.abp.identity.profile;
$('#ChangePasswordForm').submit(function (e) {
e.preventDefault();
if (!$('#ChangePasswordForm').valid()) {
return false;
}
var input = $('#ChangePasswordForm').serializeFormToObject()
.changePasswordInfoModel;
if (
input.newPassword != input.newPasswordConfirm ||
input.newPassword == ''
) {
abp.message.error(l('NewPasswordConfirmFailed'));
return;
}
if (input.currentPassword && input.currentPassword == ''){
return;
}
_profileService.changePassword(input).then(function (result) {
abp.message.success(l('PasswordChanged'));
});
});
$('#PersonalSettingsForm').submit(function (e) {
e.preventDefault();
if (!$('#PersonalSettingsForm').valid()) {
return false;
}
var input = $('#PersonalSettingsForm').serializeFormToObject()
.personalSettingsInfoModel;
_profileService.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved'));
});
});
})(jQuery);

1
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -18,7 +18,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
{ {
public class RegisterModel : AccountPageModel public class RegisterModel : AccountPageModel
{ {
protected IAccountAppService AccountAppService { get; }
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; } public string ReturnUrl { get; set; }

48
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/AccountProfileManagementPageContributor.cs

@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
using Volo.Abp.Identity;
using Volo.Abp.Users;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class AccountProfileManagementPageContributor : IProfileManagementPageContributor
{
public async Task ConfigureAsync(ProfileManagementPageCreationContext context)
{
var l = context.ServiceProvider.GetRequiredService<IStringLocalizer<AccountResource>>();
if (await IsPasswordChangeEnabled(context))
{
context.Groups.Add(
new ProfileManagementPageGroup(
"Volo.Abp.Account.Password",
l["ProfileTab:Password"],
typeof(AccountProfilePasswordManagementGroupViewComponent)
)
);
}
context.Groups.Add(
new ProfileManagementPageGroup(
"Volo.Abp.Account.PersonalInfo",
l["ProfileTab:PersonalInfo"],
typeof(AccountProfilePersonalInfoManagementGroupViewComponent)
)
);
}
protected virtual async Task<bool> IsPasswordChangeEnabled(ProfileManagementPageCreationContext context)
{
var userManager = context.ServiceProvider.GetRequiredService<IdentityUserManager>();
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
var user = await userManager.GetByIdAsync(currentUser.GetId());
return !user.IsExternal;
}
}
}

9
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/IProfileManagementPageContributor.cs

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public interface IProfileManagementPageContributor
{
Task ConfigureAsync(ProfileManagementPageCreationContext context);
}
}

19
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageCreationContext.cs

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageCreationContext
{
public IServiceProvider ServiceProvider { get; }
public List<ProfileManagementPageGroup> Groups { get; }
public ProfileManagementPageCreationContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Groups = new List<ProfileManagementPageGroup>();
}
}
}

39
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageGroup.cs

@ -0,0 +1,39 @@
using System;
using JetBrains.Annotations;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageGroup
{
public string Id
{
get => _id;
set => _id = Check.NotNullOrWhiteSpace(value, nameof(Id));
}
private string _id;
public string DisplayName
{
get => _displayName;
set => _displayName = Check.NotNullOrWhiteSpace(value, nameof(DisplayName));
}
private string _displayName;
public Type ComponentType
{
get => _componentType;
set => _componentType = Check.NotNull(value, nameof(ComponentType));
}
private Type _componentType;
public object Parameter { get; set; }
public ProfileManagementPageGroup([NotNull] string id, [NotNull] string displayName, [NotNull] Type componentType, object parameter = null)
{
Id = id;
DisplayName = displayName;
ComponentType = componentType;
Parameter = parameter;
}
}
}

14
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageOptions.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageOptions
{
public List<IProfileManagementPageContributor> Contributors { get; }
public ProfileManagementPageOptions()
{
Contributors = new List<IProfileManagementPageContributor>();
}
}
}

7
modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using IdentityModel; using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@ -108,12 +109,12 @@ namespace Volo.CmsKit
AbpClaimTypes.Role = JwtClaimTypes.Role; AbpClaimTypes.Role = JwtClaimTypes.Role;
AbpClaimTypes.Email = JwtClaimTypes.Email; AbpClaimTypes.Email = JwtClaimTypes.Email;
context.Services.AddAuthentication("Bearer") context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => .AddJwtBearer(options =>
{ {
options.Authority = configuration["AuthServer:Authority"]; options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.ApiName = "CmsKit"; options.Audience = "CmsKit";
}); });
Configure<AbpDistributedCacheOptions>(options => Configure<AbpDistributedCacheOptions>(options =>

3
modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj

@ -13,7 +13,8 @@
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityModel" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" />
<PackageReference Include="Volo.Abp.Autofac" Version="3.0.3" /> <PackageReference Include="Volo.Abp.Autofac" Version="3.0.3" />

4
modules/cms-kit/host/Volo.CmsKit.IdentityServer/CmsKitIdentityServerModule.cs

@ -123,11 +123,11 @@ namespace Volo.CmsKit
}); });
context.Services.AddAuthentication() context.Services.AddAuthentication()
.AddIdentityServerAuthentication(options => .AddJwtBearer(options =>
{ {
options.Authority = configuration["AuthServer:Authority"]; options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.ApiName = configuration["AuthServer:ApiName"]; options.Audience = configuration["AuthServer:ApiName"];
}); });
Configure<AbpDistributedCacheOptions>(options => Configure<AbpDistributedCacheOptions>(options =>

1
modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj

@ -11,7 +11,6 @@
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" />
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="3.0.3" /> <PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="3.0.3" />

1
modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;

1235
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.Designer.cs

File diff suppressed because it is too large

39
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.cs

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Volo.CmsKit.Migrations
{
public partial class CmsRatings_Added : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CmsRatings",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
TenantId = table.Column<Guid>(nullable: true),
EntityType = table.Column<string>(maxLength: 64, nullable: false),
EntityId = table.Column<string>(maxLength: 64, nullable: false),
StarCount = table.Column<short>(nullable: false),
CreatorId = table.Column<Guid>(nullable: false),
CreationTime = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CmsRatings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_CmsRatings_TenantId_EntityType_EntityId",
table: "CmsRatings",
columns: new[] { "TenantId", "EntityType", "EntityId" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CmsRatings");
}
}
}

2429
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs

File diff suppressed because it is too large

9
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml

@ -5,6 +5,7 @@
@using Volo.CmsKit.GlobalFeatures @using Volo.CmsKit.GlobalFeatures
@using Volo.CmsKit.Pages @using Volo.CmsKit.Pages
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting @using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection @using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection
@model IndexModel @model IndexModel
@inject IStringLocalizer<AbpUiResource> Localizer @inject IStringLocalizer<AbpUiResource> Localizer
@ -41,6 +42,12 @@
</abp-card-body> </abp-card-body>
</abp-card> </abp-card>
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new { entityType = "quote", entityId = "2" })
}
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>()) @if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{ {
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "2" }) @await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "2" })
@ -49,4 +56,4 @@
{ {
@await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "quote", entityId = "2" }) @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "quote", entityId = "2" })
} }
<hr class="my-5" /> <hr class="my-5" />

37
modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/css/star-rating-svg.css

@ -0,0 +1,37 @@
.jq-stars {
display: inline-block;
}
.jq-rating-label {
font-size: 22px;
display: inline-block;
position: relative;
vertical-align: top;
font-family: helvetica, arial, verdana;
}
.jq-star {
width: 100px;
height: 100px;
display: inline-block;
cursor: pointer;
}
.jq-star-svg {
width: 100%;
height: 100% ;
}
.jq-star:hover .fs-star-svg polygon {
}
.jq-star-svg polygon {
stroke: #000;
stroke-linejoin: round;
}
/* un-used */
.jq-shadow {
-webkit-filter: drop-shadow( -2px -2px 2px #888 );
filter: drop-shadow( -2px -2px 2px #888 );
}

1
modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/js/jquery.star-rating-svg.min.js

@ -0,0 +1 @@
!function(a){"use strict";var b="starRating",c=function(){},d={totalStars:5,useFullStars:!1,emptyColor:"lightgray",hoverColor:"orange",activeColor:"gold",useGradient:!0,readOnly:!1,disableAfterRate:!0,starGradient:{start:"#FEF7CD",end:"#FF9511"},strokeWidth:0,strokeColor:"black",initialRating:0,starSize:40,callback:c,onHover:c,onLeave:c},e=function(c,e){var f;this.element=c,this.$el=a(c),this.settings=a.extend({},d,e),f=this.$el.data("rating")||this.settings.initialRating,this._state={rating:(Math.round(2*f)/2).toFixed(1)},this._uid=Math.floor(999*Math.random()),e.starGradient||this.settings.useGradient||(this.settings.starGradient.start=this.settings.starGradient.end=this.settings.activeColor),this._defaults=d,this._name=b,this.init()},f={init:function(){this.renderMarkup(),this.addListeners(),this.initRating()},addListeners:function(){this.settings.readOnly||(this.$stars.on("mouseover",this.hoverRating.bind(this)),this.$stars.on("mouseout",this.restoreState.bind(this)),this.$stars.on("click",this.handleRating.bind(this)))},hoverRating:function(a){var b=this.getIndex(a);this.paintStars(b,"hovered"),this.settings.onHover(b+1,this._state.rating,this.$el)},handleRating:function(a){var b=this.getIndex(a),c=b+1;this.applyRating(c,this.$el),this.executeCallback(c,this.$el),this.settings.disableAfterRate&&this.$stars.off()},applyRating:function(a){var b=a-1;this.paintStars(b,"active"),this._state.rating=b+1},restoreState:function(a){var b=this.getIndex(a),c=this._state.rating||-1;this.paintStars(c-1,"active"),this.settings.onLeave(b+1,this._state.rating,this.$el)},getIndex:function(b){var c=a(b.currentTarget),d=c.width(),e=a(b.target).attr("data-side");e=e?e:this.getOffsetByPixel(b,c,d),e=this.settings.useFullStars?"right":e;var f=c.index()-("left"===e?.5:0);return f=.5>f&&b.offsetX<d/4?-1:f},getOffsetByPixel:function(a,b,c){var d=a.pageX-b.offset().left;return c/2>=d&&!this.settings.useFullStars?"left":"right"},initRating:function(){this.paintStars(this._state.rating-1,"active")},paintStars:function(b,c){var d,e,f,g;a.each(this.$stars,function(h,i){d=a(i).find('polygon[data-side="left"]'),e=a(i).find('polygon[data-side="right"]'),f=g=b>=h?c:"empty",f=h-b===.5?c:f,d.attr("class","svg-"+f+"-"+this._uid),e.attr("class","svg-"+g+"-"+this._uid)}.bind(this))},renderMarkup:function(){for(var a='<div class="jq-star" style="width:'+this.settings.starSize+"px; height:"+this.settings.starSize+'px;"><svg version="1.0" class="jq-star-svg" shape-rendering="geometricPrecision" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="305px" height="305px" viewBox="60 -62 309 309" style="enable-background:new 64 -59 305 305; stroke-width:'+this.settings.strokeWidth+'px;" xml:space="preserve"><style type="text/css">.svg-empty-'+this._uid+"{fill:url(#"+this._uid+"_SVGID_1_);}.svg-hovered-"+this._uid+"{fill:url(#"+this._uid+"_SVGID_2_);}.svg-active-"+this._uid+"{fill:url(#"+this._uid+"_SVGID_3_);}</style>"+this.getLinearGradient(this._uid+"_SVGID_1_",this.settings.emptyColor,this.settings.emptyColor)+this.getLinearGradient(this._uid+"_SVGID_2_",this.settings.hoverColor,this.settings.hoverColor)+this.getLinearGradient(this._uid+"_SVGID_3_",this.settings.starGradient.start,this.settings.starGradient.end)+'<polygon data-side="left" class="svg-empty-'+this._uid+'" points="281.1,129.8 364,55.7 255.5,46.8 214,-59 172.5,46.8 64,55.4 146.8,129.7 121.1,241 213.9,181.1 213.9,181 306.5,241 " style="stroke: '+this.settings.strokeColor+'"/><polygon data-side="right" class="svg-empty-'+this._uid+'" points="364,55.7 255.5,46.8 214,-59 213.9,181 306.5,241 281.1,129.8 " style="stroke-dasharray: 230 232 210 0; stroke: '+this.settings.strokeColor+'"/></svg></div>',b="",c=0;c<this.settings.totalStars;c++)b+=a;this.$el.append(b),this.$stars=this.$el.find(".jq-star")},getLinearGradient:function(a,b,c){return'<linearGradient id="'+a+'" gradientUnits="userSpaceOnUse" x1="121.1501" y1="-70.35" x2="121.15" y2="125.0045"><stop offset="0" style="stop-color:'+b+'"/><stop offset="1" style="stop-color:'+c+'"/> </linearGradient>'},executeCallback:function(a,b){var c=this.settings.callback;c(a,b)}},g={unload:function(){var c="plugin_"+b,d=a(this),e=d.data(c).$stars;e.off(),d.removeData(c).remove()},setRating:function(c,d){var e="plugin_"+b,f=a(this),g=f.data(e);c>g.settings.totalStars||0>c||(d&&(c=Math.round(c)),g.applyRating(c))},getRating:function(){var c="plugin_"+b,d=a(this),e=d.data(c);return e._state.rating}};a.extend(e.prototype,f),a.fn[b]=function(c){if(!a.isPlainObject(c)){if(g.hasOwnProperty(c))return g[c].apply(this,Array.prototype.slice.call(arguments,1));a.error("Method "+c+" does not exist on "+b+".js")}return this.each(function(){a.data(this,"plugin_"+b)||a.data(this,"plugin_"+b,new e(this,c))})}}(jQuery,window,document);

3
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs

@ -11,11 +11,14 @@ namespace Volo.CmsKit.GlobalFeatures
public CommentsFeature Comments => GetFeature<CommentsFeature>(); public CommentsFeature Comments => GetFeature<CommentsFeature>();
public RatingsFeature Ratings => GetFeature<RatingsFeature>();
public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager) public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager)
: base(featureManager) : base(featureManager)
{ {
AddFeature(new ReactionsFeature(this)); AddFeature(new ReactionsFeature(this));
AddFeature(new CommentsFeature(this)); AddFeature(new CommentsFeature(this));
AddFeature(new RatingsFeature(this));
} }
} }
} }

18
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/RatingsFeature.cs

@ -0,0 +1,18 @@
using JetBrains.Annotations;
using Volo.Abp.GlobalFeatures;
namespace Volo.CmsKit.GlobalFeatures
{
[GlobalFeatureName(Name)]
public class RatingsFeature : GlobalFeature
{
public const string Name = "CmsKit.Ratings";
internal RatingsFeature(
[NotNull] GlobalCmsKitFeatures cmsKit
) : base(cmsKit)
{
}
}
}

6
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json

@ -13,6 +13,10 @@
"LoginToAddComment": "Login to add comment", "LoginToAddComment": "Login to add comment",
"LoginToReply": "Login to reply", "LoginToReply": "Login to reply",
"MessageDeletionConfirmationMessage": "This comment will be deleted completely.", "MessageDeletionConfirmationMessage": "This comment will be deleted completely.",
"CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display." "CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display.",
"Undo": "Undo",
"RatingUndoMessage": "Your rating will be undo.",
"LoginToRate": "Login to rate",
"Star": "Star"
} }
} }

6
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json

@ -13,6 +13,10 @@
"LoginToAddComment": "Yorum yapmak için giriş yap", "LoginToAddComment": "Yorum yapmak için giriş yap",
"LoginToReply": "Cevap vermek için giriş yap", "LoginToReply": "Cevap vermek için giriş yap",
"MessageDeletionConfirmationMessage": "Bu yorum tamamen silinecektir", "MessageDeletionConfirmationMessage": "Bu yorum tamamen silinecektir",
"CommentAuthorizationExceptionMessage": "Bu yorumları görebilmek için yetki gerekir." "CommentAuthorizationExceptionMessage": "Bu yorumları görebilmek için yetki gerekir.",
"Undo": "Geri al",
"RatingUndoMessage": "Oylamanız geri alınacak.",
"LoginToRate": "Oylamak için giriş yapın",
"Star": "Yıldız"
} }
} }

15
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Ratings/RatingConsts.cs

@ -0,0 +1,15 @@
using Volo.CmsKit.Entities;
namespace Volo.CmsKit.Ratings
{
public static class RatingConsts
{
public static int MaxEntityTypeLength { get; set; } = CmsEntityConsts.MaxEntityTypeLength;
public static int MaxEntityIdLength { get; set; } = CmsEntityConsts.MaxEntityIdLength;
public static int MaxStarCount { get; set; } = 5;
public static int MinStarCount { get; set; } = 1;
}
}

25
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/IRatingRepository.cs

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp.Domain.Repositories;
namespace Volo.CmsKit.Ratings
{
public interface IRatingRepository : IBasicRepository<Rating, Guid>
{
Task<Rating> GetCurrentUserRatingAsync(
[NotNull] string entityType,
[NotNull] string entityId,
Guid userId,
CancellationToken cancellationToken = default
);
Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(
[NotNull] string entityType,
[NotNull] string entityId,
CancellationToken cancellationToken = default
);
}
}

57
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/Rating.cs

@ -0,0 +1,57 @@
using System;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
namespace Volo.CmsKit.Ratings
{
public class Rating : BasicAggregateRoot<Guid>, IHasCreationTime, IMustHaveCreator
{
public virtual Guid? TenantId { get; protected set; }
public virtual string EntityType { get; protected set; }
public virtual string EntityId { get; protected set; }
public virtual short StarCount { get; protected set; }
public virtual Guid CreatorId { get; set; }
public virtual DateTime CreationTime { get; set; }
protected Rating()
{
}
public Rating(
Guid id,
[NotNull] string entityType,
[NotNull] string entityId,
short starCount,
Guid creatorId,
Guid? tenantId = null
)
: base(id)
{
EntityType = Check.NotNullOrWhiteSpace(entityType, nameof(entityType), RatingConsts.MaxEntityTypeLength);
EntityId = Check.NotNullOrWhiteSpace(entityId, nameof(entityId), RatingConsts.MaxEntityIdLength);
SetStarCount(starCount);
CreatorId = creatorId;
TenantId = tenantId;
}
public virtual void SetStarCount(short starCount)
{
if(starCount <= RatingConsts.MaxStarCount && starCount >= RatingConsts.MinStarCount)
{
StarCount = starCount;
}
else
{
throw new ArgumentOutOfRangeException($"Choosen star must between {RatingConsts.MinStarCount} and {RatingConsts.MaxStarCount}");
}
}
}
}

9
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/RatingWithStarCountQueryResultItem.cs

@ -0,0 +1,9 @@
namespace Volo.CmsKit.Ratings
{
public class RatingWithStarCountQueryResultItem
{
public short StarCount { get; set; }
public int Count { get; set; }
}
}

17
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs

@ -8,6 +8,7 @@ using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
using Volo.Abp.Users.EntityFrameworkCore; using Volo.Abp.Users.EntityFrameworkCore;
using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.EntityFrameworkCore namespace Volo.CmsKit.EntityFrameworkCore
{ {
@ -71,6 +72,22 @@ namespace Volo.CmsKit.EntityFrameworkCore
b.HasIndex(x => new { x.TenantId, x.RepliedCommentId }); b.HasIndex(x => new { x.TenantId, x.RepliedCommentId });
}); });
} }
if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
builder.Entity<Rating>(r =>
{
r.ToTable(options.TablePrefix + "Ratings", options.Schema);
r.ConfigureByConvention();
r.Property(x => x.StarCount).IsRequired();
r.Property(x => x.EntityType).IsRequired().HasMaxLength(RatingConsts.MaxEntityTypeLength);
r.Property(x => x.EntityId).IsRequired().HasMaxLength(RatingConsts.MaxEntityIdLength);
r.HasIndex(x => new {x.TenantId, x.EntityType, x.EntityId, x.CreatorId});
});
}
} }
} }
} }

2
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs

@ -3,6 +3,7 @@ using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.Users.EntityFrameworkCore; using Volo.Abp.Users.EntityFrameworkCore;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions; using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
@ -22,6 +23,7 @@ namespace Volo.CmsKit.EntityFrameworkCore
options.AddRepository<CmsUser, EfCoreCmsUserRepository>(); options.AddRepository<CmsUser, EfCoreCmsUserRepository>();
options.AddRepository<UserReaction, EfCoreUserReactionRepository>(); options.AddRepository<UserReaction, EfCoreUserReactionRepository>();
options.AddRepository<Comment, EfCoreCommentRepository>(); options.AddRepository<Comment, EfCoreCommentRepository>();
options.AddRepository<Rating, EfCoreRatingRepository>();
}); });
} }
} }

57
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Ratings/EfCoreRatingRepository.cs

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.CmsKit.EntityFrameworkCore;
namespace Volo.CmsKit.Ratings
{
public class EfCoreRatingRepository : EfCoreRepository<ICmsKitDbContext, Rating, Guid>, IRatingRepository
{
public EfCoreRatingRepository(IDbContextProvider<ICmsKitDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async Task<Rating> GetCurrentUserRatingAsync(string entityType, string entityId, Guid userId,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var rating = await DbSet.FirstOrDefaultAsync(
r => r.EntityType == entityType && r.EntityId == entityId && r.CreatorId == userId,
GetCancellationToken(cancellationToken));
return rating;
}
public async Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(string entityType,
string entityId, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var query = (
from rating in DbSet
where rating.EntityType == entityType && rating.EntityId == entityId
group rating by rating.StarCount
into g
select new RatingWithStarCountQueryResultItem
{
StarCount = g.Key,
Count = g.Count()
}
).OrderByDescending(r => r.StarCount);
var ratings = await query.ToListAsync(GetCancellationToken(cancellationToken));
return ratings;
}
}
}

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContext.cs

@ -2,6 +2,7 @@
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.MongoDB; using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions; using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
@ -16,6 +17,8 @@ namespace Volo.CmsKit.MongoDB
public IMongoCollection<CmsUser> CmsUsers => Collection<CmsUser>(); public IMongoCollection<CmsUser> CmsUsers => Collection<CmsUser>();
public IMongoCollection<Rating> Ratings => Collection<Rating>();
protected override void CreateModel(IMongoModelBuilder modelBuilder) protected override void CreateModel(IMongoModelBuilder modelBuilder)
{ {
base.CreateModel(modelBuilder); base.CreateModel(modelBuilder);

6
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContextExtensions.cs

@ -2,6 +2,7 @@
using Volo.Abp; using Volo.Abp;
using Volo.Abp.MongoDB; using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions; using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
@ -35,6 +36,11 @@ namespace Volo.CmsKit.MongoDB
{ {
x.CollectionName = CmsKitDbProperties.DbTablePrefix + "Comments"; x.CollectionName = CmsKitDbProperties.DbTablePrefix + "Comments";
}); });
builder.Entity<Rating>(x =>
{
x.CollectionName = CmsKitDbProperties.DbTablePrefix + "Ratings";
});
} }
} }
} }

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbModule.cs

@ -4,8 +4,10 @@ using Volo.Abp.MongoDB;
using Volo.Abp.Users.MongoDB; using Volo.Abp.Users.MongoDB;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.MongoDB.Comments; using Volo.CmsKit.MongoDB.Comments;
using Volo.CmsKit.MongoDB.Ratings;
using Volo.CmsKit.MongoDB.Reactions; using Volo.CmsKit.MongoDB.Reactions;
using Volo.CmsKit.MongoDB.Users; using Volo.CmsKit.MongoDB.Users;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions; using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
@ -25,6 +27,7 @@ namespace Volo.CmsKit.MongoDB
options.AddRepository<CmsUser, MongoCmsUserRepository>(); options.AddRepository<CmsUser, MongoCmsUserRepository>();
options.AddRepository<UserReaction, MongoUserReactionRepository>(); options.AddRepository<UserReaction, MongoUserReactionRepository>();
options.AddRepository<Comment, MongoCommentRepository>(); options.AddRepository<Comment, MongoCommentRepository>();
options.AddRepository<Rating, MongoRatingRepository>();
}); });
} }
} }

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/ICmsKitMongoDbContext.cs

@ -2,6 +2,7 @@
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.MongoDB; using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions; using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
@ -15,5 +16,7 @@ namespace Volo.CmsKit.MongoDB
IMongoCollection<Comment> Comments { get; } IMongoCollection<Comment> Comments { get; }
IMongoCollection<CmsUser> CmsUsers { get; } IMongoCollection<CmsUser> CmsUsers { get; }
IMongoCollection<Rating> Ratings { get; }
} }
} }

58
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Ratings/MongoRatingRepository.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.MongoDB.Ratings
{
public class MongoRatingRepository : MongoDbRepository<ICmsKitMongoDbContext, Rating, Guid>, IRatingRepository
{
public MongoRatingRepository(IMongoDbContextProvider<ICmsKitMongoDbContext> dbContextProvider) : base(
dbContextProvider)
{
}
public async Task<Rating> GetCurrentUserRatingAsync(string entityType, string entityId, Guid userId,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var rating = await GetMongoQueryable()
.FirstOrDefaultAsync(r => r.EntityType == entityType && r.EntityId == entityId && r.CreatorId == userId,
GetCancellationToken(cancellationToken));
return rating;
}
public async Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(string entityType,
string entityId, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var query = (
from rating in GetMongoQueryable()
where rating.EntityType == entityType && rating.EntityId == entityId
group rating by rating.StarCount
into g
select new RatingWithStarCountQueryResultItem
{
StarCount = g.Key,
Count = g.Count()
}
).OrderByDescending(r => r.StarCount);
var ratings = await query.ToListAsync(GetCancellationToken(cancellationToken));
return ratings;
}
}
}

30
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo.CmsKit.Public.Application.Contracts.csproj

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" /> <Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" /> <Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace /> <RootNamespace />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" /> <ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

13
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/CreateUpdateRatingInput.cs

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Validation;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.Public.Ratings
{
public class CreateUpdateRatingInput
{
[Required]
[DynamicRange(typeof(RatingConsts), typeof(int), nameof(RatingConsts.MinStarCount), nameof(RatingConsts.MaxStarCount))]
public short StarCount { get; set; }
}
}

19
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/IRatingPublicAppService.cs

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Volo.CmsKit.Public.Ratings
{
public interface IRatingPublicAppService : IApplicationService
{
Task<RatingDto> CreateAsync(string entityType, string entityId, CreateUpdateRatingInput input);
Task DeleteAsync(string entityType, string entityId);
Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId);
Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType, string entityId);
}
}

19
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingDto.cs

@ -0,0 +1,19 @@
using System;
namespace Volo.CmsKit.Public.Ratings
{
public class RatingDto
{
public Guid Id { get; set; }
public string EntityType { get; set; }
public string EntityId { get; set; }
public short StarCount { get; set; }
public Guid CreatorId { get; set; }
public DateTime CreationTime { get; set; }
}
}

11
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingWithStarCountDto.cs

@ -0,0 +1,11 @@
namespace Volo.CmsKit.Public.Ratings
{
public class RatingWithStarCountDto
{
public short StarCount { get; set; }
public int Count { get; set; }
public bool IsSelectedByCurrentUser { get; set; }
}
}

32
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" /> <Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" /> <Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace /> <RootNamespace />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" /> <ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" /> <ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

4
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs

@ -2,6 +2,8 @@
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
using Volo.CmsKit.Comments; using Volo.CmsKit.Comments;
using Volo.CmsKit.Public.Comments; using Volo.CmsKit.Public.Comments;
using Volo.CmsKit.Public.Ratings;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Users; using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public namespace Volo.CmsKit.Public
@ -18,6 +20,8 @@ namespace Volo.CmsKit.Public
CreateMap<Comment, CommentWithDetailsDto>() CreateMap<Comment, CommentWithDetailsDto>()
.Ignore(x=> x.Replies) .Ignore(x=> x.Replies)
.Ignore(x=> x.Author); .Ignore(x=> x.Author);
CreateMap<Rating, RatingDto>();
} }
} }
} }

104
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Ratings/RatingPublicAppService.cs

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.Users;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public.Ratings
{
public class RatingPublicAppService : ApplicationService, IRatingPublicAppService
{
protected IRatingRepository RatingRepository { get; }
public ICmsUserLookupService CmsUserLookupService { get; }
public RatingPublicAppService(IRatingRepository ratingRepository, ICmsUserLookupService cmsUserLookupService)
{
RatingRepository = ratingRepository;
CmsUserLookupService = cmsUserLookupService;
}
[Authorize]
public virtual async Task<RatingDto> CreateAsync(string entityType, string entityId,
CreateUpdateRatingInput input)
{
var userId = CurrentUser.GetId();
var user = await CmsUserLookupService.GetByIdAsync(userId);
var currentUserRating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, userId);
if (currentUserRating != null)
{
currentUserRating.SetStarCount(input.StarCount);
var updatedRating = await RatingRepository.UpdateAsync(currentUserRating);
return ObjectMapper.Map<Rating, RatingDto>(updatedRating);
}
var rating = await RatingRepository.InsertAsync(
new Rating(
GuidGenerator.Create(),
entityType,
entityId,
input.StarCount,
user.Id,
CurrentTenant.Id
)
);
return ObjectMapper.Map<Rating, RatingDto>(rating);
}
[Authorize]
public virtual async Task DeleteAsync(string entityType, string entityId)
{
var rating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, CurrentUser.GetId());
if (rating.CreatorId != CurrentUser.GetId())
{
throw new AbpAuthorizationException();
}
await RatingRepository.DeleteAsync(rating.Id);
}
[Authorize]
public virtual async Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId)
{
var currentUserId = CurrentUser.GetId();
var rating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, currentUserId);
return ObjectMapper.Map<Rating, RatingDto>(rating);
}
public virtual async Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType,
string entityId)
{
var ratings = await RatingRepository.GetGroupedStarCountsAsync(entityType, entityId);
var userRatingOrNull = CurrentUser.IsAuthenticated
? await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, CurrentUser.GetId())
: null;
var ratingWithStarCountDto = new List<RatingWithStarCountDto>();
foreach (var rating in ratings)
{
ratingWithStarCountDto.Add(
new RatingWithStarCountDto
{
StarCount = rating.StarCount,
Count = rating.Count,
IsSelectedByCurrentUser = userRatingOrNull != null && userRatingOrNull.StarCount == rating.StarCount
});
}
return ratingWithStarCountDto;
}
}
}

32
modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo.CmsKit.Public.HttpApi.csproj

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" /> <Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" /> <Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace /> <RootNamespace />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" /> <ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" /> <ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

51
modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Ratings/RatingPublicController.cs

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.GlobalFeatures;
using Volo.CmsKit.GlobalFeatures;
namespace Volo.CmsKit.Public.Ratings
{
[RequiresGlobalFeature(typeof(RatingsFeature))]
[RemoteService(Name = CmsKitPublicRemoteServiceConsts.RemoteServiceName)]
[Area("cms-kit")]
[Route("api/cms-kit-public/ratings")]
public class RatingPublicController : CmsKitPublicControllerBase, IRatingPublicAppService
{
protected IRatingPublicAppService RatingPublicAppService { get; }
public RatingPublicController(IRatingPublicAppService ratingPublicAppService)
{
RatingPublicAppService = ratingPublicAppService;
}
[HttpPut]
[Route("{entityType}/{entityId}")]
public virtual Task<RatingDto> CreateAsync(string entityType, string entityId, CreateUpdateRatingInput input)
{
return RatingPublicAppService.CreateAsync(entityType, entityId, input);
}
[HttpDelete]
[Route("{entityType}/{entityId}")]
public virtual Task DeleteAsync(string entityType, string entityId)
{
return RatingPublicAppService.DeleteAsync(entityType, entityId);
}
[HttpGet]
[Route("{entityType}/{entityId}")]
public virtual Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId)
{
return RatingPublicAppService.GetCurrentUserRatingAsync(entityType, entityId);
}
[HttpGet]
[Route("{entityType}/{entityId}")]
public virtual Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType, string entityId)
{
return RatingPublicAppService.GetGroupedStarCountsAsync(entityType, entityId);
}
}
}

6
modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting; using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection; using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection;
namespace Volo.CmsKit.Public.Web.Controllers namespace Volo.CmsKit.Public.Web.Controllers
@ -16,5 +17,10 @@ namespace Volo.CmsKit.Public.Web.Controllers
{ {
return Task.FromResult((IActionResult)ViewComponent(typeof(CommentingViewComponent), new {entityType, entityId})); return Task.FromResult((IActionResult)ViewComponent(typeof(CommentingViewComponent), new {entityType, entityId}));
} }
public Task<IActionResult> Rating(string entityType, string entityId)
{
return Task.FromResult((IActionResult) ViewComponent(typeof(RatingViewComponent), new {entityType, entityId}));
}
} }
} }

2
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml

@ -175,7 +175,7 @@
</div> </div>
</div> </div>
} }
@if (comment.Replies.Count >= 5) @if (comment.Replies.Count >= 3)
{ {
<div class="text-right mt-4"> <div class="text-right mt-4">
@if (CurrentUser.IsAuthenticated) @if (CurrentUser.IsAuthenticated)

46
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/Default.cshtml

@ -0,0 +1,46 @@
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Users
@using Volo.CmsKit.Localization
@inject ICurrentUser CurrentUser
@model Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating.RatingViewModel
@inject IHtmlLocalizer<CmsKitResource> L
<div class="cms-rating-area container" data-entity-type="@Model.EntityType" data-entity-id="@Model.EntityId" id="cms-rating_{@Model.EntityType}_{@Model.EntityId}">
@if (CurrentUser.IsAuthenticated)
{
<div class="m-auto">
<span class="my-rating-5" data-rating="@(Model.CurrentRating ?? 0)" data-authenticated="@(Model.CurrentRating != null)"></span>
<span class="live-rating">@(Model.CurrentRating != null ? Model.CurrentRating + " | " : 0 + "")</span>
@if (Model.CurrentRating != null)
{
<a href="#" class="rating-undo-link m-auto">
<i class="fa fa-undo"></i> @L["Undo"]
</a>
}
</div>
if (Model.Ratings != null)
{
<div class="row text-center col-6 mt-3">
@foreach (var rating in Model.Ratings)
{
<div class="side col-2">
<div>@rating.StarCount @L["Star"]</div>
</div>
<div class="middle col-8">
<div class="bar-container">
<div class="bar bar-@rating.StarCount" style="width: @(rating.Count * 100 / Model.TotalRating)%"></div>
</div>
</div>
<div class="side col-2">
<div>@rating.Count</div>
</div>
}
</div>
}
}
else
{
<span class="my-rating-5" data-authenticated="True" data-toggle="popover" data-placement="right" data-html="true" data-content="<div class='text-center'><a href='@Model.LoginUrl' class='btn btn-primary btn-block'>@L["LoginToRate"]</a></div>"></span>
<span class="rating-login"></span>
}
</div>

16
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingScriptBundleContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg;
using Volo.Abp.Modularity;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[DependsOn(typeof(StarRatingSvgScriptContributor))]
public class RatingScriptBundleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Rating/default.js");
}
}
}

16
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingStyleBundleContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg;
using Volo.Abp.Modularity;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[DependsOn(typeof(StarRatingSvgStyleContributor))]
public class RatingStyleBundleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Rating/default.css");
}
}
}

75
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingViewComponent.cs

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI;
using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
using Volo.Abp.Users;
using Volo.CmsKit.Public.Ratings;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[ViewComponent(Name = "CmsRating")]
[Widget(
StyleTypes = new[] {typeof(RatingStyleBundleContributor)},
ScriptTypes = new[] {typeof(RatingScriptBundleContributor)},
RefreshUrl = "/CmsKitPublicWidgets/Rating"
)]
public class RatingViewComponent : AbpViewComponent
{
public IRatingPublicAppService RatingPublicAppService { get; }
public AbpMvcUiOptions AbpMvcUiOptions { get; }
public ICurrentUser CurrentUser { get; }
public RatingViewComponent(IRatingPublicAppService ratingPublicAppService, IOptions<AbpMvcUiOptions> options, ICurrentUser currentUser)
{
RatingPublicAppService = ratingPublicAppService;
AbpMvcUiOptions = options.Value;
CurrentUser = currentUser;
}
public virtual async Task<IViewComponentResult> InvokeAsync(string entityType, string entityId)
{
var ratings = await RatingPublicAppService.GetGroupedStarCountsAsync(entityType, entityId);
var totalRating = ratings.Sum(x => x.Count);
short? currentUserRating = null;
if (CurrentUser.IsAuthenticated)
{
currentUserRating = ratings.Find(x => x.IsSelectedByCurrentUser)?.StarCount;
}
var loginUrl =
$"{AbpMvcUiOptions.LoginUrl}?returnUrl={HttpContext.Request.Path.ToString()}&returnUrlHash=#cms-rating_{entityType}_{entityId}";
var viewModel = new RatingViewModel
{
EntityId = entityId,
EntityType = entityType,
LoginUrl = loginUrl,
Ratings = ratings,
CurrentRating = currentUserRating,
TotalRating = totalRating
};
return View("~/Pages/CmsKit/Shared/Components/Rating/Default.cshtml", viewModel);
}
}
public class RatingViewModel
{
public string EntityType { get; set; }
public string EntityId { get; set; }
public string LoginUrl { get; set; }
public List<RatingWithStarCountDto> Ratings { get; set; }
public short? CurrentRating { get; set; }
public int TotalRating { get; set; }
}
}

44
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.css

@ -0,0 +1,44 @@
.side {
width: 15%;
margin-top:10px;
}
.middle {
float: left;
width: 70%;
margin-top:10px;
padding-top: 4px;
}
.row:after {
content: "";
display: table;
clear: both;
}
.bar-container {
width: 100%;
background-color: #f1f1f1;
text-align: center;
color: white;
}
.bar {
height: 18px;
}
/* Individual bars */
.bar-5 { background-color: #173F5F; }
.bar-4 { background-color: #20639B; }
.bar-3 { background-color: #3CAEA3; }
.bar-2 { background-color: #F6D55C; }
.bar-1 { background-color: #ED553B; }
@media (max-width: 400px) {
.side, .middle {
width: 100%;
}
.right {
display: none;
}
}

87
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.js

@ -0,0 +1,87 @@
(function () {
var l = abp.localization.getResource("CmsKit");
$(document).ready(function () {
abp.widgets.CmsRating = function ($widget) {
var widgetManager = $widget.data("abp-widget-manager");
var $ratingArea = $widget.find(".cms-rating-area");
function getFilters() {
return {
entityType: $ratingArea.attr("data-entity-type"),
entityId: $ratingArea.attr("data-entity-id")
};
}
function registerCreateOfNewRating() {
$widget.find(".my-rating-5").each(function () {
var authenticated = $(this).attr("data-authenticated");
$(this).starRating({
initialRating: 0,
disableAfterRate: true,
useFullStars: true,
readOnly: authenticated === "True",
onHover: function (currentIndex, currentRating, $el) {
$widget.find(".live-rating").text(currentIndex);
},
onLeave: function (currentIndex, currentRating, $el) {
$widget.find(".live-rating").text(currentRating);
},
callback: function (currentRating, $el) {
volo.cmsKit.public.ratings.ratingPublic.create(
$ratingArea.attr("data-entity-type"),
$ratingArea.attr("data-entity-id"),
{
starCount: parseInt(currentRating)
}
).then(function () {
widgetManager.refresh($widget);
})
}
});
}
);
}
function registerUndoLink() {
$widget.find(".rating-undo-link").each(function () {
$(this).on('click', '', function (e) {
e.preventDefault();
abp.message.confirm(l("RatingUndoMessage"), function (ok) {
if (ok) {
volo.cmsKit.public.ratings.ratingPublic.delete(
$ratingArea.attr("data-entity-type"),
$ratingArea.attr("data-entity-id")
).then(function () {
widgetManager.refresh($widget);
});
}
})
});
});
}
function init() {
registerCreateOfNewRating();
registerUndoLink();
}
return {
init: init,
getFilters: getFilters
}
};
$('.abp-widget-wrapper[data-widget-name="CmsRating"]')
.each(function () {
var widgetManager = new abp.WidgetManager({
wrapper: $(this),
});
widgetManager.init($(this));
});
});
})
(jQuery);

137
modules/cms-kit/test/Volo.CmsKit.Application.Tests/Ratings/RatingPublicAppService_Tests.cs

@ -0,0 +1,137 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Volo.Abp.Users;
using Volo.CmsKit.Public.Ratings;
using Xunit;
namespace Volo.CmsKit.Ratings
{
public class RatingPublicAppService_Tests : CmsKitApplicationTestBase
{
private readonly IRatingPublicAppService _ratingAppService;
private ICurrentUser _currentUser;
private readonly CmsKitTestData _cmsKitTestData;
public RatingPublicAppService_Tests()
{
_ratingAppService = GetRequiredService<IRatingPublicAppService>();
_cmsKitTestData = GetRequiredService<CmsKitTestData>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
_currentUser = Substitute.For<ICurrentUser>();
services.AddSingleton(_currentUser);
}
[Fact]
public async Task CreateAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var newRating = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId2,
new CreateUpdateRatingInput
{
StarCount = 4
});
UsingDbContext(context =>
{
var ratings = context.Set<Rating>().Where(x =>
x.EntityId == _cmsKitTestData.EntityId1 && x.EntityType == _cmsKitTestData.EntityType1).ToList();
ratings
.Any(c => c.Id == newRating.Id && c.CreatorId == newRating.CreatorId &&
c.StarCount == newRating.StarCount)
.ShouldBeTrue();
});
}
[Fact]
public async Task CreateAsync_Should_Update_If_Rating_Is_Exist()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var entity =
await _ratingAppService.GetCurrentUserRatingAsync(_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1);
var updatedEntity = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateUpdateRatingInput
{
StarCount = 5
});
entity.Id.ShouldBe(updatedEntity.Id);
entity.EntityId.ShouldBe(updatedEntity.EntityId);
entity.EntityType.ShouldBe(updatedEntity.EntityType);
entity.StarCount.ShouldBe(updatedEntity.StarCount);
}
[Fact]
public async Task GetCurrentUserRatingAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var rating = await _ratingAppService.GetCurrentUserRatingAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1
);
UsingDbContext(context =>
{
var ratings = context.Set<Rating>().Where(x =>
x.EntityId == _cmsKitTestData.EntityId1 && x.EntityType == _cmsKitTestData.EntityType1).ToList();
ratings
.Any(c => c.Id == rating.Id && c.EntityId == rating.EntityId && c.EntityType == rating.EntityType)
.ShouldBeTrue();
});
}
[Fact]
public async Task GetGroupedStarCountsAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var ratings = await _ratingAppService.GetGroupedStarCountsAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1
);
ratings.ShouldNotBeNull();
ratings.Count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task DeleteAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var rating = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateUpdateRatingInput
{
StarCount = 4
});
await _ratingAppService.DeleteAsync(_cmsKitTestData.EntityType1, _cmsKitTestData.EntityId1);
UsingDbContext(context =>
{
var deletedComment = context.Set<Rating>().FirstOrDefault(x => x.Id == rating.Id);
deletedComment.ShouldBeNull();
});
}
}
}

9
modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/Ratings/RatingRepository_Tests.cs

@ -0,0 +1,9 @@
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.EntityFrameworkCore.Ratings
{
public class RatingRepository_Tests : RatingRepository_Tests<CmsKitEntityFrameworkCoreTestModule>
{
}
}

11
modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/Ratings/RatingRepository_Tests.cs

@ -0,0 +1,11 @@
using Volo.CmsKit.Ratings;
using Xunit;
namespace Volo.CmsKit.MongoDB.Ratings
{
[Collection(MongoTestCollection.Name)]
public class RatingRepository_Tests : RatingRepository_Tests<CmsKitMongoDbTestModule>
{
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save