diff --git a/Library.Encyclopedia.API/.config/dotnet-tools.json b/Library.Encyclopedia.API/.config/dotnet-tools.json new file mode 100644 index 0000000..0d1da73 --- /dev/null +++ b/Library.Encyclopedia.API/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.1", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/Library.Encyclopedia.API/Attributes/ApiKeyAttribute.cs b/Library.Encyclopedia.API/Attributes/ApiKeyAttribute.cs index 924a61a..a3ee159 100644 --- a/Library.Encyclopedia.API/Attributes/ApiKeyAttribute.cs +++ b/Library.Encyclopedia.API/Attributes/ApiKeyAttribute.cs @@ -7,6 +7,10 @@ namespace Library.Encyclopedia.API.Attributes { + /// + /// This is a custom attribute for API Kay validation + /// Extracts API key from the header and then checks it with the API KEY hardcoded in appsettings + /// [AttributeUsage(validOn: AttributeTargets.Class | AttributeTargets.Method)] public class ApiKeyAttribute : Attribute, IAsyncActionFilter { diff --git a/Library.Encyclopedia.API/Controllers/CommentController.cs b/Library.Encyclopedia.API/Controllers/CommentController.cs new file mode 100644 index 0000000..7152f3d --- /dev/null +++ b/Library.Encyclopedia.API/Controllers/CommentController.cs @@ -0,0 +1,197 @@ +using Library.Encyclopedia.DataAccess; +using Library.Encyclopedia.DataAccess.DataAccess; +using Library.Encyclopedia.Entity.Interfaces; +using Library.Encyclopedia.Entity.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.DirectoryServices.AccountManagement; +using System.Linq; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class CommentController : ControllerBase + { + private readonly ILogger _logger; + private readonly ICommentsDataAccess dataAccess; + private readonly ILikesDataAccess likesDataAccess; + + public CommentController(ILogger logger, IApplicationDbContext dbContext) + { + this._logger = logger; + dataAccess = new CommentsDataAccess(dbContext); + likesDataAccess = new LikesDataAccess(dbContext); + } + + [HttpGet] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Get(int offset = 0, int limit = 10, bool asc = false) + { + try + { + var result = await dataAccess.Get(offset, limit, asc); + var likes = await likesDataAccess.GetLikes(result.Result.Select(s => s.Id).ToList()); + + if (likes != null) + { + foreach (var item in result.Result) + item.Likes = likes.ContainsKey(item.Id) ? likes[item.Id] : 0; + } + + if (result != null) + return Ok(result); + else + return StatusCode(500); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpPost("Resolve/{id}")] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task ResolveComment(Guid id, bool flag = true) + { + try + { + await dataAccess.Resolve(id, flag); + + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpGet("{id}")] + public async Task Get(Guid id) + { + try + { + var result = await dataAccess.Get(id); + var likes = await likesDataAccess.GetLikes(result.Select(s => s.Id).ToList()); + + if (likes != null) + { + foreach (var item in result) + item.Likes = likes.ContainsKey(item.Id) ? likes[item.Id] : 0; + } + + if (result != null) + return Ok(result); + else + return StatusCode(500); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpPost] + [Authorize] + public async Task Post([FromBody]Comments comments) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + comments.UserName = usr.DisplayName; + comments.UserSid = usr.Sid.Value; + + var result = await dataAccess.CreateComment(comments); + + if (result != null) + return Ok(result); + else + return StatusCode(500); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpDelete("{id}")] + [Authorize] + public async Task Delete(Guid id) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + + if (usr.GetAuthorizationGroups().Select(s => s.Name).Contains("EncyclopediaAdministrators")) + await dataAccess.DeleteComment(id, string.Empty, true); + else + await dataAccess.DeleteComment(id, usr.Sid.Value); + + return Ok(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpPut("{id}")] + [Authorize] + public async Task Update(Guid id, string comment) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + + await dataAccess.UpdateComment(id, comment, usr.Sid.Value); + + return Ok(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpPost("Like/{id}")] + [Authorize] + public async Task LikeComment(Guid id, bool flag = true) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + + await likesDataAccess.Like(id, usr.Sid.Value, flag); + + return Ok(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + } +} diff --git a/Library.Encyclopedia.API/Controllers/EmailController.cs b/Library.Encyclopedia.API/Controllers/EmailController.cs new file mode 100644 index 0000000..f4fd07b --- /dev/null +++ b/Library.Encyclopedia.API/Controllers/EmailController.cs @@ -0,0 +1,48 @@ +using Library.Encyclopedia.DataAccess.Email; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.DirectoryServices.AccountManagement; +using System.Linq; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class EmailController : ControllerBase + { + public readonly EmailAdapter adapter; + private readonly ILogger logger; + + public EmailController(ILogger logger, IConfiguration configuration) + { + adapter = new EmailAdapter(configuration); + this.logger = logger; + } + + [HttpPost] + [Authorize] + public async Task SendEmail([FromBody]string content) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + await adapter.SendEmail(usr.DisplayName, usr.EmailAddress, content); + + return Ok(); + } + } + catch (Exception ex) + { + logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + } +} diff --git a/Library.Encyclopedia.API/Controllers/EncylopediaController.cs b/Library.Encyclopedia.API/Controllers/EncylopediaController.cs index 8d264b4..360e975 100644 --- a/Library.Encyclopedia.API/Controllers/EncylopediaController.cs +++ b/Library.Encyclopedia.API/Controllers/EncylopediaController.cs @@ -1,56 +1,91 @@ -using Library.Encyclopedia.API.Attributes; -using Library.Encyclopedia.DataAccess; +using Library.Encyclopedia.DataAccess; using Library.Encyclopedia.DataAccess.DataAccess; +using Library.Encyclopedia.DataAccess.QueryStatsAccess; using Library.Encyclopedia.Entity.Interfaces; using Library.Encyclopedia.Entity.Models; using Library.Encyclopedia.Entity.Models.External; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; +using System.DirectoryServices.AccountManagement; using System.Threading.Tasks; namespace Library.Encyclopedia.Controllers { /// - /// CRUD operations on Main Table + /// Controller to perform CRUD operations on Main Table /// [ApiController] [Route("[controller]")] public class EncylopediaController : ControllerBase { + #region PRIVATE FIELDS private readonly ILogger _logger; private readonly IMainDataAccess mainDataAccess; + #endregion + #region CONSTRUCTOR /// - /// Constructor + /// Contructor for EncylopediaController /// - /// - /// - public EncylopediaController(ILogger logger, IApplicationDbContext dbContext) + /// logger instance + /// db instance + /// appsettings configuration + /// file upload/download/delete layer + public EncylopediaController(ILogger logger, IApplicationDbContext dbContext, IConfiguration configuration, IFilesAdapter filesAdapter) { _logger = logger; - this.mainDataAccess = new MainDataAccess(dbContext); + this.mainDataAccess = new MainDataAccess(dbContext, configuration, filesAdapter); + } + #endregion + + #region LOGIN + /// + /// Login to perform POST/PUT/DELETE operations on the database + /// + /// + [HttpGet("Login")] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public IActionResult Login() + { + try + { + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + return Ok(usr.DisplayName); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } } + #endregion + #region GET /// /// Get all items based on search query /// /// /// - /// + /// /// /// [HttpGet] public async Task Get(string query, int offset = 0, - int size = 10, + int limit = 10, + int previewSize = 50, bool asc = true) { try { - var response = await mainDataAccess.GetAsync(query, offset, size, asc); + var response = await mainDataAccess.GetAsync(query, offset, limit, previewSize, asc); if (response == null) { return StatusCode(204); @@ -72,18 +107,19 @@ public async Task Get(string query, /// /// /// - /// + /// /// /// [HttpGet("category")] public async Task GetByCategory(string category, int offset = 0, - int size = 10, + int limit = 10, int previewSize = 50, + bool asc = true) { try { - var response = await mainDataAccess.GetByCategoryAsync(category, offset, size, asc); + var response = await mainDataAccess.GetByCategoryAsync(category, offset, limit, previewSize, asc); if (response == null) { @@ -106,18 +142,18 @@ public async Task GetByCategory(string category, /// /// /// - /// + /// /// /// [HttpGet("alphabet")] public async Task GetByStartingAlphabet(char alphabet, int offset = 0, - int size = 10, + int limit = 10, int previewSize = 50, bool asc = true) { try { - var response = await mainDataAccess.GetByAlphabetAsync(alphabet, offset, size, asc); + var response = await mainDataAccess.GetByAlphabetAsync(alphabet, offset, limit, previewSize, asc); if (response == null) { @@ -163,21 +199,19 @@ public async Task Get(Guid id) } /// - /// Create new entry + /// Get all categories /// /// - [HttpPost] - [ApiKey] - //[Authorize] - public async Task Create([FromBody]Main model) + [HttpGet("fetchallcategories")] + public async Task GetAllCategories() { try { - var response = await mainDataAccess.CreateAsync(model); + var response = await mainDataAccess.GetAllCategoriesAsync(); if (response == null) { - return StatusCode(500); + return StatusCode(204); } else { @@ -190,21 +224,36 @@ public async Task Create([FromBody]Main model) throw; } } + #endregion + #region POST /// /// Create new entry /// /// - [HttpPut("{id}")] - [ApiKey] - //[Authorize] - public async Task Update(Guid id, [FromBody]MainUpdateModel model) + [HttpPost] + //[ApiKey] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Create([FromBody]Main model) { try { - await mainDataAccess.UpdateAsync(id, model); + using (var context = new PrincipalContext(ContextType.Domain | ContextType.Machine)) + { + var usr = UserPrincipal.FindByIdentity(context, this.HttpContext.User.Identity.Name); + model.Author = usr.DisplayName; - return Ok(); + var response = await mainDataAccess.CreateAsync(model); + + if (response == null) + { + return StatusCode(500); + } + else + { + return Ok(response); + } + } } catch (Exception ex) { @@ -212,21 +261,30 @@ public async Task Update(Guid id, [FromBody]MainUpdateModel model throw; } } + #endregion + #region PUT /// /// Create new entry /// /// - [HttpDelete("{id}")] - [ApiKey] - //[Authorize] - public async Task Delete(Guid id) + [HttpPut("{id}")] + //[ApiKey] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Update(Guid id, [FromBody]MainUpdateModel model) { try { - await mainDataAccess.DeleteAsync(id); + var response = await mainDataAccess.UpdateAsync(id, model); - return Ok(); + if (response == null) + { + return StatusCode(500); + } + else + { + return Ok(response); + } } catch (Exception ex) { @@ -234,32 +292,30 @@ public async Task Delete(Guid id) throw; } } + #endregion + #region DELETE /// - /// Get all categories + /// Create new entry /// /// - [HttpGet("fetchallcategories")] - public async Task GetAllCategories() + [HttpDelete("{id}")] + //[ApiKey] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Delete(Guid id) { try { - var response = await mainDataAccess.GetAllCategoriesAsync(); + await mainDataAccess.DeleteAsync(id); - if (response == null) - { - return StatusCode(204); - } - else - { - return Ok(response); - } + return Ok(); } catch (Exception ex) { _logger.LogError(ex, $"an error has occured {ex.Message}"); throw; } - } + } + #endregion } } \ No newline at end of file diff --git a/Library.Encyclopedia.API/Controllers/FilesController.cs b/Library.Encyclopedia.API/Controllers/FilesController.cs index a98ffcb..7a711fa 100644 --- a/Library.Encyclopedia.API/Controllers/FilesController.cs +++ b/Library.Encyclopedia.API/Controllers/FilesController.cs @@ -3,8 +3,10 @@ using Library.Encyclopedia.DataAccess.DataAccess; using Library.Encyclopedia.Entity.Interfaces; using Library.Encyclopedia.Entity.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -20,12 +22,14 @@ public class FilesController : ControllerBase { private readonly ILogger _logger; private readonly IFilesAdapter filesAdapter; + private readonly IConfiguration configuration; private readonly IFilesDataAccess filesDataAccess; - public FilesController(ILogger logger, IApplicationDbContext dbContext, IFilesAdapter filesAdapter) + public FilesController(ILogger logger, IApplicationDbContext dbContext, IFilesAdapter filesAdapter, IConfiguration configuration) { _logger = logger; this.filesAdapter = filesAdapter; + this.configuration = configuration; this.filesDataAccess = new FilesDataAccess(dbContext); } @@ -38,38 +42,35 @@ public FilesController(ILogger logger, IApplicationDbContext db /// /// [HttpPost("{id}")] - [ApiKey] - public async Task Post(Guid id, List files, [FromForm] Dictionary descriptions) + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Post(Guid id, IFormFile file, string description) { try { // Store files at remote location - var responses = filesAdapter.UploadFiles(id, files); + var response = await filesAdapter.UploadFile(id, file); - List fileResponses = new List(); - - // filter out the failed uploads - for (int i = 0; i < responses.Count(); i++) + if (response) { - if (responses.ToList()[i]) + string fileNameWithoutExt = Path.GetFileNameWithoutExtension(file.FileName); + Files file_entry = new Files { - string fileNameWithoutExt = Path.GetFileNameWithoutExtension(files[i].FileName); - fileResponses.Add(new Files - { - Id = Guid.NewGuid(), - MainId = id, - FileName = files[i].FileName, - FilePath = $@"/encyclopedia-media/{id}/{files[i].FileName}", - FileType = files[i].ContentType, - FileDescription = descriptions.ContainsKey(fileNameWithoutExt) ? descriptions[fileNameWithoutExt] : null - }); - } - } + Id = Guid.NewGuid(), + MainId = id, + FileName = file.FileName, + FilePath = $@"/{id}/{file.FileName}", + FileSize = file.Length, + FileType = file.ContentType, + FileDescription = description + }; - // Save to database - await filesDataAccess.AddFiles(fileResponses); + // Save to database + await filesDataAccess.AddFiles(new List { file_entry }); - return Ok(fileResponses); + return Ok(file_entry); + } + else + return BadRequest(); } catch (Exception ex) { @@ -79,7 +80,7 @@ public async Task Post(Guid id, List files, [FromForm] } /// - /// Upload Files + /// Download Files /// /// /// @@ -87,14 +88,84 @@ public async Task Post(Guid id, List files, [FromForm] /// /// [HttpGet("{id}")] - [ApiKey] - public async Task Get(Guid id, string directory) + public IActionResult Get(string id, string name, string contentType) + { + try + { + var stream = this.filesAdapter.DownloadFile(id, name); + return File(stream, contentType); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + /// + /// Create Viewable link + /// + /// + /// + /// + /// + /// + [HttpGet("CreateViewableLink/{id}")] + public IActionResult CreateViewableLink(string id, string name) + { + try + { + this.filesAdapter.CreateViewableLink(id, name); + var baseUrl = configuration.GetSection("TempAssetsBaseUrl").Value; + return Ok(baseUrl + '/' + id + '/' + name); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + /// + /// Destroy Viewable link + /// + /// + /// + /// + /// + /// + [HttpDelete("DestroyViewableLink/{id}")] + public IActionResult DestroyViewableLink(string id, string name) + { + try + { + this.filesAdapter.DestroyViewableLink(id, name); + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + /// + /// Delete File + /// + /// + /// + /// + /// + /// + [HttpDelete("{id}")] + [Authorize(Roles = @"EncyclopediaAdministrators")] + public async Task Delete(string id, string fileName, string fileId) { try { - var files = await filesDataAccess.GetFiles(id); - var fileResponses = filesAdapter.DownloadFiles(files, directory); - return Ok(fileResponses); + this.filesAdapter.DeleteFile(id, fileName); + await filesDataAccess.RemoveFiles(new List { Guid.Parse(fileId) }); + return Ok(); } catch (Exception ex) { diff --git a/Library.Encyclopedia.API/Controllers/StatisticsController.cs b/Library.Encyclopedia.API/Controllers/StatisticsController.cs new file mode 100644 index 0000000..8d5c0e9 --- /dev/null +++ b/Library.Encyclopedia.API/Controllers/StatisticsController.cs @@ -0,0 +1,68 @@ +using Library.Encyclopedia.DataAccess; +using Library.Encyclopedia.DataAccess.QueryStatsAccess; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class StatisticsController : ControllerBase + { + private readonly ILogger _logger; + private readonly CommonlyOccuringWordsAdapter commonlyOccuringWordsAdapter; + + public StatisticsController(ILogger logger, IApplicationDbContext applicationDbContext, IMemoryCache memoryCache) + { + _logger = logger; + this.commonlyOccuringWordsAdapter = new CommonlyOccuringWordsAdapter(applicationDbContext, memoryCache); + } + + [HttpGet("GetRecommendedWords")] + public IActionResult GetCommonlyOccuringWordsStats(int count = 10) + { + try + { + return Ok(commonlyOccuringWordsAdapter.GetCommonlyUsedWords(count)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpGet("GetPopularWords")] + public IActionResult GetMostSearchedWordStats(int count = 10) + { + try + { + return Ok(commonlyOccuringWordsAdapter.GetCommonlySearchedWords(count)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + + [HttpGet("GetNLPResult")] + public IActionResult GetNLPResult() + { + try + { + return Ok(commonlyOccuringWordsAdapter.GetNLPResult()); + } + catch (Exception ex) + { + _logger.LogError(ex, $"an error has occured {ex.Message}"); + throw; + } + } + } +} diff --git a/Library.Encyclopedia.API/Library.Encyclopedia.API.csproj b/Library.Encyclopedia.API/Library.Encyclopedia.API.csproj index 5216164..37b1980 100644 --- a/Library.Encyclopedia.API/Library.Encyclopedia.API.csproj +++ b/Library.Encyclopedia.API/Library.Encyclopedia.API.csproj @@ -4,6 +4,10 @@ netcoreapp3.1 + + + + diff --git a/Library.Encyclopedia.API/Program.cs b/Library.Encyclopedia.API/Program.cs index 8b36f8c..abca967 100644 --- a/Library.Encyclopedia.API/Program.cs +++ b/Library.Encyclopedia.API/Program.cs @@ -21,6 +21,16 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }) + .ConfigureLogging((logging)=> { + // clear default logging providers + logging.ClearProviders(); + + // add built-in providers manually, as needed + logging.AddConsole(); + logging.AddDebug(); + logging.AddEventLog(); + logging.AddEventSourceLogger(); + }); } } diff --git a/Library.Encyclopedia.API/Properties/launchSettings.json b/Library.Encyclopedia.API/Properties/launchSettings.json index 3984a6a..95445d5 100644 --- a/Library.Encyclopedia.API/Properties/launchSettings.json +++ b/Library.Encyclopedia.API/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "iisSettings": { - "windowsAuthentication": false, + "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:63327", diff --git a/Library.Encyclopedia.API/Startup.cs b/Library.Encyclopedia.API/Startup.cs index 5510e7b..96f30d8 100644 --- a/Library.Encyclopedia.API/Startup.cs +++ b/Library.Encyclopedia.API/Startup.cs @@ -1,8 +1,11 @@ using Library.Encyclopedia.DataAccess; using Library.Encyclopedia.DataAccess.FileAccess; +using Library.Encyclopedia.DataAccess.QueryStatsAccess; using Library.Encyclopedia.Entity.Interfaces; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IISIntegration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -23,27 +26,53 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddLogging(); services.AddCors(options => { options.AddPolicy(name: MyAllowSpecificOrigins, builder => { - builder.WithOrigins("http://localhost:4200").AllowAnyHeader(); - builder.WithOrigins("https://tools.library.pfw.edu").AllowAnyHeader(); + builder.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowCredentials().AllowAnyMethod(); + builder.WithOrigins("https://tools.library.pfw.edu").AllowAnyHeader().AllowCredentials().AllowAnyMethod(); }); }); - //services.AddAuthentication(IISDefaults.AuthenticationScheme); + services.AddAuthentication(IISDefaults.AuthenticationScheme); + services.AddControllers(); - string url = Configuration.GetValue("FTPSettings:url"); - string userName = Configuration.GetValue("FTPSettings:username"); - string password = Configuration.GetValue("FTPSettings:password"); + if (Configuration.GetValue("FileAdapterSettings:type").Equals("linux", System.StringComparison.OrdinalIgnoreCase)) + { + string url = Configuration.GetValue("FileAdapterSettings:url"); + string userName = Configuration.GetValue("FileAdapterSettings:username"); + string password = Configuration.GetValue("FileAdapterSettings:password"); + string basePath = Configuration.GetValue("FileAdapterSettings:basePath"); + string tempAssetsFilePath = Configuration.GetValue("TempAssetsLocation"); + + services.AddSingleton(s => + { + return new LinuxFileSaveAdapter(url, userName, password, basePath, tempAssetsFilePath); + }); + } + else if (Configuration.GetValue("FileAdapterSettings:type").Equals("windows", System.StringComparison.OrdinalIgnoreCase)) + { + string filePath = Configuration.GetValue("FileAdapterSettings:filePath"); + string tempAssetsFilePath = Configuration.GetValue("TempAssetsLocation"); + + services.AddSingleton(s => + { + return new WindowsFileSaveAdapter(filePath, tempAssetsFilePath); + }); + } + + services.AddSingleton(s => new ApplicationDbContext(Configuration.GetConnectionString("DefaultConnection"))); + services.AddMemoryCache(); - services.AddScoped(s => new ApplicationDbContext(Configuration.GetConnectionString("DefaultConnection"))); - services.AddScoped(s => + services.Configure(x => { - return new FTPAdapter(url, new System.Net.NetworkCredential(userName, password)); + x.ValueLengthLimit = int.MaxValue; + x.MultipartBodyLengthLimit = int.MaxValue; + x.MultipartHeadersLengthLimit = int.MaxValue; }); } @@ -66,8 +95,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseStaticFiles(); app.UseRouting(); - //app.UseAuthentication(); - //app.UseAuthorization(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( diff --git a/Library.Encyclopedia.API/appsettings.json b/Library.Encyclopedia.API/appsettings.json index 8d02394..d8b1515 100644 --- a/Library.Encyclopedia.API/appsettings.json +++ b/Library.Encyclopedia.API/appsettings.json @@ -6,14 +6,32 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + //"App-Base-Url": "http://localhost:4200", + "App-Base-Url": "https://tools.library.pfw.edu/encyclopedia", "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=Encyclopedia;User=root;Password=root" + //"DefaultConnection": "Server=localhost;Database=Encyclopedia;User=root;Password=RW_qh+-ta5hW*2s" }, "ApiKey": "5929b003-8895-4fb3-bbb0-2eb101c48f66", - "FTPSettings": { - "url": "10.161.100.82", - "username": "mazus01", - "password": "" - } + "TempAssetsBaseUrl": "https://tools.library.pfw.edu/pfwencyclopediaassets", + "TempAssetsLocation": "C:\\inetpub\\wwwroot\\pfwencyclopediaassets", + "FileAdapterSettings": { + // linux or windows + "type": "windows", + + // For Linux + //"url": "10.161.100.82", + //"username": "serviceuser", + //"password": "P!ssword+007", + //"basePath": "Applications/PFW Encyclopedia Files" + + // For Windows + "filePath": "C:\\inetpub\\wwwroot\\PFW Encyclopedia Files\\" + }, + "SendGrid-APIKEY": "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", + "Template-Id": "d-af8da9debd13445898a1d82e2674e329", + "Admin-Email": "mazus01@pfw.edu", + "Admin-Username": "Souvik Mazumder", + "PFW-EmailList-Group": "Test" } \ No newline at end of file diff --git a/Library.Encyclopedia.API/web.config b/Library.Encyclopedia.API/web.config new file mode 100644 index 0000000..a991ee8 --- /dev/null +++ b/Library.Encyclopedia.API/web.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Library.Encyclopedia.DataAccess/ApplicationDbContext.cs b/Library.Encyclopedia.DataAccess/ApplicationDbContext.cs index 574dc9d..35eb7ec 100644 --- a/Library.Encyclopedia.DataAccess/ApplicationDbContext.cs +++ b/Library.Encyclopedia.DataAccess/ApplicationDbContext.cs @@ -1,5 +1,6 @@ using Library.Encyclopedia.Entity.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using System; using System.Threading.Tasks; @@ -16,12 +17,17 @@ public ApplicationDbContext(string connectionString) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var serverVersion = new MySqlServerVersion(new Version(8, 0, 18)); - optionsBuilder.UseMySql(connectionString, serverVersion); + optionsBuilder.UseMySql(connectionString, serverVersion, (mySqlOptions)=> { + mySqlOptions.EnableRetryOnFailure(); + }); } public DbSet
Main { get; set; } public DbSet Files { get; set; } public DbSet Links { get; set; } + public DbSet QueryStats { get; set; } + public DbSet Comments { get; set; } + public DbSet Likes { get; set; } public new async Task SaveChanges() { diff --git a/Library.Encyclopedia.DataAccess/DataAccess/CommentsDataAccess.cs b/Library.Encyclopedia.DataAccess/DataAccess/CommentsDataAccess.cs new file mode 100644 index 0000000..73aca2c --- /dev/null +++ b/Library.Encyclopedia.DataAccess/DataAccess/CommentsDataAccess.cs @@ -0,0 +1,106 @@ +using Library.Encyclopedia.Entity.Interfaces; +using Library.Encyclopedia.Entity.Models; +using Library.Encyclopedia.Entity.Models.External; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.DataAccess +{ + public class CommentsDataAccess : ICommentsDataAccess + { + private readonly IApplicationDbContext dbContext; + + public CommentsDataAccess(IApplicationDbContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task CreateComment(Comments comments) + { + comments.Id = Guid.NewGuid(); + comments.PostedTime = DateTime.Now; + + await dbContext.Comments.AddAsync(comments); + await dbContext.SaveChanges(); + + return comments; + } + + public async Task DeleteComment(Guid id, string userSid, bool isAdmin = false) + { + var item = await dbContext.Comments.FirstOrDefaultAsync(s => s.Id == id); + + if (item != null) + { + if (!isAdmin) + { + if (item.UserSid == userSid) + { + dbContext.Comments.Remove(item); + dbContext.Likes.RemoveRange(await dbContext.Likes.Where(s=>s.CommentId == item.Id).ToListAsync()); + await dbContext.SaveChanges(); + } + else + throw new Exception("This user did not post this comment!"); + } + else + { + dbContext.Comments.Remove(item); + dbContext.Likes.RemoveRange(await dbContext.Likes.Where(s => s.CommentId == item.Id).ToListAsync()); + await dbContext.SaveChanges(); + } + } + else + throw new Exception("Comment not found!"); + } + + public async Task Get(int offset = 0, int limit = 10, bool asc = false) + { + if(asc) + return new CommentsExternalCollection(await dbContext.Comments.OrderBy(s => s.PostedTime).Skip(offset).Take(limit).ToListAsync(), await dbContext.Comments.CountAsync()); + else + return new CommentsExternalCollection(await dbContext.Comments.OrderByDescending(s => s.PostedTime).Skip(offset).Take(limit).ToListAsync(), await dbContext.Comments.CountAsync()); + } + + public async Task> Get(Guid mainId) + { + return await dbContext.Comments.Where(s => s.MainId == mainId).OrderByDescending(s => s.PostedTime).ToListAsync(); + } + + public async Task Resolve(Guid id, bool flag = true) + { + var comment = await dbContext.Comments.FirstOrDefaultAsync(s => s.Id == id); + + if(comment != null) + { + comment.IsResolved = flag; + dbContext.Comments.Update(comment); + await dbContext.SaveChanges(); + } + } + + public async Task UpdateComment(Guid id, string comment, string userSid) + { + var commentObj = await dbContext.Comments.FirstOrDefaultAsync(s => s.Id == id); + + if (commentObj != null) + { + if (commentObj.UserSid == userSid) + { + commentObj.Comment = comment; + dbContext.Comments.Update(commentObj); + await dbContext.SaveChanges(); + } + else + throw new Exception("This user did not post this comment!"); + } + else + throw new Exception("Comment not found!"); + + } + } +} diff --git a/Library.Encyclopedia.DataAccess/DataAccess/LikesDataAccess.cs b/Library.Encyclopedia.DataAccess/DataAccess/LikesDataAccess.cs new file mode 100644 index 0000000..d5760b5 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/DataAccess/LikesDataAccess.cs @@ -0,0 +1,54 @@ +using Library.Encyclopedia.Entity.Interfaces; +using Library.Encyclopedia.Entity.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.DataAccess +{ + public class LikesDataAccess : ILikesDataAccess + { + private readonly IApplicationDbContext dbContext; + + public LikesDataAccess(IApplicationDbContext dbContext) + { + this.dbContext = dbContext; + } + + public Task DeleteLikes(List commentIds) + { + throw new NotImplementedException(); + } + + public async Task> GetLikes(List commentIds) + { + return (await dbContext.Likes.Where(s => commentIds.Contains(s.CommentId)).ToListAsync()).GroupBy(s => s.CommentId).ToDictionary(s => s.Key, s => s.LongCount()); + } + + public async Task Like(Guid commentId, string userSid, bool flag = true) + { + var result = await dbContext.Likes.FirstOrDefaultAsync(s => s.CommentId == commentId && s.UserSid == userSid); + if (result != null) + { + // if disliked + if (!flag) + { + dbContext.Likes.Remove(result); + await dbContext.SaveChanges(); + } + else throw new Exception("Already Liked"); + } + else + { + if (flag) + { + await dbContext.Likes.AddAsync(new Likes { CommentId = commentId, UserSid = userSid }); + await dbContext.SaveChanges(); + } + } + } + } +} diff --git a/Library.Encyclopedia.DataAccess/DataAccess/MainDataAccess.cs b/Library.Encyclopedia.DataAccess/DataAccess/MainDataAccess.cs index 3374177..4eae680 100644 --- a/Library.Encyclopedia.DataAccess/DataAccess/MainDataAccess.cs +++ b/Library.Encyclopedia.DataAccess/DataAccess/MainDataAccess.cs @@ -1,137 +1,417 @@ -using Library.Encyclopedia.Entity.Exceptions; +using Library.Encyclopedia.DataAccess.QueryStatsAccess; +using Library.Encyclopedia.Entity.Exceptions; using Library.Encyclopedia.Entity.Interfaces; using Library.Encyclopedia.Entity.Models; using Library.Encyclopedia.Entity.Models.External; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; +using System.Web; namespace Library.Encyclopedia.DataAccess.DataAccess { public class MainDataAccess : IMainDataAccess { private IApplicationDbContext _dbcontext; - public MainDataAccess(IApplicationDbContext dbcontext) + private readonly IFilesAdapter filesAdapter; + private readonly QueryStatsAdapter queryStatsAdapter; + private string APP_BASE_URL; + + public MainDataAccess(IApplicationDbContext dbcontext, IConfiguration configuration, IFilesAdapter filesAdapter) { + APP_BASE_URL = configuration.GetSection("App-Base-Url").Value; _dbcontext = dbcontext; + this.filesAdapter = filesAdapter; + this.queryStatsAdapter = new QueryStatsAdapter(dbcontext); } #region GET - async Task IMainDataAccess.GetAsync(string query, int offset, int pagesize, bool ascending) + async Task IMainDataAccess.GetAsync(string query, int offset, int pagesize, int previewSize, bool ascending) { - query = query.ToLower(); - var temp = _dbcontext.Main.Where(s => s.Description.ToLower().Contains(query) || s.Title.ToLower().Contains(query)) - .Skip(offset) - .Take(pagesize); - + query = query != null ? query.ToLower() : string.Empty; IEnumerable
data; + if (ascending) - data = await temp.OrderBy(s => s.Title) - .ThenBy(s => s.Description) - .ToListAsync(); + data = await _dbcontext.Main.Where(s => s.RawDescription.ToLower().Contains(query) || s.Title.ToLower().Contains(query)) + .OrderBy(s => s.Title.ToLower()) + .ThenBy(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .Include(s => s.Links) + .ToListAsync(); else - data = await temp.OrderByDescending(s => s.Title) - .ThenByDescending(s => s.Description) - .ToListAsync(); + data = await _dbcontext.Main.Where(s => s.RawDescription.ToLower().Contains(query) || s.Title.ToLower().Contains(query)) + .OrderByDescending(s => s.Title.ToLower()) + .ThenByDescending(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .Include(s => s.Links) + .ToListAsync(); - var total = await _dbcontext.Main.CountAsync(s => s.Description.ToLower().Contains(query) || s.Title.ToLower().Contains(query)); + var total = await _dbcontext.Main.CountAsync(s => s.RawDescription.ToLower().Contains(query) || s.Title.ToLower().Contains(query)); - MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(), total); + MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.MinimizeWithQuery(query, previewSize), total); - // random cleanup - await CleanUpData(); + if (!string.IsNullOrEmpty(query) && !string.IsNullOrWhiteSpace(query) && total > 0 && query.Length >= 3) + await this.queryStatsAdapter.AddQuery(query); return result; } - async Task IMainDataAccess.GetByCategoryAsync(string category, int offset, int pagesize, bool ascending) + async Task IMainDataAccess.GetByCategoryAsync(string category, int offset, int pagesize, int previewSize, bool ascending) { - category = category.ToLower(); - List
rawData = await _dbcontext.Main.ToListAsync(); - var temp = rawData.Where(s => s.Category.ToLower().Split(',', StringSplitOptions.None).Contains(category)) - .Skip(offset) - .Take(pagesize); - - IEnumerable
data; - if (ascending) - data = temp.OrderBy(s => s.Title) - .ThenBy(s => s.Description); + if (!string.IsNullOrEmpty(category)) + { + category = category.ToLower(); + + IEnumerable
data; + if (ascending) + data = await _dbcontext.Main.Where(s => s.Category != null) + .Where(s => s.Category.ToLower().Contains(category + ",") || s.Category.ToLower().Contains("," + category) || s.Category.ToLower() == category) + .OrderBy(s => s.Title.ToLower()) + .ThenBy(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .ToListAsync(); + else + data = await _dbcontext.Main.Where(s => s.Category != null) + .Where(s => s.Category.ToLower().Contains(category + ",") || s.Category.ToLower().Contains("," + category) || s.Category.ToLower() == category) + .OrderByDescending(s => s.Title.ToLower()) + .ThenByDescending(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .ToListAsync(); + + var total = await _dbcontext.Main.Where(s => s.Category != null).CountAsync(s => s.Category.ToLower().Contains(category + ",") || s.Category.ToLower().Contains("," + category) || s.Category.ToLower() == category); + + MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(previewSize), total); + + return result; + } else - data = temp.OrderByDescending(s => s.Title) - .ThenByDescending(s => s.Description); - - var total = rawData.Count(s => s.Category.ToLower().Split(',', StringSplitOptions.None).Contains(category)); - - MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(), total); - - return result; + { + IEnumerable
data; + if (ascending) + data = await _dbcontext.Main.Where(s => s.Category == null) + .OrderBy(s => s.Title.ToLower()) + .ThenBy(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .ToListAsync(); + else + data = await _dbcontext.Main.Where(s => s.Category == null) + + .OrderByDescending(s => s.Title.ToLower()) + .ThenByDescending(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) + .ToListAsync(); + + var total = await _dbcontext.Main.CountAsync(s => s.Category == null); + + MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(previewSize), total); + + return result; + } } - async Task IMainDataAccess.GetByAlphabetAsync(char startingAlphabet, int offset, int pagesize, bool ascending) + async Task IMainDataAccess.GetByAlphabetAsync(char startingAlphabet, int offset, int pagesize, int previewSize, bool ascending) { - var alph = startingAlphabet.ToString().ToLower(); - var temp = _dbcontext.Main.Where(s => s.Title.ToLower().StartsWith(alph)) - .Skip(offset) - .Take(pagesize); + IQueryable
temp; + int total; + + if (startingAlphabet == '#') + { + temp = _dbcontext.Main.Where(s => s.Title.StartsWith("0") || + s.Title.StartsWith("1") || + s.Title.StartsWith("2") || + s.Title.StartsWith("3") || + s.Title.StartsWith("4") || + s.Title.StartsWith("5") || + s.Title.StartsWith("6") || + s.Title.StartsWith("7") || + s.Title.StartsWith("8") || + s.Title.StartsWith("9")); + + total = await _dbcontext.Main.CountAsync(s => s.Title.StartsWith("0") || + s.Title.StartsWith("1") || + s.Title.StartsWith("2") || + s.Title.StartsWith("3") || + s.Title.StartsWith("4") || + s.Title.StartsWith("5") || + s.Title.StartsWith("6") || + s.Title.StartsWith("7") || + s.Title.StartsWith("8") || + s.Title.StartsWith("9")); + } + else + { + temp = _dbcontext.Main.Where(s => s.Title.ToLower().StartsWith(startingAlphabet.ToString().ToLower())); + total = await _dbcontext.Main.CountAsync(s => s.Title.ToLower().StartsWith(startingAlphabet.ToString().ToLower())); + } IEnumerable
data; if (ascending) - data = await temp.OrderBy(s => s.Title) - .ThenBy(s => s.Description) + data = await temp.OrderBy(s => s.Title.ToLower()) + .ThenBy(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) .ToListAsync(); else - data = await temp.OrderByDescending(s => s.Title) - .ThenByDescending(s => s.Description) + data = await temp.OrderByDescending(s => s.Title.ToLower()) + .ThenByDescending(s => s.RawDescription.ToLower()) + .Skip(offset) + .Take(pagesize) .ToListAsync(); - var total = await _dbcontext.Main.CountAsync(s => s.Title.ToLower().StartsWith(alph)); - - MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(), total); + MainMinimizedExternalCollection result = new MainMinimizedExternalCollection(data.Minimize(previewSize), total); return result; } async Task
IMainDataAccess.GetAsync(Guid id) { - return await _dbcontext.Main.Include(s => s.Files).Include(s => s.Links).FirstOrDefaultAsync(s => s.Id == id); + Main item = await _dbcontext.Main.Include(s => s.Files).Include(s => s.Links).Include(s => s.Files).FirstOrDefaultAsync(s => s.Id == id); + + if (item != null) + { + if (item.RichDescription != null) + { + // replace links in item + while (item.RichDescription.IndexOf("$$$") != -1) + { + var startIndex = item.RichDescription.IndexOf("$$$") + 3; + var endIndex = item.RichDescription.Substring(startIndex).IndexOf("$$$"); + + var referenceid = item.RichDescription.Substring(startIndex, endIndex); + + item.RichDescription = item.RichDescription.Replace($"$$${referenceid}$$$", $"{item.Links.FirstOrDefault(s => s.ReferenceId.ToString() == referenceid).Description}"); + } + } + + return item; + } + + return item; } #endregion #region CREATE public async Task CreateAsync(Main model) { + if (model.RichDescription.Contains("$$$")) + throw new CreateFailedException(CreateFailErrorCode.INVALID_INPUT_SEQUENCE); + // assign a random id model.Id = Guid.NewGuid(); + model.Category = model.Category == string.Empty ? null : model.Category; + + model = ReplaceInternalLinks(model); + model = ReplaceLineBreaks(model); + //santize data + model.RawDescription = StripHTML(model.RawDescription); await _dbcontext.Main.AddAsync(model); + await _dbcontext.Links.AddRangeAsync(model.Links); + await _dbcontext.SaveChanges(); return model.Id; } + + private Main ReplaceLineBreaks(Main model) + { + string searchStartString = " startIndexes = new List(); + for (int index = 0; ; index += searchStartString.Length) + { + index = model.RichDescription.IndexOf(searchStartString, index); + if (index != -1) + startIndexes.Add(index); + else break; + } + + // Extract end indices + List endIndexes = new List(); + for (int index = 0; ; index += searchEndString.Length) + { + index = model.RichDescription.IndexOf(searchEndString, index); + if (index != -1) + endIndexes.Add(index); + else break; + } + + var newDesc = new string(model.RichDescription); + + for (int i = 0; i < startIndexes.Count; i++) + { + string tag = model.RichDescription.Substring(startIndexes[i], endIndexes[i] - startIndexes[i] + searchEndString.Length); + + string value = model.RichDescription.Substring(startIndexes[i] + searchStartString.Length, endIndexes[i] - startIndexes[i] - +searchStartString.Length); + value = value.Substring(value.IndexOf('>') + 1); + + if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value)) + { + newDesc = newDesc.Replace(tag, "
"); + } + } + + model.RichDescription = newDesc; + } + + return model; + } + + private Main ReplaceInternalLinks(Main model, bool isUpdate = false) + { + string searchStartString = ""; + model.Links ??= new List(); + var newCreatedLinks = new List(); + var updatedLinks = new List(); + + if (model.RichDescription != null) + { + // Extract start indices + List startIndexes = new List(); + for (int index = 0; ; index += searchStartString.Length) + { + index = model.RichDescription.IndexOf(searchStartString, index); + if (index != -1) + startIndexes.Add(index); + else break; + } + + // Extract end indices + List endIndexes = new List(); + for (int index = 0; ; index += searchEndString.Length) + { + index = model.RichDescription.IndexOf(searchEndString, index); + if (index != -1) + endIndexes.Add(index); + else break; + } + + var newDesc = new string(model.RichDescription); + var newDesc2 = new string(model.RichDescription); + + for (int i = 0; i < startIndexes.Count; i++) + { + string value = model.RichDescription.Substring(startIndexes[i], endIndexes[i] - startIndexes[i] + searchEndString.Length); + if (value.Contains(APP_BASE_URL)) + { + var id = value.Substring(value.IndexOf(APP_BASE_URL) + APP_BASE_URL.Length + 1, 36); + var desc = value.Substring(value.IndexOf(">") + 1, value.IndexOf("<", value.IndexOf(">")) - value.IndexOf(">") - 1); + + newDesc = newDesc.Replace(value, $"$$${id}$$$"); + newDesc2 = newDesc2.Replace(value, desc); + + if (!model.Links.Any(s => s.ReferenceId == Guid.Parse(id))) + { + // add to links list + Links item = new Links + { + Id = Guid.NewGuid(), + MainId = model.Id, + ReferenceId = Guid.Parse(id), + Link = value, + Description = desc, + IsInternal = true + }; + model.Links.Add(item); + + newCreatedLinks.Add(item); + } + else + { + model.Links.FirstOrDefault(s => s.ReferenceId == Guid.Parse(id)).Link = value; + model.Links.FirstOrDefault(s => s.ReferenceId == Guid.Parse(id)).Description = desc; + updatedLinks.Add(model.Links.FirstOrDefault(s => s.ReferenceId == Guid.Parse(id))); + } + + } + } + + model.RichDescription = newDesc; + model.RawDescription = newDesc2; + + if (isUpdate) + { + _dbcontext.Links.AddRangeAsync(newCreatedLinks); + _dbcontext.Links.UpdateRange(updatedLinks); + _dbcontext.SaveChanges().Wait(); + } + + } + + return model; + } #endregion #region UPDATE - public async Task UpdateAsync(Guid id, MainUpdateModel model) + public async Task
UpdateAsync(Guid id, MainUpdateModel model) { // find the entry - var entry = await _dbcontext.Main.FirstOrDefaultAsync(s => s.Id == id); + var entry = await _dbcontext.Main.Include(s => s.Links).Include(s => s.Files).FirstOrDefaultAsync(s => s.Id == id); if (entry != null) { - entry.Title = model.Title; - entry.Description = model.Description; - entry.Category = model.Category; + if (model != null) + { + entry.Title = model.Title ?? entry.Title; - _dbcontext.Main.Update(entry); - await _dbcontext.SaveChanges(); + if (model.RichDescription != null) + { + if (model.RichDescription != null && model.RichDescription.Contains("$$$")) + throw new UpdateFailedException(UpdateFailErrorCode.INVALID_INPUT_SEQUENCE); + + entry.RichDescription = model.RichDescription; + + entry = ReplaceInternalLinks(entry, true); + entry = ReplaceLineBreaks(entry); + + //santize data + entry.RawDescription = StripHTML(entry.RawDescription); + } + + entry.Category = model.Category == null ? entry.Category : model.Category == string.Empty ? null : model.Category; + + _dbcontext.Main.Update(entry); + await _dbcontext.SaveChanges(); + + if (entry.RichDescription != null) + { + // replace links in item + while (entry.RichDescription.IndexOf("$$$") != -1) + { + var startIndex = entry.RichDescription.IndexOf("$$$") + 3; + var endIndex = entry.RichDescription.Substring(startIndex).IndexOf("$$$"); + + var referenceid = entry.RichDescription.Substring(startIndex, endIndex); + + entry.RichDescription = entry.RichDescription.Replace($"$$${referenceid}$$$", $"{entry.Links.FirstOrDefault(s => s.ReferenceId.ToString() == referenceid).Description}"); + } + } + + return entry; + } } else throw new UpdateFailedException(UpdateFailErrorCode.EntryNotFound); + + return null; } #endregion @@ -139,11 +419,24 @@ public async Task UpdateAsync(Guid id, MainUpdateModel model) public async Task DeleteAsync(Guid id) { // find the entry - var entry = await _dbcontext.Main.FirstOrDefaultAsync(s => s.Id == id); + var entry = await _dbcontext.Main.Include(s => s.Links).Include(s => s.Files).FirstOrDefaultAsync(s => s.Id == id); if (entry != null) { + if (entry.Links != null && entry.Links.Any()) _dbcontext.Links.RemoveRange(entry.Links); + if (entry.Files != null && entry.Files.Any()) + { + _dbcontext.Files.RemoveRange(entry.Files); + this.filesAdapter.DeleteFiles(entry.Id.ToString()); + } _dbcontext.Main.Remove(entry); + + // delete all comments and likes + var allComments = await _dbcontext.Comments.Where(s => s.MainId == entry.Id).ToListAsync(); + _dbcontext.Comments.RemoveRange(allComments); + var allCommentIds = allComments.Select(s => s.Id); + _dbcontext.Likes.RemoveRange(await _dbcontext.Likes.Where(s => allCommentIds.Contains(s.CommentId)).ToListAsync()); + await _dbcontext.SaveChanges(); } else @@ -158,8 +451,13 @@ public async Task> GetAllCategoriesAsync() var result = new List(); data.ForEach(item => { - result.AddRange(item.Split(',')); + if (!string.IsNullOrEmpty(item)) + { + result.AddRange(item.Split(',')); + } }); + + result.RemoveAll(s => s == string.Empty); return result.Distinct(); } @@ -172,19 +470,18 @@ public async Task CleanUpData() { string searchStartString = ""; - var links = new List(); + var allLinks = new List(); var allData = await _dbcontext.Main.ToListAsync(); - //using StreamWriter file = new StreamWriter(@"C:\temp\temp.txt", append: true); foreach (var item in allData) { - if (item.Description != null) + if (item.RichDescription != null) { // Extract start indices List startIndexes = new List(); for (int index = 0; ; index += searchStartString.Length) { - index = item.Description.IndexOf(searchStartString, index); + index = item.RichDescription.IndexOf(searchStartString, index); if (index != -1) startIndexes.Add(index); else break; @@ -194,32 +491,32 @@ public async Task CleanUpData() List endIndexes = new List(); for (int index = 0; ; index += searchEndString.Length) { - index = item.Description.IndexOf(searchEndString, index); + index = item.RichDescription.IndexOf(searchEndString, index); if (index != -1) endIndexes.Add(index); else break; } // - var newDesc = new string(item.Description); + var newDesc = new string(item.RichDescription); + var links = new List(); + for (int i = 0; i < startIndexes.Count; i++) { - string value = item.Description.Substring(startIndexes[i], endIndexes[i] - startIndexes[i] + searchEndString.Length); + string value = item.RichDescription.Substring(startIndexes[i], endIndexes[i] - startIndexes[i] + searchEndString.Length); if (value.Contains("http://")) { var id = value.Substring(value.IndexOf("id=") + 3, 36); var desc = value.Substring(value.IndexOf(">") + 1, value.IndexOf("<", value.IndexOf(">")) - value.IndexOf(">") - 1); - //await file.WriteLineAsync($"id = {id} | Description = {desc}"); - newDesc = newDesc.Replace(value, $"$$${id}$$$"); - //await file.WriteLineAsync($"new description = {newDesc}"); // add to links list links.Add(new Links { Id = Guid.NewGuid(), - MainId = Guid.Parse(id), + MainId = item.Id, + ReferenceId = Guid.Parse(id), Link = value, Description = desc, IsInternal = true @@ -228,18 +525,54 @@ public async Task CleanUpData() } // update the description - item.Description = newDesc; + item.RichDescription = newDesc; + // update the links array + item.Links = links; + allLinks.AddRange(links); } } // add to the links table - await _dbcontext.Links.AddRangeAsync(links); - + await _dbcontext.Links.AddRangeAsync(allLinks); // update the main data _dbcontext.Main.UpdateRange(allData); + await _dbcontext.SaveChanges(); + allData = await _dbcontext.Main.Include(s => s.Links).ToListAsync(); + + foreach (var item in allData) + { + var temp = new string(item.RichDescription); + + if (!string.IsNullOrEmpty(temp)) + { + // replace the links + if (item.Links != null && item.Links.Any()) + { + foreach (var link in item.Links) + { + var identifier = $"$$${link.ReferenceId}$$$"; + temp = temp.Replace(identifier, link.Description); + } + } + + item.RawDescription = temp; + + // replace the HTML tags + item.RawDescription = StripHTML(item.RawDescription, true); + } + } + // update the main data + _dbcontext.Main.UpdateRange(allData); await _dbcontext.SaveChanges(); } + + public static string StripHTML(string HTMLText, bool decode = true) + { + Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); + var stripped = reg.Replace(HTMLText, ""); + return decode ? HttpUtility.HtmlDecode(stripped) : stripped; + } #endregion } } \ No newline at end of file diff --git a/Library.Encyclopedia.DataAccess/Email/EmailAdapter.cs b/Library.Encyclopedia.DataAccess/Email/EmailAdapter.cs new file mode 100644 index 0000000..8378950 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/Email/EmailAdapter.cs @@ -0,0 +1,52 @@ +using Library.Encyclopedia.Entity.Models; +using Microsoft.Extensions.Configuration; +using SendGrid; +using SendGrid.Helpers.Mail; +using System; +using System.Collections.Generic; +using System.DirectoryServices.AccountManagement; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.Email +{ + public class EmailAdapter + { + private readonly IConfiguration configuration; + + public EmailAdapter(IConfiguration configuration) + { + this.configuration = configuration; + } + + public async Task SendEmail(string userName, string email, string content) + { + var apiKey = configuration.GetSection("SendGrid-APIKEY").Value; + var client = new SendGridClient(apiKey); + var from = new EmailAddress(configuration.GetSection("Admin-Email").Value, configuration.GetSection("Admin-Username").Value); + + var to = new List(); + + using (var context = new PrincipalContext(ContextType.Machine)) + { + using (var group = GroupPrincipal.FindByIdentity(context, configuration.GetSection("PFW-EmailList-Group").Value)) + { + if (group != null) + { + var members = group.GetMembers(true); + foreach (UserPrincipal user in members) + { + to.Add(new EmailAddress { Email = user.EmailAddress, Name = user.DisplayName }); + } + } + } + } + + var msg = MailHelper.CreateSingleTemplateEmailToMultipleRecipients(from, to, configuration.GetSection("Template-Id").Value, new EmailPropertiesV1 { Content = content, Name = userName, Email = email }); + var response = await client.SendEmailAsync(msg); + + if (!response.IsSuccessStatusCode) + throw new Exception("Email not sent!"); + } + } +} diff --git a/Library.Encyclopedia.DataAccess/FileAccess/FTPAdapter.cs b/Library.Encyclopedia.DataAccess/FileAccess/FTPAdapter.cs deleted file mode 100644 index 1e1cae2..0000000 --- a/Library.Encyclopedia.DataAccess/FileAccess/FTPAdapter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Library.Encyclopedia.Entity.Interfaces; -using Library.Encyclopedia.Entity.Models; -using Microsoft.AspNetCore.Http; -using Renci.SshNet; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; - -namespace Library.Encyclopedia.DataAccess.FileAccess -{ - public class FTPAdapter : IFilesAdapter - { - private readonly string url; - private readonly NetworkCredential networkCredential; - - public FTPAdapter(string url, NetworkCredential networkCredential) - { - this.url = url; - this.networkCredential = networkCredential; - } - - public IEnumerable DownloadFiles(IEnumerable files, string directory) - { - List responses = new List(); - var customUrl = $@"/home/{networkCredential.UserName}"; - - using (SftpClient client = new SftpClient(url, 22, networkCredential.UserName, networkCredential.Password)) - { - client.Connect(); - foreach (var file in files) - { - using (var stream = File.OpenWrite(Path.Combine(directory, file.FileName))) - { - client.DownloadFile(customUrl + file.FilePath, stream); - responses.Add(true); - } - } - } - - return responses; - } - - public IEnumerable UploadFiles(Guid id, List files) - { - List responses = new List(); - var customUrl = $@"/home/{networkCredential.UserName}/encyclopedia-media/{id}/"; - - using (SftpClient client = new SftpClient(url, 22, networkCredential.UserName, networkCredential.Password)) - { - client.Connect(); - if (!client.Exists(customUrl)) - client.CreateDirectory(customUrl); - - foreach (var file in files) - { - if (client.Exists(customUrl + file.FileName)) - client.DeleteFile(customUrl + file.FileName); - - client.UploadFile(file.OpenReadStream(), customUrl + file.FileName); - responses.Add(true); - } - } - - return responses; - } - } -} diff --git a/Library.Encyclopedia.DataAccess/FileAccess/LinuxFileSaveAdapter.cs b/Library.Encyclopedia.DataAccess/FileAccess/LinuxFileSaveAdapter.cs new file mode 100644 index 0000000..7eddfd7 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/FileAccess/LinuxFileSaveAdapter.cs @@ -0,0 +1,105 @@ +using Library.Encyclopedia.Entity.Interfaces; +using Microsoft.AspNetCore.Http; +using Renci.SshNet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.FileAccess +{ + public class LinuxFileSaveAdapter : IFilesAdapter, IDisposable + { + private readonly string url; + private readonly string basePath; + private readonly string tempAssetsFilePath; + private readonly NetworkCredential networkCredential; + private SftpClient client; + + public LinuxFileSaveAdapter(string url, string userName, string password, string basePath, string tempAssetsFilePath) + { + this.url = url; + this.basePath = basePath; + this.tempAssetsFilePath = tempAssetsFilePath; + this.networkCredential = new NetworkCredential(userName, password); + + client = new SftpClient(url, 22, networkCredential.UserName, networkCredential.Password); + client.Connect(); + } + + public void CreateViewableLink(string id, string name) + { + if (!Directory.Exists(Path.Combine(tempAssetsFilePath, id))) + Directory.CreateDirectory(Path.Combine(tempAssetsFilePath, id)); + + var remotePath = $@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString() + '/' + name; + using (Stream fileStream = File.Create(Path.Combine(tempAssetsFilePath, id, name))) + { + client.DownloadFile(remotePath, fileStream); + } + } + + public void DeleteFile(string id, string name) + { + var completeUrl = $@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString() + '/' + name; + + + if (client.Exists(completeUrl)) + client.DeleteFile(completeUrl); + } + + public void DeleteFiles(string id) + { + var completeUrl = $@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString(); + + if (client.Exists(completeUrl)) + { + IEnumerable files = client.ListDirectory(completeUrl); + foreach (var file in files) + { + if (file.Name != "." && file.Name != "..") + { + client.DeleteFile(file.FullName); + } + } + + client.DeleteDirectory(completeUrl); + } + } + + public void DestroyViewableLink(string id, string name) + { + File.Delete(Path.Combine(tempAssetsFilePath, id, name)); + } + + public void Dispose() + { + this.client.Disconnect(); + this.client.Dispose(); + } + + public Stream DownloadFile(string id, string name) + { + var completeUrl = $@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString() + '/' + name; + return client.OpenRead(completeUrl); + } + + public Task UploadFile(Guid id, IFormFile file) + { + var completeUrl = $@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString() + '/' + file.FileName; + + if (!client.Exists($@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString())) + client.CreateDirectory($@"/home/{networkCredential.UserName}/" + basePath + '/' + id.ToString()); + + if (client.Exists(completeUrl)) + client.DeleteFile(completeUrl); + + client.Create(completeUrl); + client.UploadFile(file.OpenReadStream(), completeUrl, true); + + return Task.FromResult(true); + } + } +} diff --git a/Library.Encyclopedia.DataAccess/FileAccess/WindowsFileSaveAdapter.cs b/Library.Encyclopedia.DataAccess/FileAccess/WindowsFileSaveAdapter.cs new file mode 100644 index 0000000..2782462 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/FileAccess/WindowsFileSaveAdapter.cs @@ -0,0 +1,83 @@ +using Library.Encyclopedia.Entity.Interfaces; +using Library.Encyclopedia.Entity.Models; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.FileAccess +{ + public class WindowsFileSaveAdapter : IFilesAdapter + { + private readonly string networkPath; + private readonly string tempAssetFileLocation; + + public WindowsFileSaveAdapter(string networkPath, string tempAssetFileLocation) + { + this.networkPath = networkPath; + this.tempAssetFileLocation = tempAssetFileLocation; + } + + public void CreateViewableLink(string id, string name) + { + if (!Directory.Exists(Path.Combine(tempAssetFileLocation, id))) + Directory.CreateDirectory(Path.Combine(tempAssetFileLocation, id)); + + File.Copy(Path.Combine(networkPath, id, name), Path.Combine(tempAssetFileLocation, id, name), true); + } + + public void DeleteFile(string id, string name) + { + File.Delete(Path.Combine(networkPath, id, name)); + } + + public void DeleteFiles(string id) + { + foreach (var file in Directory.GetFiles(Path.Combine(networkPath, id))) + { + File.Delete(file); + } + Directory.Delete(Path.Combine(networkPath, id)); + } + + public void DestroyViewableLink(string id, string name) + { + File.Delete(Path.Combine(tempAssetFileLocation, id, name)); + } + + public Stream DownloadFile(string id, string name) + { + return File.OpenRead(Path.Combine(networkPath, id, name)); + } + + public async Task UploadFile(Guid id, IFormFile file) + { + try + { + var path = Path.Combine(networkPath, id.ToString(), file.FileName); + if (File.Exists(path)) + using (var stream = File.OpenWrite(path)) + { + await file.CopyToAsync(stream); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using (var stream = File.Create(path)) + { + await file.CopyToAsync(stream); + } + } + + return true; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Library.Encyclopedia.DataAccess/IApplicationDbContext.cs b/Library.Encyclopedia.DataAccess/IApplicationDbContext.cs index 4c1fcf0..562475d 100644 --- a/Library.Encyclopedia.DataAccess/IApplicationDbContext.cs +++ b/Library.Encyclopedia.DataAccess/IApplicationDbContext.cs @@ -1,5 +1,6 @@ using Library.Encyclopedia.Entity.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using System.Threading.Tasks; namespace Library.Encyclopedia.DataAccess @@ -9,6 +10,10 @@ public interface IApplicationDbContext DbSet
Main { get; set; } DbSet Files { get; set; } DbSet Links { get; set; } + DbSet QueryStats { get; set; } + DbSet Comments { get; set; } + DbSet Likes { get; set; } + Task SaveChanges(); } } \ No newline at end of file diff --git a/Library.Encyclopedia.DataAccess/Library.Encyclopedia.DataAccess.csproj b/Library.Encyclopedia.DataAccess/Library.Encyclopedia.DataAccess.csproj index e53781f..1b6b377 100644 --- a/Library.Encyclopedia.DataAccess/Library.Encyclopedia.DataAccess.csproj +++ b/Library.Encyclopedia.DataAccess/Library.Encyclopedia.DataAccess.csproj @@ -5,6 +5,8 @@ + + @@ -17,7 +19,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Library.Encyclopedia.DataAccess/QueryStatsAccess/CommonlyOccuringWordsAdapter.cs b/Library.Encyclopedia.DataAccess/QueryStatsAccess/CommonlyOccuringWordsAdapter.cs new file mode 100644 index 0000000..681bd64 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/QueryStatsAccess/CommonlyOccuringWordsAdapter.cs @@ -0,0 +1,175 @@ +using Catalyst; +using Catalyst.Models; +using Library.Encyclopedia.Entity.Models; +using Microsoft.Extensions.Caching.Memory; +using Mosaik.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Library.Encyclopedia.DataAccess.QueryStatsAccess +{ + public class CommonlyOccuringWordsAdapter + { + private readonly IApplicationDbContext applicationDbContext; + private Dictionary commonlyOccuringWords; + private Dictionary> forReference; + private IMemoryCache _cache; + + public CommonlyOccuringWordsAdapter(IApplicationDbContext applicationDbContext, IMemoryCache cache) + { + commonlyOccuringWords = new Dictionary(); + forReference = new Dictionary>(); + this.applicationDbContext = applicationDbContext; + this._cache = cache; + } + + private void PopulateRecommendedWords() + { + commonlyOccuringWords = new Dictionary(); + forReference = new Dictionary>(); + + var allData = applicationDbContext.Main.ToList(); + + //BasicParser(allData); + AdvancedParser(allData); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromHours(1)); + + if (commonlyOccuringWords != null) + { + _cache.Set("commonlyOccuringWords", commonlyOccuringWords, cacheEntryOptions); + } + } + + private void AdvancedParser(List
allData) + { + English.Register(); + Storage.Current = new DiskStorage("catalyst-models"); + var nlp = Pipeline.For(Language.English); + + foreach (var item in allData) + { + ProcessText(nlp, item.Title, item.Id); + ProcessText(nlp, item.RawDescription, item.Id); + } + } + + private void ProcessText(Pipeline nlp, string text, Guid id) + { + var doc = new Document(text, Language.English); + nlp.ProcessSingle(doc); + + foreach (var sentence in doc) + { + foreach (var token in sentence) + { + switch (token.POS) + { + case PartOfSpeech.PROPN: + case PartOfSpeech.NOUN: + case PartOfSpeech.ADJ: + case PartOfSpeech.ADV: + case PartOfSpeech.NUM: + if (token.Value.Length >= 3) + { + if (commonlyOccuringWords.ContainsKey(token.Value)) + commonlyOccuringWords[token.Value]++; + else + commonlyOccuringWords.Add(token.Value, 1); + } + break; + case PartOfSpeech.NONE: + case PartOfSpeech.ADP: + case PartOfSpeech.AUX: + case PartOfSpeech.CCONJ: + case PartOfSpeech.DET: + case PartOfSpeech.INTJ: + case PartOfSpeech.PART: + case PartOfSpeech.PRON: + case PartOfSpeech.PUNCT: + case PartOfSpeech.SCONJ: + case PartOfSpeech.SYM: + case PartOfSpeech.VERB: + case PartOfSpeech.X: + default: + break; + } + + if (!forReference.ContainsKey(token.POS)) + forReference.Add(token.POS, new List() { token.Value.ToLower() }); + else + { + if (!forReference[token.POS].Contains(token.Value.ToLower())) + forReference[token.POS].Add(token.Value.ToLower()); + } + } + } + } + + private void BasicParser(List
allData) + { + foreach (var item in allData) + { + string[] seperators = { ", ", ". ", "! ", "? ", ": ", "; ", " " }; + + string v1 = item.Title.Replace('(', ' ') + .Replace(')', ' ') + .Replace('{', ' ') + .Replace('}', ' ') + .Replace('[', ' ') + .Replace(']', ' '); + foreach (var word in v1.Split(seperators, StringSplitOptions.RemoveEmptyEntries)) + { + if (!string.IsNullOrEmpty(word) && !string.IsNullOrWhiteSpace(word) && !word.ToCharArray().All(s => !char.IsLetterOrDigit(s))) + { + if (commonlyOccuringWords.ContainsKey(word)) + commonlyOccuringWords[word]++; + else + commonlyOccuringWords.Add(word, 1); + } + } + + string v2 = item.RawDescription.Replace('(', ' ') + .Replace(')', ' ') + .Replace('{', ' ') + .Replace('}', ' ') + .Replace('[', ' ') + .Replace(']', ' '); + + foreach (var word in v2.Split(seperators, StringSplitOptions.RemoveEmptyEntries)) + { + if (!string.IsNullOrEmpty(word) && !string.IsNullOrWhiteSpace(word) && !word.ToCharArray().All(s => !char.IsLetterOrDigit(s))) + { + if (commonlyOccuringWords.ContainsKey(word)) + commonlyOccuringWords[word]++; + else + commonlyOccuringWords.Add(word, 1); + } + } + } + } + + public Dictionary GetCommonlyUsedWords(int count = 0) + { + bool resultCacheValueFlag = _cache.TryGetValue("commonlyOccuringWords", out object value); + if (!resultCacheValueFlag || value == null) + PopulateRecommendedWords(); + + return _cache.Get>("commonlyOccuringWords").OrderByDescending(s => s.Value).Take(count).ToDictionary(x => x.Key, x => x.Value); + } + + public Dictionary GetCommonlySearchedWords(int count = 0) + { + IQueryable queryable = applicationDbContext.QueryStats.OrderByDescending(s => s.Count).Take(count); + return queryable.ToDictionary(s => s.Query, s => s.Count); + } + + public Dictionary> GetNLPResult() + { + return forReference.ToDictionary(s => s.Key.ToString(), s => s.Value); + } + } +} diff --git a/Library.Encyclopedia.DataAccess/QueryStatsAccess/QueryStatsAdapter.cs b/Library.Encyclopedia.DataAccess/QueryStatsAccess/QueryStatsAdapter.cs new file mode 100644 index 0000000..a659441 --- /dev/null +++ b/Library.Encyclopedia.DataAccess/QueryStatsAccess/QueryStatsAdapter.cs @@ -0,0 +1,31 @@ +using Library.Encyclopedia.Entity.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.DataAccess.QueryStatsAccess +{ + public class QueryStatsAdapter + { + private readonly IApplicationDbContext _dbcontext; + + public QueryStatsAdapter(IApplicationDbContext applicationDbContext) + { + this._dbcontext = applicationDbContext; + } + + public async Task AddQuery(string query) + { + QueryStats queryStats = await _dbcontext.QueryStats.FirstOrDefaultAsync(s => s.Query == query); + if (queryStats == null) + await _dbcontext.QueryStats.AddAsync(new QueryStats { Query = query, Count = 1 }); + else + { + queryStats.Count++; + _dbcontext.QueryStats.Update(queryStats); + } + + await _dbcontext.SaveChanges(); + } + } +} diff --git a/Library.Encyclopedia.Entity/Exceptions/CreateFailedException.cs b/Library.Encyclopedia.Entity/Exceptions/CreateFailedException.cs new file mode 100644 index 0000000..1fd9049 --- /dev/null +++ b/Library.Encyclopedia.Entity/Exceptions/CreateFailedException.cs @@ -0,0 +1,19 @@ +using EnumsNET; +using System; +using System.ComponentModel; + +namespace Library.Encyclopedia.Entity.Exceptions +{ + public class CreateFailedException : Exception + { + public CreateFailedException(CreateFailErrorCode code) : base(((CreateFailErrorCode)code).AsString(EnumFormat.Description)) + { + } + } + + public enum CreateFailErrorCode + { + [Description("$$$ is not an accepted input")] + INVALID_INPUT_SEQUENCE + } +} \ No newline at end of file diff --git a/Library.Encyclopedia.Entity/Exceptions/GetFailedException.cs b/Library.Encyclopedia.Entity/Exceptions/GetFailedException.cs new file mode 100644 index 0000000..8fdf675 --- /dev/null +++ b/Library.Encyclopedia.Entity/Exceptions/GetFailedException.cs @@ -0,0 +1,18 @@ +using EnumsNET; +using System; +using System.ComponentModel; + +namespace Library.Encyclopedia.Entity.Exceptions +{ + public class GetFailedException : Exception + { + public GetFailedException(GetFailErrorCode code) : base(((GetFailErrorCode)code).AsString(EnumFormat.Description)) + { + } + } + + public enum GetFailErrorCode + { + + } +} \ No newline at end of file diff --git a/Library.Encyclopedia.Entity/Exceptions/UpdateFailedException.cs b/Library.Encyclopedia.Entity/Exceptions/UpdateFailedException.cs index 7bd1aa6..eb04a1e 100644 --- a/Library.Encyclopedia.Entity/Exceptions/UpdateFailedException.cs +++ b/Library.Encyclopedia.Entity/Exceptions/UpdateFailedException.cs @@ -14,6 +14,8 @@ public UpdateFailedException(UpdateFailErrorCode code) : base(((UpdateFailErrorC public enum UpdateFailErrorCode { [Description("Entry not found!")] - EntryNotFound + EntryNotFound, + [Description("$$$ is not an accepted input")] + INVALID_INPUT_SEQUENCE } } diff --git a/Library.Encyclopedia.Entity/Interfaces/ICommentsDataAccess.cs b/Library.Encyclopedia.Entity/Interfaces/ICommentsDataAccess.cs new file mode 100644 index 0000000..6275b6f --- /dev/null +++ b/Library.Encyclopedia.Entity/Interfaces/ICommentsDataAccess.cs @@ -0,0 +1,19 @@ +using Library.Encyclopedia.Entity.Models; +using Library.Encyclopedia.Entity.Models.External; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.Entity.Interfaces +{ + public interface ICommentsDataAccess + { + Task Get(int offset = 0, int limit = 10, bool asc = false); + Task> Get(Guid mainId); + Task CreateComment(Comments comments); + Task UpdateComment(Guid id, string comment, string userSid); + Task DeleteComment(Guid id, string userSid, bool isAdmin = false); + Task Resolve(Guid id, bool flag = true); + } +} diff --git a/Library.Encyclopedia.Entity/Interfaces/IFilesAdapter.cs b/Library.Encyclopedia.Entity/Interfaces/IFilesAdapter.cs index 95bfb79..afbc621 100644 --- a/Library.Encyclopedia.Entity/Interfaces/IFilesAdapter.cs +++ b/Library.Encyclopedia.Entity/Interfaces/IFilesAdapter.cs @@ -2,12 +2,19 @@ using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; namespace Library.Encyclopedia.Entity.Interfaces { public interface IFilesAdapter { - IEnumerable UploadFiles(Guid id, List files); - IEnumerable DownloadFiles(IEnumerable files, string directory); + public Task UploadFile(Guid id, IFormFile file); + Stream DownloadFile(string id, string name); + public void DeleteFiles(string id); + public void DeleteFile(string id, string name); + + public void CreateViewableLink(string id, string name); + public void DestroyViewableLink(string id, string name); } } \ No newline at end of file diff --git a/Library.Encyclopedia.Entity/Interfaces/ILikesDataAccess.cs b/Library.Encyclopedia.Entity/Interfaces/ILikesDataAccess.cs new file mode 100644 index 0000000..ffe27c9 --- /dev/null +++ b/Library.Encyclopedia.Entity/Interfaces/ILikesDataAccess.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Library.Encyclopedia.Entity.Interfaces +{ + public interface ILikesDataAccess + { + Task Like(Guid commentId, string userSid, bool flag = true); + Task> GetLikes(List commentIds); + Task DeleteLikes(List commentIds); + } +} diff --git a/Library.Encyclopedia.Entity/Interfaces/IMainDataAccess.cs b/Library.Encyclopedia.Entity/Interfaces/IMainDataAccess.cs index cc8c6eb..2ba0aa5 100644 --- a/Library.Encyclopedia.Entity/Interfaces/IMainDataAccess.cs +++ b/Library.Encyclopedia.Entity/Interfaces/IMainDataAccess.cs @@ -9,15 +9,15 @@ namespace Library.Encyclopedia.Entity.Interfaces public interface IMainDataAccess { #region GET - public Task GetAsync(string query, int offset, int pagesize, bool ascending); - public Task GetByCategoryAsync(string category, int offset, int pagesize, bool ascending); - public Task GetByAlphabetAsync(char startingAlphabet, int offset, int pagesize, bool ascending); + public Task GetAsync(string query, int offset, int pagesize, int previewSize, bool ascending); + public Task GetByCategoryAsync(string category, int offset, int pagesize, int previewSize, bool ascending); + public Task GetByAlphabetAsync(char startingAlphabet, int offset, int pagesize, int previewSize, bool ascending); public Task
GetAsync(Guid id); #endregion public Task CreateAsync(Main main); - public Task UpdateAsync(Guid id, MainUpdateModel model); + public Task
UpdateAsync(Guid id, MainUpdateModel model); public Task DeleteAsync(Guid id); public Task> GetAllCategoriesAsync(); diff --git a/Library.Encyclopedia.Entity/Library.Encyclopedia.Entity.csproj b/Library.Encyclopedia.Entity/Library.Encyclopedia.Entity.csproj index f6f8e6a..66d87ed 100644 --- a/Library.Encyclopedia.Entity/Library.Encyclopedia.Entity.csproj +++ b/Library.Encyclopedia.Entity/Library.Encyclopedia.Entity.csproj @@ -7,6 +7,7 @@ + diff --git a/Library.Encyclopedia.Entity/Models/Comments.cs b/Library.Encyclopedia.Entity/Models/Comments.cs new file mode 100644 index 0000000..a1f17c1 --- /dev/null +++ b/Library.Encyclopedia.Entity/Models/Comments.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace Library.Encyclopedia.Entity.Models +{ + public class Comments + { + public Guid Id { get; set; } + public Guid MainId { get; set; } + public string Comment { get; set; } + public DateTime PostedTime { get; set; } + public string UserName { get; set; } + public string UserSid { get; set; } + public long Likes { get; set; } + public bool IsResolved { get; set; } + } +} diff --git a/Library.Encyclopedia.Entity/Models/EmailPropertiesV1.cs b/Library.Encyclopedia.Entity/Models/EmailPropertiesV1.cs new file mode 100644 index 0000000..c50a039 --- /dev/null +++ b/Library.Encyclopedia.Entity/Models/EmailPropertiesV1.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Library.Encyclopedia.Entity.Models +{ + public class EmailPropertiesV1 + { + [JsonProperty("content")] + public string Content { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + } +} diff --git a/Library.Encyclopedia.Entity/Models/External/CommentsExternalCollection.cs b/Library.Encyclopedia.Entity/Models/External/CommentsExternalCollection.cs new file mode 100644 index 0000000..3d2dbec --- /dev/null +++ b/Library.Encyclopedia.Entity/Models/External/CommentsExternalCollection.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Library.Encyclopedia.Entity.Models.External +{ + + public class CommentsExternalCollection : QueryExternalModel + { + public CommentsExternalCollection() + { + this.Count = 0; + this.Result = new List(); + this.Total = 0; + } + public CommentsExternalCollection(List collection, int total) + { + this.Result = collection; + this.Count = collection.Count(); + this.Total = total; + } + } +} diff --git a/Library.Encyclopedia.Entity/Models/External/MainMinimizedExternal.cs b/Library.Encyclopedia.Entity/Models/External/MainMinimizedExternal.cs index 7aca354..3e36cbf 100644 --- a/Library.Encyclopedia.Entity/Models/External/MainMinimizedExternal.cs +++ b/Library.Encyclopedia.Entity/Models/External/MainMinimizedExternal.cs @@ -8,11 +8,16 @@ public class MainMinimizedExternal { public Guid Id { get; set; } public string Title { get; set; } - public string Description { get; set; } - public string Category { get; set; } + public string Preview { get; set; } } public class MainMinimizedExternalCollection : QueryExternalModel { + public MainMinimizedExternalCollection() + { + this.Count = 0; + this.Result = new List(); + this.Total = 0; + } public MainMinimizedExternalCollection(IEnumerable collection, int total) { this.Result = collection; diff --git a/Library.Encyclopedia.Entity/Models/External/MainUpdateModel.cs b/Library.Encyclopedia.Entity/Models/External/MainUpdateModel.cs index 225404b..90bbab1 100644 --- a/Library.Encyclopedia.Entity/Models/External/MainUpdateModel.cs +++ b/Library.Encyclopedia.Entity/Models/External/MainUpdateModel.cs @@ -3,7 +3,8 @@ public class MainUpdateModel { public string Title { get; set; } - public string Description { get; set; } + public string RawDescription { get; set; } + public string RichDescription { get; set; } public string Category { get; set; } } } \ No newline at end of file diff --git a/Library.Encyclopedia.Entity/Models/Files.cs b/Library.Encyclopedia.Entity/Models/Files.cs index 3c981cf..f862f9d 100644 --- a/Library.Encyclopedia.Entity/Models/Files.cs +++ b/Library.Encyclopedia.Entity/Models/Files.cs @@ -13,6 +13,8 @@ public class Files public string FileName { get; set; } [Required] public string FileType { get; set; } + [Required] + public long FileSize { get; set; } public string FileDescription { get; set; } [Required] public string FilePath { get; set; } diff --git a/Library.Encyclopedia.Entity/Models/Likes.cs b/Library.Encyclopedia.Entity/Models/Likes.cs new file mode 100644 index 0000000..eab4562 --- /dev/null +++ b/Library.Encyclopedia.Entity/Models/Likes.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Library.Encyclopedia.Entity.Models +{ + public class Likes + { + public long Id { get; set; } + public Guid CommentId { get; set; } + public string UserSid { get; set; } + } +} diff --git a/Library.Encyclopedia.Entity/Models/Links.cs b/Library.Encyclopedia.Entity/Models/Links.cs index 0d2eb11..d75f55d 100644 --- a/Library.Encyclopedia.Entity/Models/Links.cs +++ b/Library.Encyclopedia.Entity/Models/Links.cs @@ -10,6 +10,8 @@ public class Links [Required] public Guid MainId { get; set; } [Required] + public Guid ReferenceId { get; set; } + [Required] public string Link { get; set; } public string Description { get; set; } [Required] diff --git a/Library.Encyclopedia.Entity/Models/Main.cs b/Library.Encyclopedia.Entity/Models/Main.cs index bc6bf11..9e81b7f 100644 --- a/Library.Encyclopedia.Entity/Models/Main.cs +++ b/Library.Encyclopedia.Entity/Models/Main.cs @@ -12,9 +12,12 @@ public class Main [Required] [StringLength(1024)] public string Title { get; set; } - public string Description { get; set; } + public string RichDescription { get; set; } + public string RawDescription { get; set; } + [StringLength(256)] public string Category { get; set; } + public string Author { get; set; } public ICollection Files { get; set; } public ICollection Links { get; set; } @@ -22,20 +25,82 @@ public class Main public static class MainExtensions { - public static MainMinimizedExternal Minimize(this Main main) + public static MainMinimizedExternal MinimizeWithQuery(this Main main, string query, int previewText_maxlength) { - return new MainMinimizedExternal { - Id = main.Id, - Category = main.Category, - Description = main.Description, - Title = main.Title - }; + if (!string.IsNullOrEmpty(main.RawDescription)) + { + if (!string.IsNullOrEmpty(query)) + { + int indexOfQuery = main.RawDescription.IndexOf(query, StringComparison.OrdinalIgnoreCase); + + // find the complete word + if (indexOfQuery != -1) + { + for (int i = indexOfQuery; i >= 0; i--) + { + if (char.IsSeparator(main.RawDescription[i])) + { + indexOfQuery = i; + break; + } + } + } + + int lengthOfPreviewText = indexOfQuery == -1 + ? Math.Min(previewText_maxlength, main.RawDescription.Length) + : Math.Min(previewText_maxlength, main.RawDescription.Length - indexOfQuery); + + return new MainMinimizedExternal + { + Id = main.Id, + Preview = indexOfQuery == -1 + ? main.RawDescription.Length > previewText_maxlength + ? main.RawDescription.Substring(0, lengthOfPreviewText) + " ..." + : main.RawDescription.Substring(0, lengthOfPreviewText) + : main.RawDescription.Length - indexOfQuery > previewText_maxlength + ? " ... " + main.RawDescription.Substring(indexOfQuery, lengthOfPreviewText) + " ..." + : main.RawDescription.Length > previewText_maxlength + ? " ... " + main.RawDescription.Substring(main.RawDescription.Length - previewText_maxlength, previewText_maxlength) + : main.RawDescription, + Title = main.Title + }; + } + else + { + int lengthOfPreviewText = Math.Min(previewText_maxlength, main.RawDescription.Length); + return new MainMinimizedExternal + { + Id = main.Id, + Preview = main.RawDescription.Length > previewText_maxlength + ? main.RawDescription.Substring(0, lengthOfPreviewText) + " ..." + : main.RawDescription.Substring(0, lengthOfPreviewText), + Title = main.Title + }; + } + } + else + return new MainMinimizedExternal { + Id = main.Id, + Preview = string.Empty, + Title = main.Title + }; + } + public static MainMinimizedExternal Minimize(this Main main, int previewText_maxlength) + { + return main.MinimizeWithQuery(null, previewText_maxlength); + } + public static IEnumerable MinimizeWithQuery(this IEnumerable
collection, string query, int previewText_maxlength) + { + foreach (var item in collection) + { + yield return MinimizeWithQuery(item, query, previewText_maxlength); + } } - public static IEnumerable Minimize(this IEnumerable
collection) + public static IEnumerable Minimize(this IEnumerable
collection, int previewText_maxlength) { foreach (var item in collection) { - yield return Minimize(item); + yield return Minimize(item, previewText_maxlength); } } } diff --git a/Library.Encyclopedia.Entity/Models/QueryStats.cs b/Library.Encyclopedia.Entity/Models/QueryStats.cs new file mode 100644 index 0000000..52d304f --- /dev/null +++ b/Library.Encyclopedia.Entity/Models/QueryStats.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Library.Encyclopedia.Entity.Models +{ + public class QueryStats + { + public long Id { get; set; } + public string Query { get; set; } + public long Count { get; set; } + } +}