Made description and note optional, auto added title and description from metadata if not provided

This commit is contained in:
Carl Tibule
2023-01-28 17:04:49 -06:00
parent 22f5764589
commit 3f4c32107e
12 changed files with 327 additions and 20 deletions

View File

@ -25,6 +25,8 @@ namespace YABA.API.Controllers
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateBookmarkRequestDTO request) public async Task<IActionResult> Create([FromBody] CreateBookmarkRequestDTO request)
{ {
if (!ModelState.IsValid) return BadRequest(ModelState);
var result = await _bookmarkService.CreateBookmark(request); var result = await _bookmarkService.CreateBookmark(request);
if(!result.IsSuccessful) return BadRequest(); if(!result.IsSuccessful) return BadRequest();
@ -103,12 +105,12 @@ namespace YABA.API.Controllers
return Ok(new GenericResponse<int>(result)); return Ok(new GenericResponse<int>(result));
} }
[HttpDelete()] [HttpDelete]
[ProducesResponseType(typeof(IEnumerable<GenericResponse<int>>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<GenericResponse<int>>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> DeleteBookmarks(IEnumerable<int> ids) public async Task<IActionResult> DeleteBookmarks([FromBody] DeleteBookmarksRequest request)
{ {
var result = await _bookmarkService.DeleteBookmarks(ids); var result = await _bookmarkService.DeleteBookmarks(request.Ids);
if(result.All(x => !x.IsSuccessful)) return NotFound(); if(result.All(x => !x.IsSuccessful)) return NotFound();

View File

@ -0,0 +1,7 @@
namespace YABA.API.ViewModels
{
public class DeleteBookmarksRequest
{
public IEnumerable<int> Ids { get; set; }
}
}

View File

@ -11,11 +11,10 @@ namespace YABA.Common.DTOs.Bookmarks
public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset CreatedOn { get; set; }
public DateTimeOffset LastModified { get; set; } public DateTimeOffset LastModified { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string? Description { get; set; }
public string Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public string Url { get; set; } public string Url { get; set; }
public IList<TagSummaryDTO> Tags { get; set; } = new List<TagSummaryDTO>(); public IList<TagSummaryDTO> Tags { get; set; } = new List<TagSummaryDTO>();
} }
} }

View File

@ -1,13 +1,16 @@
using YABA.Common.Interfaces; using System.ComponentModel.DataAnnotations;
using YABA.Common.Interfaces;
namespace YABA.Common.DTOs.Bookmarks namespace YABA.Common.DTOs.Bookmarks
{ {
public class CreateBookmarkRequestDTO : IBookmark public class CreateBookmarkRequestDTO : IBookmark
{ {
public string Title { get; set; } public string? Title { get; set; }
public string Description { get; set; } public string? Description { get; set; }
public string Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
[Required]
public string Url { get; set; } public string Url { get; set; }
} }
} }

View File

@ -1,12 +1,16 @@
using YABA.Common.Interfaces; using System.ComponentModel.DataAnnotations;
using YABA.Common.Interfaces;
namespace YABA.Common.DTOs.Bookmarks namespace YABA.Common.DTOs.Bookmarks
{ {
public class UpdateBookmarkRequestDTO : IBookmark public class UpdateBookmarkRequestDTO : IBookmark
{ {
public string Title { get; set; } public string? Title { get; set; }
public string Description { get; set; } public string? Description { get; set; }
public string Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
[Required]
public string Url { get; set; }
} }
} }

View File

@ -4,8 +4,9 @@ namespace YABA.Common.Interfaces
public interface IBookmark public interface IBookmark
{ {
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string? Description { get; set; }
public string Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public string Url { get; set; }
} }
} }

View File

@ -0,0 +1,210 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using YABA.Data.Context;
namespace YABA.Data.Migrations
{
[DbContext(typeof(YABABaseContext))]
[Migration("20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional")]
partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.17")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("YABA.Models.Bookmark", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTimeOffset>("CreatedOn")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_on");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<bool>("IsHidden")
.HasColumnType("boolean")
.HasColumnName("is_hidden");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.Property<string>("Note")
.HasColumnType("text")
.HasColumnName("note");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text")
.HasColumnName("title");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text")
.HasColumnName("url");
b.Property<int>("UserId")
.HasColumnType("integer")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_bookmarks");
b.HasIndex("UserId")
.HasDatabaseName("ix_bookmarks_user_id");
b.ToTable("bookmarks");
});
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
{
b.Property<int>("BookmarkId")
.HasColumnType("integer")
.HasColumnName("bookmark_id");
b.Property<int>("TagId")
.HasColumnType("integer")
.HasColumnName("tag_id");
b.HasKey("BookmarkId", "TagId")
.HasName("pk_bookmark_tags");
b.HasIndex("TagId")
.HasDatabaseName("ix_bookmark_tags_tag_id");
b.ToTable("bookmark_tags");
});
modelBuilder.Entity("YABA.Models.Tag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("IsHidden")
.HasColumnType("boolean")
.HasColumnName("is_hidden");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<int>("UserId")
.HasColumnType("integer")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_tags");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("ix_tags_name");
b.HasIndex("UserId")
.HasDatabaseName("ix_tags_user_id");
b.ToTable("tags");
});
modelBuilder.Entity("YABA.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Auth0Id")
.IsRequired()
.HasColumnType("text")
.HasColumnName("auth0id");
b.Property<DateTimeOffset>("CreatedOn")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_on");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean")
.HasColumnName("is_deleted");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_modified");
b.HasKey("Id")
.HasName("pk_users");
b.HasIndex("Auth0Id")
.IsUnique()
.HasDatabaseName("ix_users_auth0id");
b.ToTable("users");
});
modelBuilder.Entity("YABA.Models.Bookmark", b =>
{
b.HasOne("YABA.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.HasConstraintName("fk_bookmarks_users_user_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
{
b.HasOne("YABA.Models.Bookmark", "Bookmark")
.WithMany()
.HasForeignKey("BookmarkId")
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("YABA.Models.Tag", "Tag")
.WithMany()
.HasForeignKey("TagId")
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Bookmark");
b.Navigation("Tag");
});
modelBuilder.Entity("YABA.Models.Tag", b =>
{
b.HasOne("YABA.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.HasConstraintName("fk_tags_users_user_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace YABA.Data.Migrations
{
public partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "note",
table: "bookmarks",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "description",
table: "bookmarks",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "note",
table: "bookmarks",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "description",
table: "bookmarks",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
}
}

View File

@ -32,7 +32,6 @@ namespace YABA.Data.Migrations
.HasColumnName("created_on"); .HasColumnName("created_on");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired()
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("description"); .HasColumnName("description");
@ -45,7 +44,6 @@ namespace YABA.Data.Migrations
.HasColumnName("last_modified"); .HasColumnName("last_modified");
b.Property<string>("Note") b.Property<string>("Note")
.IsRequired()
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("note"); .HasColumnName("note");

View File

@ -12,8 +12,8 @@ namespace YABA.Models
public DateTimeOffset CreatedOn { get; set; } public DateTimeOffset CreatedOn { get; set; }
public DateTimeOffset LastModified { get; set; } public DateTimeOffset LastModified { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string? Description { get; set; }
public string Note { get; set; } public string? Note { get; set; }
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public string Url { get; set; } public string Url { get; set; }

View File

@ -1,8 +1,11 @@
using AutoMapper; using AutoMapper;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using YABA.Common.DTOs; using YABA.Common.DTOs;
using YABA.Common.DTOs.Bookmarks; using YABA.Common.DTOs.Bookmarks;
@ -84,6 +87,7 @@ namespace YABA.Service
} }
var bookmark = _mapper.Map<Bookmark>(request); var bookmark = _mapper.Map<Bookmark>(request);
UpdateBookmarkWithMetaData(bookmark);
bookmark.UserId = currentUserId; bookmark.UserId = currentUserId;
await _context.Bookmarks.AddAsync(bookmark); await _context.Bookmarks.AddAsync(bookmark);
@ -106,6 +110,8 @@ namespace YABA.Service
bookmark.Description = request.Description; bookmark.Description = request.Description;
bookmark.Note = request.Note; bookmark.Note = request.Note;
bookmark.IsHidden = request.IsHidden; bookmark.IsHidden = request.IsHidden;
bookmark.Url = request.Url;
UpdateBookmarkWithMetaData(bookmark);
if (await _context.SaveChangesAsync() > 0) crudResult.CrudResult = CrudResultLookup.UpdateSucceeded; if (await _context.SaveChangesAsync() > 0) crudResult.CrudResult = CrudResultLookup.UpdateSucceeded;
@ -216,5 +222,32 @@ namespace YABA.Service
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId); int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
return userId; return userId;
} }
private void UpdateBookmarkWithMetaData(IBookmark bookmark)
{
var webClient = new WebClient();
var sourceData = webClient.DownloadString(bookmark.Url);
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
var description = string.Empty;
var getHtmlDoc = new HtmlWeb();
var document = getHtmlDoc.Load(bookmark.Url);
var metaTags = document.DocumentNode.SelectNodes("//meta");
if (metaTags != null)
{
foreach (var sitetag in metaTags)
{
if (sitetag.Attributes["name"] != null && sitetag.Attributes["content"] != null && sitetag.Attributes["name"].Value == "description")
{
description = sitetag.Attributes["content"].Value;
}
}
}
bookmark.Title = !string.IsNullOrEmpty(bookmark.Title) ? bookmark.Title : string.IsNullOrEmpty(title) ? bookmark.Url : title;
bookmark.Description = !string.IsNullOrEmpty(bookmark.Description) ? bookmark.Description : description;
}
} }
} }

View File

@ -7,6 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" /> <PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />