@ -0,0 +1,98 @@ |
|||
# Send Real-time Notifications via SignalR in ABP Project |
|||
|
|||
SignalR is an open source library that adds real-time operation functionality to applications. Real-time web functionality enables server-side code to instantly send content to clients without refreshing the page. I'll show you how to add SignalR and use it to send notifications from backend. I'll implement this functionality in MVC template of ABP Framework. |
|||
|
|||
 |
|||
|
|||
## Implement Backend |
|||
|
|||
### Create Notification Hub |
|||
|
|||
Create a new folder named `SignalR` in your root directory of your Web project. |
|||
|
|||
 |
|||
|
|||
Then add the following classes to the folder: |
|||
|
|||
1. [INotificationClient.cs](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-inotificationclient-cs) |
|||
2. [UiNotificationClient.cs](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-uinotificationclient-cs) |
|||
3. [UiNotificationHub.cs](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-uinotificationhub-cs) |
|||
|
|||
### Configure Module |
|||
|
|||
These 3 steps will be done in your web module class. |
|||
|
|||
#### 1- Add SignalR |
|||
|
|||
Open `YourProjectWebModule.cs` class and add the following line to the `PreConfigureServices` method: |
|||
|
|||
```csharp |
|||
context.Services.AddSignalR(); |
|||
``` |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
#### 2- Add Client Scripts |
|||
|
|||
2- In the `ConfigureServices` method of your web module add the following code to add the `signalr.js` and `notification-hub.js`. We'll add these packages in the next steps. |
|||
|
|||
 |
|||
|
|||
#### 3- Add Hub Endpoint |
|||
|
|||
Add the following code to add the notification hub endpoint in `OnApplicationInitialization` method: |
|||
|
|||
```csharp |
|||
app.UseEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapHub<UiNotificationHub>("/notification-hub"); |
|||
}); |
|||
``` |
|||
|
|||
 |
|||
|
|||
### Implement Frontend |
|||
|
|||
We'll write the client-side code to be able to handle the SignalR response. |
|||
|
|||
#### 1- Add Notification Hub |
|||
|
|||
Add the following JavaScript class into your `Pages` folder in your Web project. We already added this script to our global scripts. |
|||
|
|||
[notification-hub.js](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-notification-hub-js) |
|||
|
|||
 |
|||
|
|||
#### 2- Add SignalR NPM package |
|||
|
|||
Add [Microsoft.SignalR](https://www.npmjs.com/package/@microsoft/signalr) JavaScript package to the `package.json` which is located in your root folder of the Web project. After you add it, run `yarn` command in your Web directory to be able to install this package. |
|||
|
|||
 |
|||
|
|||
#### 3- Add resource Mapping |
|||
|
|||
We added SignalR to the `package.json` but it comes into your `node_modules` folder. We need to copy the related files to `wwwroot/libs` folder. To do this copy the content of the following file to your `abp.resourcemappings.js` file. It's in your root directory of Web folder. After you do this, go to your web directory and run `gulp` command. By doing this, it'll copy the related files into your `wwwroot/libs` folder. |
|||
|
|||
[abp.resourcemappings.js](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-abp-resourcemapping-js) |
|||
|
|||
 |
|||
|
|||
#### 4- Usage |
|||
|
|||
We have completed the implementation part. Let's check if it's running... |
|||
|
|||
To do this easily, open your `Index.cshtml` which is in the Pages folder of your Web project. And replace the content with the following. Also replace the `Index.cshtml.cs` as well. |
|||
|
|||
[Index.cshtml](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-index-cshtml) |
|||
|
|||
[Index.cshtml.cs](https://gist.github.com/ebicoglu/f7dc22cca2d353f8bf7f68a03e3395b8#file-index-cshtml-cs) |
|||
|
|||
#### 5- See it in action |
|||
|
|||
Run your web project and in the Index page you'll see a button named as "Get Notification". Click the button and see the notification that comes from SignalR. This is a basic usage of SignalR notification system. You can implement it according to your own requirements. |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 75 KiB |
@ -0,0 +1,90 @@ |
|||
# Angular UI v4.3 Migration Guide |
|||
|
|||
## Breaking Changes |
|||
|
|||
### Manage Profile Page |
|||
|
|||
Before v4.3, the "Manage Your Profile" link in the current user dropdown on the top bar redirected the user to MVC's profile management page. As of v4.3, the same link will land on a page in the Angular UI account module instead. So you have to install and implement the account module to your Angular project when you update the ABP to v4.3. |
|||
|
|||
#### Account Module Implementation |
|||
|
|||
Install the `@abp/ng.account` NPM package by running the below command: |
|||
|
|||
```bash |
|||
npm install @abp/ng.account@next |
|||
``` |
|||
|
|||
> Make sure v4.3-rc or higher version is installed. |
|||
|
|||
Open the `app.module.ts` and add `AccountConfigModule.forRoot()` to the imports array as shown below: |
|||
|
|||
```js |
|||
// app.module.ts |
|||
|
|||
import { AccountConfigModule } from '@abp/ng.account/config'; |
|||
//... |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
//... |
|||
AccountConfigModule.forRoot() |
|||
], |
|||
//... |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: |
|||
|
|||
```js |
|||
// app-routing.module.ts |
|||
const routes: Routes = [ |
|||
//... |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import('@abp/ng.account').then(m => m.AccountModule.forLazy()), |
|||
}, |
|||
//... |
|||
export class AppRoutingModule {} |
|||
``` |
|||
|
|||
#### Account Module Implementation for Commercial Templates |
|||
|
|||
The pro startup template comes with `@volo/abp.ng.account` package. You should update the package version to v4.3-rc or higher version. The package can be updated by running the following command: |
|||
|
|||
```bash |
|||
npm install @volo/abp.ng.account@next |
|||
``` |
|||
> Make sure v4.3-rc or higher version is installed. |
|||
|
|||
`AccountConfigModule` is already imported to `app.module.ts` in the startup template. So no need to import the module to the `AppModule`. If you removed the `AccountConfigModule` from the `AppModule`, you can import it as shown below: |
|||
|
|||
```js |
|||
// app.module.ts |
|||
|
|||
import { AccountConfigModule } from '@volo/abp.ng.account/config'; |
|||
//... |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
//... |
|||
AccountConfigModule.forRoot() |
|||
], |
|||
//... |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: |
|||
|
|||
```js |
|||
// app-routing.module.ts |
|||
const routes: Routes = [ |
|||
//... |
|||
{ |
|||
path: 'account', |
|||
loadChildren: () => import('@volo/abp.ng.account').then(m => m.AccountPublicModule.forLazy()), |
|||
}, |
|||
//... |
|||
export class AppRoutingModule {} |
|||
``` |
|||
@ -0,0 +1,5 @@ |
|||
# ABP v4.3 Migration Guide |
|||
|
|||
## Angular UI |
|||
|
|||
See the [Angular UI Migration Guide](Abp-4_3-Angular.md). |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"culture": "zh-Hant", |
|||
"texts": { |
|||
"Volo.Authorization:010001": "認證失敗! Given policy has not granted.", |
|||
"Volo.Authorization:010002": "認證失敗! Given policy has not granted: {PolicyName}", |
|||
"Volo.Authorization:010003": "認證失敗! Given policy has not granted for given resource: {ResourceName}", |
|||
"Volo.Authorization:010004": "認證失敗! Given requirement has not granted for given resource: {ResourceName}", |
|||
"Volo.Authorization:010005": "認證失敗! Given requirements has not granted for given resource: {ResourceName}" |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps |
|||
{ |
|||
public class RemoveProjectFromPrometheusStep : ProjectBuildPipelineStep |
|||
{ |
|||
private readonly string _name; |
|||
|
|||
public RemoveProjectFromPrometheusStep(string name) |
|||
{ |
|||
_name = name; |
|||
} |
|||
|
|||
public override void Execute(ProjectBuildContext context) |
|||
{ |
|||
var tyeFile = context.Files.FirstOrDefault(f => f.Name == "/etc/prometheus/prometheus.yml"); |
|||
|
|||
if (tyeFile == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var lines = tyeFile.GetLines(); |
|||
var newLines = new List<string>(); |
|||
|
|||
var nameLine = $"- job_name:"; |
|||
var isOneOfTargetLines = false; |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
if (line.Trim().Equals($"{nameLine} '{_name}'")) |
|||
{ |
|||
isOneOfTargetLines = true; |
|||
continue; |
|||
} |
|||
|
|||
if (line.Trim().StartsWith(nameLine)) |
|||
{ |
|||
isOneOfTargetLines = false; |
|||
} |
|||
|
|||
if (!isOneOfTargetLines) |
|||
{ |
|||
newLines.Add(line); |
|||
} |
|||
} |
|||
|
|||
tyeFile.SetContent(String.Join(Environment.NewLine, newLines)); |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"culture": "zh-Hant", |
|||
"texts": { |
|||
"Volo.Feature:010001": "功能尚未啟用: {FeatureName}", |
|||
"Volo.Feature:010002": "請求的功能尚未啟用。這些功能被須全部啟用: {FeatureNames}", |
|||
"Volo.Feature:010003": "請求的功能尚未啟用。這些功能至少需啟用一個: {FeatureNames}" |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"culture": "zh-Hant", |
|||
"texts": { |
|||
"hello": "嗨" |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "zh-Hant", |
|||
"texts": { |
|||
"HelloText": "嗨 {0}", |
|||
"HowAreYou": "你好嗎?" |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
{ |
|||
"culture": "zh-Hant", |
|||
"texts": { |
|||
"BlogDeletionConfirmationMessage": "部落格 '{0}' 將被刪除. 你確定嗎?", |
|||
"BlogFeatureNotAvailable": "目前此功能不可使用. 請用 `GlobalFeatureManager` 來啟用它.", |
|||
"BlogId": "部落格", |
|||
"BlogPostDeletionConfirmationMessage": "部落格貼文 '{0}' 將被刪除. 你確定嗎?", |
|||
"BlogPosts": "部落格貼文", |
|||
"Blogs": "部落格", |
|||
"ChoosePreference": "選擇 Preference...", |
|||
"Cms": "Cms", |
|||
"CmsKit.Comments": "評論", |
|||
"CmsKit.Ratings": "評分", |
|||
"CmsKit.Reactions": "反應", |
|||
"CmsKit.Tags": "標籤", |
|||
"CmsKit:0002": "內容已經存在!", |
|||
"CmsKit:0003": "實體 {0} 不可標記.", |
|||
"CmsKit:Blog:0001": "給定的slug ({Slug}) 已經存在!", |
|||
"CmsKit:BlogPost:0001": "給定的slug已經存在!", |
|||
"CmsKit:Comments:0001": "實體不可 {0} 不可評論.", |
|||
"CmsKit:Media:0001": "'{Name}' 不是有效的媒體名稱.", |
|||
"CmsKit:Media:0002": "實體不可以含有媒體", |
|||
"CmsKit:Page:0001": "給定的url ({0}) 已經存在.", |
|||
"CmsKit:Reaction:0001": "實體 {EntityType} 不能有反應", |
|||
"CmsKit:Tag:0002": "實體不可標記!", |
|||
"CommentAuthorizationExceptionMessage": "些評論不允許公開顯示", |
|||
"CommentDeletionConfirmationMessage": "此評論和所有回覆將被刪除!", |
|||
"Comments": "評論", |
|||
"ContentDeletionConfirmationMessage": "你確定要刪除這個內容嗎?", |
|||
"Contents": "內容", |
|||
"CoverImage": "封面圖片", |
|||
"CreateBlogPostPage": "新部落格貼文", |
|||
"CreationTime": "建立時間", |
|||
"Delete": "刪除", |
|||
"Detail": "詳情", |
|||
"Details": "詳情", |
|||
"DoYouPreferAdditionalEmails": "你是否更喜歡額外的郵件", |
|||
"Edit": "修改", |
|||
"EndDate": "結束時間", |
|||
"EntityId": "實體Id", |
|||
"EntityType": "實體類型", |
|||
"ExportCSV": "匯出 CSV", |
|||
"Features": "功能", |
|||
"GenericDeletionConfirmationMessage": "你確定刪除 '{0}' 嗎?", |
|||
"LastModification": "最後一次修改", |
|||
"LoginToAddComment": "登錄後添加評論", |
|||
"LoginToRate": "登錄後進行評分", |
|||
"LoginToReply": "登錄後進行回覆", |
|||
"Menu:CMS": "CMS", |
|||
"Message": "消息", |
|||
"MessageDeletionConfirmationMessage": "這條評論將被完全刪除", |
|||
"Name": "名稱", |
|||
"New": "新", |
|||
"OK": "好", |
|||
"PageDeletionConfirmationMessage": "你確定刪除這個頁面嗎?", |
|||
"PageSlugInformation": "Slug用於網址. 你的網址將是 '/pages/{{slug}}'.", |
|||
"Permission:BlogManagement": "部落格管理", |
|||
"Permission:BlogManagement.Create": "創建", |
|||
"Permission:BlogManagement.Delete": "刪除", |
|||
"Permission:BlogManagement.Features": "功能", |
|||
"Permission:BlogManagement.Update": "更新", |
|||
"Permission:BlogPostManagement": "部落格貼文管理", |
|||
"Permission:BlogPostManagement.Create": "創建", |
|||
"Permission:BlogPostManagement.Delete": "刪除", |
|||
"Permission:BlogPostManagement.Update": "更新", |
|||
"Permission:CmsKit": "Cms工具包", |
|||
"Permission:Comments": "評論管理", |
|||
"Permission:Comments.Delete": "刪除", |
|||
"Permission:Contents": "內容管理", |
|||
"Permission:Contents.Create": "創建內容", |
|||
"Permission:Contents.Delete": "刪除內容", |
|||
"Permission:Contents.Update": "更新內容", |
|||
"Permission:MediaDescriptorManagement": "媒體管理", |
|||
"Permission:MediaDescriptorManagement:Create": "創建", |
|||
"Permission:MediaDescriptorManagement:Delete": "刪除", |
|||
"Permission:PageManagement": "頁面管理", |
|||
"Permission:PageManagement:Create": "創建", |
|||
"Permission:PageManagement:Delete": "刪除", |
|||
"Permission:PageManagement:Update": "更新", |
|||
"Permission:TagManagement": "標籤管理", |
|||
"Permission:TagManagement.Create": "創建", |
|||
"Permission:TagManagement.Delete": "刪除", |
|||
"Permission:TagManagement.Update": "更新", |
|||
"PickYourReaction": "選擇你的回應", |
|||
"RatingUndoMessage": "您的評分將被收回", |
|||
"Read": "閱讀", |
|||
"RepliesToThisComment": "回覆此評論", |
|||
"Reply": "回覆", |
|||
"ReplyTo": "回覆給", |
|||
"SamplePageMessage": "Pro模組的展示頁面", |
|||
"SaveChanges": "保存更改", |
|||
"SelectAll": "選擇全部", |
|||
"Send": "發送", |
|||
"SendMessage": "發送消息", |
|||
"ShortDescription": "簡介", |
|||
"Slug": "Slug", |
|||
"Source": "來源", |
|||
"SourceUrl": "來源 Url", |
|||
"Star": "星", |
|||
"StartDate": "開始時間", |
|||
"Subject": "主題", |
|||
"SubjectPlaceholder": "請輸入主題", |
|||
"Submit": "提交", |
|||
"Subscribe": "訂閱", |
|||
"SuccessfullyDeleted": "刪除成功!", |
|||
"SuccessfullySaved": "保存成功!", |
|||
"TagDeletionConfirmationMessage": "你確定刪除 '{0}' 標籤嗎?", |
|||
"Tags": "標籤", |
|||
"Text": "文本", |
|||
"ThankYou": "謝謝你", |
|||
"Title": "標題", |
|||
"Undo": "復原", |
|||
"Update": "更新", |
|||
"UpdatePreferenceSuccessMessage": "您的 preferences 已經保存", |
|||
"UpdateYourEmailPreferences": "更新你的郵件preferences", |
|||
"UploadFailedMessage": "上傳失敗", |
|||
"UserId": "用戶Id", |
|||
"Username": "用戶名稱", |
|||
"YourComment": "你的評論", |
|||
"YourEmailAddress": "你的郵件地址", |
|||
"YourFullName": "你的全名", |
|||
"YourMessage": "你的消息", |
|||
"YourReply": "你的回覆" |
|||
} |
|||
} |
|||
@ -1,17 +1,7 @@ |
|||
using JetBrains.Annotations; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.CmsKit.Comments; |
|||
|
|||
namespace Volo.CmsKit.Comments |
|||
namespace Volo.CmsKit.Comments |
|||
{ |
|||
public interface ICommentEntityTypeDefinitionStore |
|||
public interface ICommentEntityTypeDefinitionStore : IEntityTypeDefinitionStore<CommentEntityTypeDefinition> |
|||
{ |
|||
Task<CommentEntityTypeDefinition> GetDefinitionAsync([NotNull] string entityType); |
|||
|
|||
Task<bool> IsDefinedAsync([NotNull] string entityType); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,22 @@ |
|||
using JetBrains.Annotations; |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace Volo.CmsKit |
|||
{ |
|||
public abstract class EntityTypeDefinition : IEquatable<EntityTypeDefinition> |
|||
{ |
|||
public EntityTypeDefinition([NotNull] string entityType) |
|||
{ |
|||
EntityType = Check.NotNullOrEmpty(entityType, nameof(entityType)); |
|||
} |
|||
|
|||
[NotNull] |
|||
public string EntityType { get; protected set; } |
|||
|
|||
public bool Equals(EntityTypeDefinition other) |
|||
{ |
|||
return EntityType == other?.EntityType; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using JetBrains.Annotations; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.CmsKit |
|||
{ |
|||
public interface IEntityTypeDefinitionStore<TPolicyDefinition> : ITransientDependency |
|||
where TPolicyDefinition : EntityTypeDefinition |
|||
{ |
|||
Task<TPolicyDefinition> GetAsync([NotNull] string entityType); |
|||
|
|||
Task<bool> IsDefinedAsync([NotNull] string entityType); |
|||
} |
|||
} |
|||
@ -1,12 +1,7 @@ |
|||
using JetBrains.Annotations; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.CmsKit.MediaDescriptors |
|||
namespace Volo.CmsKit.MediaDescriptors |
|||
{ |
|||
public interface IMediaDescriptorDefinitionStore |
|||
public interface IMediaDescriptorDefinitionStore : IEntityTypeDefinitionStore<MediaDescriptorDefinition> |
|||
{ |
|||
Task<bool> IsDefinedAsync([NotNull] string entityType); |
|||
|
|||
Task<MediaDescriptorDefinition> GetDefinitionAsync([NotNull] string entityType); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,11 @@ |
|||
using JetBrains.Annotations; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class CmsKitRatingOptions |
|||
{ |
|||
[NotNull] |
|||
public List<RatingEntityTypeDefinition> EntityTypes { get; } = new (); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class DefaultRatingEntityTypeDefinitionStore : IRatingEntityTypeDefinitionStore |
|||
{ |
|||
protected CmsKitRatingOptions Options { get; } |
|||
|
|||
public DefaultRatingEntityTypeDefinitionStore(IOptions<CmsKitRatingOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public virtual Task<RatingEntityTypeDefinition> GetAsync([NotNull] string entityType) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(entityType, nameof(entityType)); |
|||
|
|||
var definition = Options.EntityTypes.SingleOrDefault(x => x.EntityType.Equals(entityType, StringComparison.InvariantCultureIgnoreCase)) ?? |
|||
throw new EntityCantHaveRatingException(entityType); |
|||
|
|||
return Task.FromResult(definition); |
|||
} |
|||
|
|||
public virtual Task<bool> IsDefinedAsync([NotNull] string entityType) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(entityType, nameof(entityType)); |
|||
|
|||
var isDefined = Options.EntityTypes.Any(x => x.EntityType.Equals(entityType, StringComparison.InvariantCultureIgnoreCase)); |
|||
|
|||
return Task.FromResult(isDefined); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using JetBrains.Annotations; |
|||
using System.Runtime.Serialization; |
|||
using Volo.Abp; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class EntityCantHaveRatingException : BusinessException |
|||
{ |
|||
public EntityCantHaveRatingException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
|||
{ |
|||
} |
|||
|
|||
public EntityCantHaveRatingException([NotNull] string entityType) |
|||
{ |
|||
Code = CmsKitErrorCodes.Ratings.EntityCantHaveRating; |
|||
EntityType = Check.NotNullOrEmpty(entityType, nameof(entityType)); |
|||
WithData(nameof(EntityType), EntityType); |
|||
} |
|||
|
|||
public string EntityType { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.CmsKit.Ratings; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public interface IRatingEntityTypeDefinitionStore : IEntityTypeDefinitionStore<RatingEntityTypeDefinition> |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class RatingEntityTypeDefinition : EntityTypeDefinition |
|||
{ |
|||
public RatingEntityTypeDefinition( |
|||
[NotNull] string entityType) : base(entityType) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Services; |
|||
using Volo.CmsKit.Users; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class RatingManager : DomainService |
|||
{ |
|||
protected IRatingRepository RatingRepository { get; } |
|||
protected IRatingEntityTypeDefinitionStore RatingDefinitionStore { get; } |
|||
|
|||
public RatingManager( |
|||
IRatingRepository ratingRepository, |
|||
IRatingEntityTypeDefinitionStore ratingDefinitionStore) |
|||
{ |
|||
RatingRepository = ratingRepository; |
|||
RatingDefinitionStore = ratingDefinitionStore; |
|||
} |
|||
|
|||
public async Task<Rating> SetStarAsync(CmsUser user, string entityType, string entityId, short starCount) |
|||
{ |
|||
var currentUserRating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, user.Id); |
|||
|
|||
if (currentUserRating != null) |
|||
{ |
|||
currentUserRating.SetStarCount(starCount); |
|||
|
|||
return await RatingRepository.UpdateAsync(currentUserRating); |
|||
} |
|||
|
|||
if (!await RatingDefinitionStore.IsDefinedAsync(entityType)) |
|||
{ |
|||
throw new EntityCantHaveRatingException(entityType); |
|||
} |
|||
|
|||
return await RatingRepository.InsertAsync( |
|||
new Rating( |
|||
GuidGenerator.Create(), |
|||
entityType, |
|||
entityId, |
|||
starCount, |
|||
user.Id, |
|||
CurrentTenant.Id |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
@model Volo.CmsKit.Public.Blogs.BlogPostPublicDto |
|||
@using Volo.CmsKit.Localization |
|||
@using Microsoft.Extensions.Localization |
|||
|
|||
@inject IStringLocalizer<CmsKitResource> L |
|||
|
|||
@{ |
|||
const string dummyImageSource = "https://dummyimage.com/300x200/a3a3a3/fff.png"; |
|||
} |
|||
|
|||
<abp-card> |
|||
<img src="/api/cms-kit/media/@Model.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'" /> |
|||
<abp-card-body> |
|||
<abp-card-title>@Model.Title</abp-card-title> |
|||
<abp-card-subtitle>@Model.Author?.UserName</abp-card-subtitle> |
|||
|
|||
@Html.Raw(Model.Content) |
|||
</abp-card-body> |
|||
<abp-card-footer> |
|||
<abp-card-subtitle>@Model.CreationTime</abp-card-subtitle> |
|||
@if (Model.LastModificationTime != null) |
|||
{ |
|||
<abp-card-text><i>@L["LastModification"].Value : @Model.LastModificationTime</i></abp-card-text> |
|||
} |
|||
</abp-card-footer> |
|||
</abp-card> |
|||
|
|||
|
|||
@ -1,29 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.CmsKit.Public.Blogs; |
|||
|
|||
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Blogs.BlogPost |
|||
{ |
|||
[ViewComponent(Name = "CmsDefaultBlogPost")] |
|||
public class DefaultBlogPostViewComponent : AbpViewComponent |
|||
{ |
|||
protected IBlogPostPublicAppService BlogPostPublicAppService { get; } |
|||
|
|||
public DefaultBlogPostViewComponent(IBlogPostPublicAppService blogPostPublicAppService) |
|||
{ |
|||
BlogPostPublicAppService = blogPostPublicAppService; |
|||
} |
|||
|
|||
public virtual async Task<IViewComponentResult> InvokeAsync(string blogSlug, string blogPostSlug) |
|||
{ |
|||
var blogPost = await BlogPostPublicAppService.GetAsync(blogSlug, blogPostSlug); |
|||
|
|||
return View("~/Pages/CmsKit/Shared/Components/Blogs/BlogPost/Default.cshtml", blogPost); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
|
|||
.reaction-in-comment span.area-title { |
|||
display: none; |
|||
} |
|||
|
|||
span.area-title { |
|||
font-weight: 600; |
|||
padding: 3px; |
|||
display: block; |
|||
} |
|||
|
|||
.popover { |
|||
min-width: 276px; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
.card-body img { |
|||
max-width: 100%; |
|||
border-radius: 4px; |
|||
margin: 20px 0; |
|||
} |
|||
|
|||
.badge.badge-light { |
|||
color: #757575; |
|||
background: #f2f2f2; |
|||
} |
|||
|
|||
.reaction-in-comment span.area-title { |
|||
display: none; |
|||
} |
|||
|
|||
span.area-title { |
|||
font-weight: 600; |
|||
padding: 3px; |
|||
display: block; |
|||
} |
|||
.popover { |
|||
min-width: 276px; |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
.card-img-top { |
|||
} |
|||
|
|||
.popover { |
|||
min-width: 276px; |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
.card-img-top { |
|||
} |
|||
|
|||
.popover { |
|||
min-width: 276px; |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:60873/", |
|||
"sslPort": 44300 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"Volo.CmsKit.Public.Web": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
}, |
|||
"applicationUrl": "https://localhost:5001;http://localhost:5000" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using Shouldly; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.CmsKit.Blogs; |
|||
using Volo.CmsKit.Users; |
|||
using Xunit; |
|||
|
|||
namespace Volo.CmsKit.Ratings |
|||
{ |
|||
public class RatingManager_Test : CmsKitDomainTestBase |
|||
{ |
|||
private readonly CmsKitTestData _cmsKitTestData; |
|||
private readonly RatingManager _ratingManager; |
|||
private readonly ICmsUserRepository _userRepository; |
|||
|
|||
public RatingManager_Test() |
|||
{ |
|||
_cmsKitTestData = GetRequiredService<CmsKitTestData>(); |
|||
_ratingManager = GetRequiredService<RatingManager>(); |
|||
_userRepository = GetRequiredService<ICmsUserRepository>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetStarAsync_ShouldCreate_WhenFirstCall() |
|||
{ |
|||
var user = await _userRepository.GetAsync(_cmsKitTestData.User1Id); |
|||
short starCount = 4; |
|||
|
|||
var rating = await _ratingManager.SetStarAsync(user, _cmsKitTestData.EntityType1, _cmsKitTestData.BlogPost_1_Id.ToString(), starCount); |
|||
|
|||
rating.ShouldNotBeNull(); |
|||
rating.Id.ShouldNotBe(Guid.Empty); |
|||
rating.StarCount.ShouldBe(starCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetStarAsync_ShouldUpdate_WithExistingRating() |
|||
{ |
|||
var user = await _userRepository.GetAsync(_cmsKitTestData.User1Id); |
|||
short starCount = 2; |
|||
|
|||
var rating = await _ratingManager.SetStarAsync(user, _cmsKitTestData.EntityType1, _cmsKitTestData.EntityId1, starCount); |
|||
|
|||
rating.ShouldNotBeNull(); |
|||
rating.Id.ShouldNotBe(Guid.Empty); |
|||
rating.StarCount.ShouldBe(starCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetStarAsync_ShouldThrowException_WithNotConfiguredentityType() |
|||
{ |
|||
var user = await _userRepository.GetAsync(_cmsKitTestData.User1Id); |
|||
var notConfiguredEntityType = "AnyOtherEntityType"; |
|||
short starCount = 3; |
|||
|
|||
var exception = await Should.ThrowAsync<EntityCantHaveRatingException>(async () => |
|||
await _ratingManager.SetStarAsync(user, notConfiguredEntityType, "1", starCount)); |
|||
|
|||
exception.ShouldNotBeNull(); |
|||
exception.EntityType.ShouldBe(notConfiguredEntityType); |
|||
} |
|||
} |
|||
} |
|||