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
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/cache@v2
with:
node-version: '12.x'
- run: yarn && yarn ci
path: 'npm/ng-packs/node_modules'
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
- 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/setup-dotnet@master
with:
dotnet-version: 3.1.100
dotnet-version: 3.1.102
- name: Build All
run: .\build-all.ps1

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

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

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

@ -78,7 +78,6 @@
"Biography": "Biography",
"HasNoPublishedArticlesYet": "has no published articles yet",
"Author": "Author",
"MyAccount": "My account",
"LatestGithubAnnouncements": "Latest Github Announcements",
"SeeAllAnnouncements": "See All Announcements",
"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!
> 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.
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.
### 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
abp.auth.isGranted('MyPermissionName');
```
### Angular UI
See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI.
## Permission Management
Permission management is normally done by an admin user using the permission management modal:
@ -374,7 +386,7 @@ Configure<AbpPermissionOptions>(options =>
### 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

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.
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)
@ -40,13 +40,13 @@ CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
### 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)
### 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)
@ -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**
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)
@ -64,18 +64,18 @@ After successful code generation, you'll see the the user can be picked from use
## 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
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
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!

447
docs/en/Features.md

@ -1,3 +1,448 @@
# 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
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

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

@ -1,3 +1,5 @@
# 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

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

@ -1,3 +1,5 @@
# 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

3
docs/en/Multi-Tenancy.md

@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.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`.
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
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 { Store } from '@ngxs/store'; // imported Store
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store // injected Store
)
{
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
}),
);
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
this.replaceableComponents.add({
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:
```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 { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store, // injected Store
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
}),
);
this.replaceableComponents.add({
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
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: LogoComponent,
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:
```js
import { ABP, ReplaceableComponents } from '@abp/ng.core';
import { ABP } from '@abp/ng.core';
import {
Component,
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:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: RoutesComponent,
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:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: NavItemsComponent,
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:
```js
import { AddReplaceableComponent } from '@abp/ng.core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component';
//...
export class AppComponent implements OnInit {
constructor(private store: Store) {} // injected store
constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
// added dispatching the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement,
})
);
});
}
}
```

4
docs/en/docs-nav.json

@ -170,6 +170,10 @@
"text": "Settings",
"path": "Settings.md"
},
{
"text": "Features",
"path": "Features.md"
},
{
"text": "Data Filtering",
"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.Extensions.FileProviders;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers;
using Volo.Abp.Guids;
using Volo.Abp.VirtualFileSystem;
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; }
private readonly IVirtualFileProvider _virtualFileProvider;
private readonly IGuidGenerator _guidGenerator;
public AbpComponentDemoSectionTagHelper(IVirtualFileProvider virtualFileProvider)
public AbpComponentDemoSectionTagHelper(
IVirtualFileProvider virtualFileProvider,
IGuidGenerator guidGenerator)
{
_virtualFileProvider = virtualFileProvider;
_guidGenerator = guidGenerator;
}
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();
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section\">");
output.PreContent.AppendHtml($"<h2>{Title}</h2>");
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section-body\">");
var previewId = _guidGenerator.Create();
var codeBlockId = _guidGenerator.Create();
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 */
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 class=\"abp-component-demo-section-bs-source\">");
output.PostContent.AppendHtml("<h3>Bootstrap</h3>");
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(content.GetContent());
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}\">");
/* CodeBlock tabs */
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("</div>");
}
private void AppendRawSource(TagHelperOutput output)
{
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-raw-source\">");
output.PostContent.AppendHtml("<h3>ABP Tag Helpers</h3>");
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"bootstrap-{bootstrapCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"bootstrap-tab-{bootstrapCodeBlockId}\">");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("</code>");
output.PostContent.AppendHtml("</pre>");
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()

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();
if (exception is IUserFriendlyException)
{
errorInfo.Message = exception.Message;
@ -101,6 +101,8 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
errorInfo.Message = L["InternalServerErrorMessage"];
}
errorInfo.Data = exception.Data;
return errorInfo;
}
@ -190,7 +192,6 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
return exception;
}
protected virtual RemoteServiceErrorInfo CreateDetailedErrorInfoFromException(Exception exception)
{
var detailBuilder = new StringBuilder();
@ -213,7 +214,7 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling
detailBuilder.AppendLine(exception.GetType().Name + ": " + exception.Message);
//Additional info for UserFriendlyException
if (exception is IUserFriendlyException &&
if (exception is IUserFriendlyException &&
exception is IHasErrorDetails)
{
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);
}
public IReadOnlyList<FeatureGroupDefinition> GetGroups()
{
return FeatureGroupDefinitions.Values.ToImmutableList();
}
protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions()
{
var features = new Dictionary<string, FeatureDefinition>();
@ -78,7 +83,7 @@ namespace Volo.Abp.Features
}
protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features,
Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature)
{
if (features.ContainsKey(feature.Name))
@ -114,4 +119,4 @@ namespace Volo.Abp.Features
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();
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)
{
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.Collections;
using System.Collections.Generic;
namespace Volo.Abp.Http
{
@ -23,6 +25,8 @@ namespace Volo.Abp.Http
/// </summary>
public string Details { get; set; }
public IDictionary Data { get; set; }
/// <summary>
/// Validation errors if exists.
/// </summary>
@ -49,4 +53,4 @@ namespace Volo.Abp.Http
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" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Volo\Abp\Http\Localization\en.json" />
</ItemGroup>
</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.Http.Client;
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.TestApp;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Http
{
@ -22,6 +26,24 @@ namespace Volo.Abp.Http
{
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 GetException2Async();
Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1);
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!");
}
[HttpGet]
[Route("get-exception2")]
public Task GetException2Async()
{
throw new BusinessException("Volo.Abp.Http.DynamicProxying:10001")
.WithData("0","TEST");
}
[HttpGet]
[Route("get-with-datetime-parameter")]
public Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1)
@ -133,4 +141,4 @@ namespace Volo.Abp.Http.DynamicProxying
[FromQuery]
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.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Shouldly;
using Volo.Abp.Http.Client;
using Volo.Abp.Http.Localization;
using Volo.Abp.Localization;
using Xunit;
@ -32,6 +34,13 @@ namespace Volo.Abp.Http.DynamicProxying
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]
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.Text;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering
{
[Collection(MongoTestCollection.Name)]
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.",
"YourPasswordIsSuccessfullyReset": "Your password is successfully reset.",
"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.",
"YourPasswordIsSuccessfullyReset": "Şifreniz başarıyla sıfırlandı.",
"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.Identity;
using AutoMapper;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
namespace Volo.Abp.Account.Web
{
@ -8,7 +9,7 @@ namespace Volo.Abp.Account.Web
{
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.Extensions.DependencyInjection;
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.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars;
using Volo.Abp.AutoMapper;
@ -50,16 +53,38 @@ namespace Volo.Abp.Account.Web
options.Contributors.Add(new AccountModuleToolbarContributor());
});
ConfigureProfileManagementPage();
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true);
});
}
private void ConfigureProfileManagementPage()
{
Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/Account/Manage");
});
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
Configure<ProfileManagementPageOptions>(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 Volo.Abp.Account.Localization
@using Volo.Abp.Account.Web.Pages.Account
@using Volo.Abp.Identity.Settings
@using Volo.Abp.Settings
@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@inject ISettingProvider SettingManager
@inject IThemeManager ThemeManager
@inject IHtmlLocalizer<AccountResource> L
@model ManageModel
@{
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 {
<abp-script-bundle name="@typeof(ManageModel).FullName">
<abp-script src="/Pages/Account/Manage.js" />
</abp-script-bundle>
<abp-script-bundle name="@typeof(ManageModel).FullName"/>
}
<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"/>
<abp-input asp-for="PersonalSettingsInfoModel.PhoneNumber"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>
</abp-tab>
</abp-tabs>
</abp-card-body>
</abp-card>
<div id="ProfileManagementWrapper">
<abp-card>
<abp-card-body>
<abp-tabs tab-style="PillVertical" vertical-header-size="_3">
@foreach (var group in Model.ProfileManagementPageCreationContext.Groups)
{
<abp-tab title="@group.DisplayName">
<h2>@group.DisplayName</h2>
<hr class="my-4" />
@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 Volo.Abp.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account
{
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 bool HideOldPasswordInput { get; set; }
protected IProfileAppService ProfileAppService { get; }
public ManageModel(IProfileAppService profileAppService)
public ManageModel(IOptions<ProfileManagementPageOptions> options)
{
ProfileAppService = profileAppService;
Options = options.Value;
}
public virtual async Task<IActionResult> OnGetAsync()
{
var user = await ProfileAppService.GetAsync();
PersonalSettingsInfoModel = ObjectMapper.Map<ProfileDto, PersonalSettingsInfoModel>(user);
ProfileManagementPageCreationContext = new ProfileManagementPageCreationContext(ServiceProvider);
DisablePasswordChange = user.IsExternal;
HideOldPasswordInput = !user.HasPassword;
foreach (var contributor in Options.Contributors)
{
await contributor.ConfigureAsync(ProfileManagementPageCreationContext);
}
return Page();
}
@ -40,50 +36,4 @@ namespace Volo.Abp.Account.Web.Pages.Account
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
{
protected IAccountAppService AccountAppService { get; }
[BindProperty(SupportsGet = true)]
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.Security.Claims;
using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.DataProtection;
@ -108,12 +109,12 @@ namespace Volo.CmsKit
AbpClaimTypes.Role = JwtClaimTypes.Role;
AbpClaimTypes.Email = JwtClaimTypes.Email;
context.Services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = "CmsKit";
options.Audience = "CmsKit";
});
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.Sinks.File" Version="4.1.0" />
<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.EntityFrameworkCore.Tools" Version="3.1.6" />
<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()
.AddIdentityServerAuthentication(options =>
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = configuration["AuthServer:ApiName"];
options.Audience = configuration["AuthServer:ApiName"];
});
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.Sinks.File" Version="4.1.0" />
<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.EntityFrameworkCore.Tools" Version="3.1.6" />
<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.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
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.Pages
@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
@model IndexModel
@inject IStringLocalizer<AbpUiResource> Localizer
@ -41,6 +42,12 @@
</abp-card-body>
</abp-card>
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new { entityType = "quote", entityId = "2" })
}
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "2" })
@ -49,4 +56,4 @@
{
@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 RatingsFeature Ratings => GetFeature<RatingsFeature>();
public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager)
: base(featureManager)
{
AddFeature(new ReactionsFeature(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",
"LoginToReply": "Login to reply",
"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",
"LoginToReply": "Cevap vermek için giriş yap",
"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.Abp.Users.EntityFrameworkCore;
using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.EntityFrameworkCore
{
@ -71,6 +72,22 @@ namespace Volo.CmsKit.EntityFrameworkCore
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.Users.EntityFrameworkCore;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -22,6 +23,7 @@ namespace Volo.CmsKit.EntityFrameworkCore
options.AddRepository<CmsUser, EfCoreCmsUserRepository>();
options.AddRepository<UserReaction, EfCoreUserReactionRepository>();
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.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -16,6 +17,8 @@ namespace Volo.CmsKit.MongoDB
public IMongoCollection<CmsUser> CmsUsers => Collection<CmsUser>();
public IMongoCollection<Rating> Ratings => Collection<Rating>();
protected override void CreateModel(IMongoModelBuilder 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.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -35,6 +36,11 @@ namespace Volo.CmsKit.MongoDB
{
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.CmsKit.Comments;
using Volo.CmsKit.MongoDB.Comments;
using Volo.CmsKit.MongoDB.Ratings;
using Volo.CmsKit.MongoDB.Reactions;
using Volo.CmsKit.MongoDB.Users;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -25,6 +27,7 @@ namespace Volo.CmsKit.MongoDB
options.AddRepository<CmsUser, MongoCmsUserRepository>();
options.AddRepository<UserReaction, MongoUserReactionRepository>();
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.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -15,5 +16,7 @@ namespace Volo.CmsKit.MongoDB
IMongoCollection<Comment> Comments { 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">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" />
</ItemGroup>
</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">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</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.CmsKit.Comments;
using Volo.CmsKit.Public.Comments;
using Volo.CmsKit.Public.Ratings;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public
@ -18,6 +20,8 @@ namespace Volo.CmsKit.Public
CreateMap<Comment, CommentWithDetailsDto>()
.Ignore(x=> x.Replies)
.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">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</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 Microsoft.AspNetCore.Mvc;
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;
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}));
}
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>
}
@if (comment.Replies.Count >= 5)
@if (comment.Replies.Count >= 3)
{
<div class="text-right mt-4">
@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