From 31212f6e6145c4f2c519b0bf92735c217c72febe Mon Sep 17 00:00:00 2001 From: csc530 <77406318+csc530@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:07:24 -0400 Subject: [PATCH] :sparkles: add ability to add education to resume created CRUD commands and added education to md and txt export formats --- .run/add education.run.xml | 20 ++ .run/edit education.run.xml | 20 ++ .run/get educations.run.xml | 20 ++ Resumer/Helpers.cs | 14 +- ...240731173406_AddEducationTable.Designer.cs | 256 ++++++++++++++++++ .../20240731173406_AddEducationTable.cs | 112 ++++++++ .../Migrations/ResumeContextModelSnapshot.cs | 21 +- Resumer/Program.cs | 29 ++ Resumer/cli/commands/ExportCommand.cs | 31 ++- .../cli/commands/add/AddEducationCommand.cs | 57 ++++ .../commands/delete/DeleteEducationCommand.cs | 9 + .../cli/commands/edit/EditEducationCommand.cs | 56 ++++ .../cli/commands/get/GetEducationCommand.cs | 58 ++++ Resumer/models/Education.cs | 10 +- Resumer/models/Profile.cs | 3 +- Resumer/models/Resume.cs | 105 ++++--- Resumer/models/ResumeContext.cs | 1 + Resumer/models/TypstTemplate.cs | 8 +- 18 files changed, 755 insertions(+), 75 deletions(-) create mode 100644 .run/add education.run.xml create mode 100644 .run/edit education.run.xml create mode 100644 .run/get educations.run.xml create mode 100644 Resumer/Migrations/20240731173406_AddEducationTable.Designer.cs create mode 100644 Resumer/Migrations/20240731173406_AddEducationTable.cs create mode 100644 Resumer/cli/commands/add/AddEducationCommand.cs create mode 100644 Resumer/cli/commands/delete/DeleteEducationCommand.cs create mode 100644 Resumer/cli/commands/edit/EditEducationCommand.cs create mode 100644 Resumer/cli/commands/get/GetEducationCommand.cs diff --git a/.run/add education.run.xml b/.run/add education.run.xml new file mode 100644 index 0000000..ddde93d --- /dev/null +++ b/.run/add education.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/edit education.run.xml b/.run/edit education.run.xml new file mode 100644 index 0000000..5338d53 --- /dev/null +++ b/.run/edit education.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.run/get educations.run.xml b/.run/get educations.run.xml new file mode 100644 index 0000000..624a308 --- /dev/null +++ b/.run/get educations.run.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/Resumer/Helpers.cs b/Resumer/Helpers.cs index d9d8be0..d4b1eb5 100644 --- a/Resumer/Helpers.cs +++ b/Resumer/Helpers.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Immutable; using System.Text; +using System.Text.RegularExpressions; using Spectre.Console; namespace Resumer; @@ -21,13 +22,14 @@ public static partial class Utility /// /// the type of the prompt input /// the prompt message + /// a of nullable public static TextPrompt SimplePrompt(string message) => - new TextPrompt(message).AllowEmpty().HideDefaultValue().DefaultValue(default); + new TextPrompt(message).AllowEmpty().HideDefaultValue().DefaultValue(default(T?)); /// /// the default value - public static TextPrompt SimplePrompt(string message, T defaultValue) => new TextPrompt(message) - .AllowEmpty().DefaultValue(defaultValue).HideDefaultValue(); + public static TextPrompt SimplePrompt(string message, T? defaultValue) => + SimplePrompt(message).DefaultValue(defaultValue); /// /// converts a string to camel case @@ -56,7 +58,7 @@ public static string PrintDuration(DateOnly? startDate, DateOnly? endDate = null startDate == null ? string.Empty : $"{startDate:MMM yyyy} - {endDate?.ToString("MMM yyyy") ?? "present"}"; } -public static class Extensions +public static partial class Extensions { // todo: inquire about default value being a property - spectre console pr/iss // .DefaultValue(textPrompt); @@ -75,6 +77,7 @@ public static string Print(this object? value) => null => string.Empty, bool bit => bit ? "true" : "false", string txt => txt, + Enum @enum => NextToUppercaseRegex().Replace(@enum.ToString(), "$1 $2").Replace('_', '-'), DictionaryEntry pair => $"{pair.Key.Print()}: {pair.Value.Print()}", IDictionary dictionary => string.Join("\n", dictionary.Cast().Select(obj => $"- {obj.Print()}")), IEnumerable enumerable => string.Join("\n", enumerable.Cast().Select(obj => $"+ {obj.Print()}")), @@ -278,4 +281,7 @@ public static void EditFromPrompt(this List description, string prompt) } } while(i < count || !string.IsNullOrWhiteSpace(input)); } + + [GeneratedRegex(@"(\w)([A-Z])")] + private static partial Regex NextToUppercaseRegex(); } \ No newline at end of file diff --git a/Resumer/Migrations/20240731173406_AddEducationTable.Designer.cs b/Resumer/Migrations/20240731173406_AddEducationTable.Designer.cs new file mode 100644 index 0000000..dec0775 --- /dev/null +++ b/Resumer/Migrations/20240731173406_AddEducationTable.Designer.cs @@ -0,0 +1,256 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Resumer.models; + +#nullable disable + +namespace Resumer.Migrations +{ + [DbContext(typeof(ResumeContext))] + [Migration("20240731173406_AddEducationTable")] + partial class AddEducationTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Resumer.models.Certificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("IssueDate") + .HasColumnType("TEXT"); + + b.Property("Issuer") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("Certificate"); + }); + + modelBuilder.Entity("Resumer.models.Education", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdditionalInformation") + .HasColumnType("TEXT"); + + b.Property("Courses") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Degree") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("FieldOfStudy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GradePointAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("School") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Education"); + }); + + modelBuilder.Entity("Resumer.models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Company") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Resumer.models.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Interests") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Languages") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Objective") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Website") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Profiles"); + }); + + modelBuilder.Entity("Resumer.models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Details") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Resumer.models.Skill", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Name"); + + b.ToTable("Skills"); + }); + + modelBuilder.Entity("Resumer.models.TypstTemplate", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("Resumer.models.Certificate", b => + { + b.HasOne("Resumer.models.Profile", null) + .WithMany("Certifications") + .HasForeignKey("ProfileId"); + }); + + modelBuilder.Entity("Resumer.models.Profile", b => + { + b.Navigation("Certifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Resumer/Migrations/20240731173406_AddEducationTable.cs b/Resumer/Migrations/20240731173406_AddEducationTable.cs new file mode 100644 index 0000000..3d02653 --- /dev/null +++ b/Resumer/Migrations/20240731173406_AddEducationTable.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Resumer.Migrations +{ + /// + public partial class AddEducationTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Education_Profiles_ProfileId", + table: "Education"); + + migrationBuilder.DropIndex( + name: "IX_Education_ProfileId", + table: "Education"); + + migrationBuilder.DropColumn( + name: "ProfileId", + table: "Education"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Templates", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "FieldOfStudy", + table: "Education", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Degree", + table: "Education", + type: "INTEGER", + nullable: false, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AddColumn( + name: "Courses", + table: "Education", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Courses", + table: "Education"); + + migrationBuilder.AlterColumn( + name: "Description", + table: "Templates", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FieldOfStudy", + table: "Education", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "Degree", + table: "Education", + type: "TEXT", + nullable: false, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddColumn( + name: "ProfileId", + table: "Education", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Education_ProfileId", + table: "Education", + column: "ProfileId"); + + migrationBuilder.AddForeignKey( + name: "FK_Education_Profiles_ProfileId", + table: "Education", + column: "ProfileId", + principalTable: "Profiles", + principalColumn: "Id"); + } + } +} diff --git a/Resumer/Migrations/ResumeContextModelSnapshot.cs b/Resumer/Migrations/ResumeContextModelSnapshot.cs index 50fe91b..6bb459a 100644 --- a/Resumer/Migrations/ResumeContextModelSnapshot.cs +++ b/Resumer/Migrations/ResumeContextModelSnapshot.cs @@ -64,14 +64,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AdditionalInformation") .HasColumnType("TEXT"); - b.Property("Degree") + b.Property("Courses") .IsRequired() .HasColumnType("TEXT"); + b.Property("Degree") + .HasColumnType("INTEGER"); + b.Property("EndDate") .HasColumnType("TEXT"); b.Property("FieldOfStudy") + .IsRequired() .HasColumnType("TEXT"); b.Property("GradePointAverage") @@ -80,9 +84,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Location") .HasColumnType("TEXT"); - b.Property("ProfileId") - .HasColumnType("TEXT"); - b.Property("School") .IsRequired() .HasColumnType("TEXT"); @@ -92,8 +93,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("ProfileId"); - b.ToTable("Education"); }); @@ -230,7 +229,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Description") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Name"); @@ -245,18 +243,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ProfileId"); }); - modelBuilder.Entity("Resumer.models.Education", b => - { - b.HasOne("Resumer.models.Profile", null) - .WithMany("Education") - .HasForeignKey("ProfileId"); - }); - modelBuilder.Entity("Resumer.models.Profile", b => { b.Navigation("Certifications"); - - b.Navigation("Education"); }); #pragma warning restore 612, 618 } diff --git a/Resumer/Program.cs b/Resumer/Program.cs index 4f2d37a..a10edb2 100644 --- a/Resumer/Program.cs +++ b/Resumer/Program.cs @@ -70,6 +70,14 @@ public static void AppConfiguration(IConfigurator config) add.AddCommand("template") .WithAlias("t") .WithDescription("add a typst file template to export resume in pdf format"); + add.AddCommand("education") + .WithAlias("e") + .WithAlias("edu") + .WithAlias("educations") + .WithAlias("schools") + .WithAlias("school") + .WithAlias("degree") + .WithDescription("add a new education"); }); config.AddBranch("edit", edit => @@ -90,6 +98,13 @@ public static void AppConfiguration(IConfigurator config) .WithDescription("edit a project's details") .WithAlias("projects") .WithAlias("proj"); + edit.AddCommand("education") + .WithAlias("edu") + .WithAlias("educations") + .WithAlias("schools") + .WithAlias("school") + .WithAlias("degree") + .WithDescription("edit an education's details"); }); config.AddBranch("delete", delete => @@ -117,6 +132,13 @@ public static void AppConfiguration(IConfigurator config) .WithDescription("delete a typst file template") .WithAlias("t") .WithAlias("templates"); + delete.AddCommand("education") + .WithAlias("edu") + .WithAlias("educations") + .WithAlias("schools") + .WithAlias("school") + .WithAlias("degree") + .WithDescription("delete an education"); }) .WithAlias("d") .WithAlias("del") @@ -160,6 +182,13 @@ public static void AppConfiguration(IConfigurator config) .WithAlias("t") .WithAlias("templates") .WithDescription("list typst templates"); + get.AddCommand("education") + .WithAlias("edu") + .WithAlias("educations") + .WithAlias("schools") + .WithAlias("school") + .WithAlias("degree") + .WithDescription("list education"); }) .WithAlias("g") .WithAlias("list") diff --git a/Resumer/cli/commands/ExportCommand.cs b/Resumer/cli/commands/ExportCommand.cs index df94c84..962b2d4 100644 --- a/Resumer/cli/commands/ExportCommand.cs +++ b/Resumer/cli/commands/ExportCommand.cs @@ -24,6 +24,7 @@ public override int Execute(CommandContext context, ExportCommandSettings settin List jobs = []; List skills = []; List projects = []; + List education = []; var formatPrompt = new SelectionPrompt() .Title("Select export format") @@ -38,7 +39,9 @@ public override int Execute(CommandContext context, ExportCommandSettings settin var profile = AnsiConsole.Prompt(new SelectionPrompt() .Title("Select profile") - .AddChoices(dbProfiles.AsEnumerable().OrderBy(profile => profile.WholeName))); + .AddChoices(dbProfiles.AsEnumerable().OrderBy(profile => profile.WholeName)) + .WrapAround() + ); if(!db.Jobs.Any()) CommandOutput.Warn("No jobs found"); @@ -46,7 +49,9 @@ public override int Execute(CommandContext context, ExportCommandSettings settin jobs = AnsiConsole.Prompt(new MultiSelectionPrompt() .Title("Select jobs") .AddChoices(db.Jobs.OrderByDescending(job => job.StartDate)) - .NotRequired()); + .WrapAround() + .NotRequired() + ); if(!db.Skills.Any()) CommandOutput.Warn("No skills found"); @@ -54,7 +59,19 @@ public override int Execute(CommandContext context, ExportCommandSettings settin skills = AnsiConsole.Prompt(new MultiSelectionPrompt() .Title("Select skills") .AddChoices(db.Skills) - .NotRequired()); + .WrapAround() + .NotRequired() + ); + + if(!db.Education.Any()) + CommandOutput.Warn("No education found"); + else + education = AnsiConsole.Prompt(new MultiSelectionPrompt() + .Title("Select education") + .AddChoices(db.Education) + .WrapAround() + .NotRequired() + ); if(!db.Projects.Any()) CommandOutput.Warn("No projects found"); @@ -62,7 +79,9 @@ public override int Execute(CommandContext context, ExportCommandSettings settin projects = AnsiConsole.Prompt(new MultiSelectionPrompt() .Title("Select projects") .AddChoices(db.Projects) - .NotRequired()); + .WrapAround() + .NotRequired() + ); var template = TypstTemplate.Default; @@ -70,7 +89,8 @@ public override int Execute(CommandContext context, ExportCommandSettings settin template = AnsiConsole.Prompt(new SelectionPrompt() .Title("Select template") .AddChoices(db.Templates) - .WrapAround()); + .WrapAround() + ); var resume = new Resume(name) { @@ -78,6 +98,7 @@ public override int Execute(CommandContext context, ExportCommandSettings settin Jobs = jobs, Skills = skills, Projects = projects, + Education = education, }; var bytes = format switch diff --git a/Resumer/cli/commands/add/AddEducationCommand.cs b/Resumer/cli/commands/add/AddEducationCommand.cs new file mode 100644 index 0000000..36317bb --- /dev/null +++ b/Resumer/cli/commands/add/AddEducationCommand.cs @@ -0,0 +1,57 @@ +using System.Text.RegularExpressions; +using Resumer.models; +using Spectre.Console; +using Spectre.Console.Cli; +using static Resumer.Utility; + +namespace Resumer.cli.commands.add; + +public class AddEducationCommand: AddCommand +{ + protected override string ContinuePrompt => "Add another education?"; + + protected override int AddItem(CommandContext context, AddCommandSettings settings) + { + var school = AnsiConsole.Ask("School (name):"); + var degree = + AnsiConsole.Prompt(new SelectionPrompt() + .AddChoices(Enum.GetValues()) + .EnableSearch() + .UseConverter(certification => certification.Print()) + .WrapAround() + ); + AnsiConsole.WriteLine($"Degree: {degree}"); + var fieldOfStudy = AnsiConsole.Ask("Field (or level) of study:"); + var location = AnsiConsole.Prompt(SimplePrompt("Location:")); + var gpa = AnsiConsole.Prompt(SimplePrompt("Grade point average:")); + var startDate = AnsiConsole.Ask("Start date:"); + var endDate = AnsiConsole.Prompt(SimplePrompt("End date or expected graduation date (optional):")); + + AnsiConsole.WriteLine("\nHighlight certain courses, classes, subjects, projects, etc."); + AnsiConsole.MarkupLine("Press [bold]Enter[/] to skip."); + var courses = new List(); + courses.AddFromPrompt("Course:"); + + var additionalInformation = AnsiConsole.Prompt(SimplePrompt("Additional information:")); + + var education = new Education() + { + School = school, + Degree = degree, + Courses = courses, + + Location = location, + FieldOfStudy = fieldOfStudy, + GradePointAverage = gpa, + + StartDate = startDate, + EndDate = endDate, + AdditionalInformation = additionalInformation, + }; + + var db = new ResumeContext(); + db.Education.Add(education); + db.SaveChanges(); + return CommandOutput.Success(); + } +} \ No newline at end of file diff --git a/Resumer/cli/commands/delete/DeleteEducationCommand.cs b/Resumer/cli/commands/delete/DeleteEducationCommand.cs new file mode 100644 index 0000000..9afd7a3 --- /dev/null +++ b/Resumer/cli/commands/delete/DeleteEducationCommand.cs @@ -0,0 +1,9 @@ +using Microsoft.EntityFrameworkCore; +using Resumer.models; + +namespace Resumer.cli.commands.delete; + +public class DeleteEducationCommand: DeleteCommand +{ + protected override DbSet DbSet => Db.Education; +} \ No newline at end of file diff --git a/Resumer/cli/commands/edit/EditEducationCommand.cs b/Resumer/cli/commands/edit/EditEducationCommand.cs new file mode 100644 index 0000000..85415f3 --- /dev/null +++ b/Resumer/cli/commands/edit/EditEducationCommand.cs @@ -0,0 +1,56 @@ +using System.Runtime.ConstrainedExecution; +using Resumer.models; +using Spectre.Console; +using Spectre.Console.Cli; +using Command = Spectre.Console.Cli.Command; + +namespace Resumer.cli.commands.edit; + +public class EditEducationCommand: Command +{ + public override int Execute(CommandContext context) + { + var db = new ResumeContext(); + var educationList = db.Education.ToList(); + + AnsiConsole.WriteLine("select an education to edit..."); + var education = AnsiConsole.Prompt( + new SelectionPrompt() + .AddChoices(educationList) + .WrapAround() + ); + AnsiConsole.WriteLine($"Editing: {education}"); + + var school = AnsiConsole.Prompt(new TextPrompt("School").DefaultValue(education.School)); + var degree = + AnsiConsole.Prompt(new SelectionPrompt().AddChoices(Enum.GetValues())); + var location = AnsiConsole.Prompt(new TextPrompt("Location:").DefaultValue(education.Location)); + var gpa = AnsiConsole.Prompt( + new TextPrompt("Grade point average:").DefaultValue(education.GradePointAverage)); + var field = AnsiConsole.Prompt( + new TextPrompt("Field (or level) of study:").DefaultValue(education.FieldOfStudy)); + var start = AnsiConsole.Prompt(new TextPrompt("Start date:").DefaultValue(education.StartDate)); + var end = AnsiConsole.Prompt(new TextPrompt("End date:").DefaultValue(education.EndDate)); + AnsiConsole.WriteLine("\nHighlight certain courses, classes, subjects, projects, etc."); + AnsiConsole.MarkupLine("Press [bold]Enter[/] to skip."); + var courses = new List(); + courses.EditFromPrompt("Course:"); + var additionalInformation = + AnsiConsole.Prompt( + new TextPrompt("Additional information:").DefaultValue(education.AdditionalInformation)); + + + education.School = school; + education.Degree = degree; + education.FieldOfStudy = field; + education.StartDate = start; + education.EndDate = end; + education.GradePointAverage = gpa; + education.Location = location; + education.AdditionalInformation = additionalInformation; + education.Courses = courses; + + db.SaveChanges(); + return CommandOutput.Success("education updated successfully"); + } +} \ No newline at end of file diff --git a/Resumer/cli/commands/get/GetEducationCommand.cs b/Resumer/cli/commands/get/GetEducationCommand.cs new file mode 100644 index 0000000..cf2cb1c --- /dev/null +++ b/Resumer/cli/commands/get/GetEducationCommand.cs @@ -0,0 +1,58 @@ +using Resumer.models; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Resumer.cli.commands.get; + +public class GetEducationCommand: Command +{ + public override int Execute(CommandContext context, GetEducationCommandSettings settings) + { + ResumeContext database = new(); + + var education = database.Education.ToList(); + if(education.Count == 0) + return CommandOutput.Success("No education found"); + else + { + var table = settings.CreateTable(); + if(table == null) + education.ForEach(edu => AnsiConsole.WriteLine(edu.ToString())); + else + { + education.ForEach(edu => settings.AddEducationToTable(table, edu)); + AnsiConsole.Write(table); + } + + return CommandOutput.Success(); + } + } +} + +public class GetEducationCommandSettings: OutputCommandSettings +{ + public override Table? CreateTable() + { + var table = CreateTable("Education"); + if(table == null) + return table; + table.AddColumns("School", "Location", "Degree", "Field of Study", "Start Date", "End Date", + "Grade Point Average", "Courses", "Additional Information"); + return table; + } + + public void AddEducationToTable(Table table, Education education) + { + table.AddRow( + education.School.Print(), + education.Location.Print(), + education.Degree.Print(), + education.FieldOfStudy, + education.StartDate.Print(), + education.EndDate == null ? "present" : education.EndDate.Value.Print(), + education.GradePointAverage.Print(), + education.Courses.Print(), + education.AdditionalInformation.Print() + ); + } +} \ No newline at end of file diff --git a/Resumer/models/Education.cs b/Resumer/models/Education.cs index 15a5c21..9086c19 100644 --- a/Resumer/models/Education.cs +++ b/Resumer/models/Education.cs @@ -1,16 +1,22 @@ +using System.ComponentModel; + namespace Resumer.models; public class Education { public Guid Id { get; init; } = Guid.NewGuid(); public string School { get; set; } - public string Degree { get; set; } - public string? FieldOfStudy { get; set; } + public Certification Degree { get; set; } + [Description("The level or field of study")] + public string FieldOfStudy { get; set; } public DateOnly StartDate { get; set; } public DateOnly? EndDate { get; set; } public double? GradePointAverage { get; set; } public string? Location { get; set; } public string? AdditionalInformation { get; set; } + public List Courses { get; set; } = []; + + public override string ToString() => $"{School} ({Degree.Print()}) {FieldOfStudy} - {StartDate:yyyy-M-d} - {(EndDate == null ? "present" : EndDate.Value.ToString("yyyy-M-d"))}"; } public enum Certification diff --git a/Resumer/models/Profile.cs b/Resumer/models/Profile.cs index e5d7f58..8a2062f 100644 --- a/Resumer/models/Profile.cs +++ b/Resumer/models/Profile.cs @@ -65,7 +65,6 @@ public required string EmailAddress public string? Location { get; set; } - public List Education { get; set; } = []; public List Interests { get; set; } = []; public List Languages { get; set; } = []; public List Certifications { get; set; } = []; @@ -125,5 +124,5 @@ public string FullName [NotMapped] public string Initials => $"{FirstName[0]}{MiddleName?[0]}{LastName[0]}"; - public override string ToString() => $"{WholeName} - {EmailAddress} - {PhoneNumber}"; + public override string ToString() => $"{WholeName} - {EmailAddress} ({PhoneNumber})"; } \ No newline at end of file diff --git a/Resumer/models/Resume.cs b/Resumer/models/Resume.cs index 38b8165..e1d0444 100644 --- a/Resumer/models/Resume.cs +++ b/Resumer/models/Resume.cs @@ -27,6 +27,7 @@ public string Name public List Jobs { get; set; } = []; public List Skills { get; set; } = []; public List Projects { get; set; } = []; + public List Education { get; set; } = []; public Resume(string name) { @@ -57,24 +58,14 @@ public static Resume ExampleResume() Issuer = f.Company.CompanyName(), Url = new Uri(f.Internet.Url()), }) - ) - .RuleFor(p => p.Education, f => f.Make(f.Random.Int(1, 10), () => new Education() - { - School = f.Company.CompanyName() + (f.Random.Bool() ? " University" : " College"), - Degree = f.PickRandom("Associate", "Bachelor", "Master", "Doctorate"), - StartDate = f.Date.PastDateOnly(f.Random.Number(10)), - EndDate = f.Random.Bool() ? null : f.Date.FutureDateOnly(f.Random.Number(10)), - FieldOfStudy = f.WaffleTitle().Trim(), - GradePointAverage = f.Random.Double(0d, 4d), - Location = f.Address.City() + ", " + f.Address.Country(), - AdditionalInformation = f.Random.Bool() ? f.WaffleText(includeHeading: false).Trim() : null, - })); + ); var jobFaker = new Faker() .CustomInstantiator(f => new Job(f.Name.JobTitle(), f.Company.CompanyName(f.Random.Number(0, 2).OrNull(f)))) .RuleFor(j => j.StartDate, f => f.Date.PastDateOnly(f.Random.Number(10))) .RuleFor(j => j.EndDate, f => f.Random.Bool() ? null : f.Date.FutureDateOnly(f.Random.Number(10))) - .RuleFor(j => j.Description, f => f.Make(f.Random.Int(1, 5), () => f.WaffleText(includeHeading: false).Trim())); + .RuleFor(j => j.Description, + f => f.Make(f.Random.Int(1, 5), () => f.WaffleText(includeHeading: false).Trim())); var projectFaker = new Faker() .CustomInstantiator(f => new Project(f.WaffleTitle().Trim())) @@ -87,7 +78,7 @@ public static Resume ExampleResume() var skillFaker = new Faker().CustomInstantiator(f => new Skill(f.Random.Bool() ? f.Name.JobArea() : f.WaffleTitle().Trim(), f.PickRandom())); - + var resume = new Resume("test") { Profile = profileFaker.Generate(), @@ -122,31 +113,57 @@ public string ExportToTxt() .AppendLine(sectionBreak) .AppendLine(Profile.Objective); - sb.AppendLine("WORK EXPERIENCE") - .AppendLine(sectionBreak); - foreach(var job in Jobs.OrderByDescending(j => j.StartDate)) + if(Jobs.Count != 0) + { + sb.AppendLine("WORK EXPERIENCE") + .AppendLine(sectionBreak); + foreach(var job in Jobs.OrderByDescending(j => j.StartDate)) + { + sb.AppendLine(job.Title) + .AppendLine(job.Company); + // if(job.Location != null) + // sb.AppendLine($" - {job.Location}"); + sb.AppendLine(Utility.PrintDuration(job.StartDate, job.EndDate)); + sb.Append(job.Description.Print()); + sb.AppendLine(); + } + } + + if(Skills.Count != 0) { - sb.AppendLine(job.Title) - .AppendLine(job.Company); - // if(job.Location != null) - // sb.AppendLine($" - {job.Location}"); - sb.AppendLine(Utility.PrintDuration(job.StartDate, job.EndDate)); - foreach(var description in job.Description) - sb.AppendLine($"+ {description}"); + sb.AppendLine("SKILLS") + .AppendLine(sectionBreak); + sb.AppendLine(Skills.OrderBy(s => s.Name).Print()); sb.AppendLine(); } - sb.AppendLine("SKILLS") - .AppendLine(sectionBreak); - foreach(var skill in Skills.OrderBy(skill => skill.Name)) - sb.AppendLine(skill.Name); - sb.AppendLine("PROJECTS") - .AppendLine(sectionBreak); - foreach(var project in Projects) - sb.AppendLine(project.Title) - .AppendLine(Utility.PrintDuration(project.StartDate, project.EndDate)) - .AppendLine(project.Description); + if(Education.Count != 0) + { + sb.AppendLine("EDUCATION") + .AppendLine(sectionBreak); + foreach(var education in Education) + sb.AppendLine($"{education.School}, {education.FieldOfStudy} - {education.Degree}") + .AppendLine(Utility.PrintDuration(education.StartDate, education.EndDate)) + .Append("Courses: ").AppendJoin(',', education.Courses) + .AppendLine(education.AdditionalInformation) + .AppendLine(); + } + + + if(Projects.Count != 0) + { + sb.AppendLine("PROJECTS") + .AppendLine(sectionBreak); + foreach(var project in Projects) + { + sb.AppendLine(project.Title) + .AppendLine(Utility.PrintDuration(project.StartDate, project.EndDate)); + if(!string.IsNullOrWhiteSpace(project.Description)) + sb.AppendLine(project.Description); + sb.AppendLine(); + } + } sb.AppendLine("REFERENCES") .AppendLine(sectionBreak) @@ -158,7 +175,7 @@ public string ExportToTxt() public string ExportToJson() { // https://jsonresume.org/schema - var obj = new JsonResume(Profile, Jobs, Skills, Projects); + var obj = new JsonResume(Profile, Education, Jobs, Skills, Projects); var typeInfo = SourceGenerationContext.Default.JsonResume; return JsonSerializer.Serialize(obj, typeInfo); @@ -204,8 +221,7 @@ public string ExportToMarkdown() { sb.AppendLine("## Skills") .AppendLine(); - foreach(var skill in Skills.OrderBy(skill => skill.Name)) - sb.AppendLine($"- {skill.Name}"); + sb.AppendLine(Skills.OrderBy(skill => skill.Name).Print()); sb.AppendLine(); } @@ -221,6 +237,10 @@ public string ExportToMarkdown() .AppendLine(); } + sb.AppendLine("## REFERENCES") + .AppendLine() + .AppendLine("Available upon request"); + return sb.ToString(); } @@ -240,7 +260,7 @@ private string PrintAsTypstVariables(bool prettyPrint) builder.AppendLine(TypVarDeclr(nameof(Profile.Website), Profile.Website)); builder.AppendLine(TypVarDeclr(nameof(Profile.Objective), Profile.Objective)); - builder.AppendLine(TypVarDeclr(nameof(Profile.Education), Profile.Education)); + builder.AppendLine(TypVarDeclr(nameof(Education), Education)); builder.AppendLine(TypVarDeclr(nameof(Profile.Languages), Profile.Languages)); builder.AppendLine(TypVarDeclr(nameof(Profile.Interests), Profile.Interests)); @@ -321,7 +341,8 @@ internal class JsonResume private const string DateFormat = "yyyy-MM-dd"; - public JsonResume(Profile profile, IEnumerable jobs, IEnumerable skills, + public JsonResume(Profile profile, IEnumerable education, IEnumerable jobs, + IEnumerable skills, IEnumerable projects) { basics = new JsonBasics @@ -357,11 +378,11 @@ public JsonResume(Profile profile, IEnumerable jobs, IEnumerable ski languages = profile.Languages.Select(lang => new JsonLanguages { language = lang }).ToList(); - education = profile.Education.Select(edu => new JsonEducation + this.education = education.Select(edu => new JsonEducation { institution = edu.School, area = edu.FieldOfStudy, - studyType = edu.Degree, + studyType = edu.Degree.Print(), startDate = edu.StartDate.ToString(DateFormat), endDate = edu.EndDate?.ToString(DateFormat), score = edu.GradePointAverage.ToString(), @@ -446,7 +467,7 @@ public class JsonEducation /// /// area/field of study /// - public string? area { get; set; } + public string area { get; set; } /// /// what level of study was it diff --git a/Resumer/models/ResumeContext.cs b/Resumer/models/ResumeContext.cs index 7512a54..febea7e 100644 --- a/Resumer/models/ResumeContext.cs +++ b/Resumer/models/ResumeContext.cs @@ -19,6 +19,7 @@ public ResumeContext() public DbSet Jobs { get; set; } public DbSet Projects { get; set; } public DbSet Profiles { get; set; } + public DbSet Education { get; set; } public DbSet Skills { get; set; } public DbSet Templates { get; set; } diff --git a/Resumer/models/TypstTemplate.cs b/Resumer/models/TypstTemplate.cs index 53375d8..85ba385 100644 --- a/Resumer/models/TypstTemplate.cs +++ b/Resumer/models/TypstTemplate.cs @@ -8,23 +8,23 @@ namespace Resumer.models; /// public class TypstTemplate { - [Key] public string Name { get; set; } + [Key] public string? Name { get; set; } public string Content { get; set; } - public string Description { get; set; } = string.Empty; + public string? Description { get; set; } = string.Empty; private const string DefaultContent = "#let dateFormat = \"[month repr:short]. [year repr:full sign:automatic]\"\n\n= #fullName\n== #phoneNumber\n== #emailAddress\n\n#location,\n\n#website\n\n#objective\n\n#if jobs != none {\n [== Work Experience\n\n #for job in jobs {\n [=== #job.title -- #job.company]\n\n job.startDate.display(dateFormat)\n \" - \"\n if job.endDate != none {\n job.endDate.display(dateFormat)\n } else { \"Present\" }\n\n linebreak()\n\n\n for desc in job.description {\n [- #desc]\n }\n }\n]}\n\n#if projects != none {\n [= Projects\n\n #for proj in projects {\n [=== #proj.title #if proj.type != none {[-- #proj.type]}]\n\n if proj.startDate != none { proj.startDate.display(dateFormat)\n \" - \"\n if proj.endDate != none {\n proj.endDate.display(dateFormat)\n } else { \"Present\" }\n }\n\n linebreak()\n\n proj.description\n\n for detail in proj.details {\n [- #detail]\n }\n }]\n}\n\n#if skills != none {\n [= Skills\n\n #for skill in skills {\n [- #skill.name]\n }]\n}\n\n#if education != none {\n [= Education\n\n #for edu in education {\n [=== #edu.school -- #edu.fieldOfStudy]\n\n if edu.startDate != none { edu.startDate.display(dateFormat)\n \" - \"\n if edu.endDate != none {\n edu.endDate.display(dateFormat)\n } else { \"Present\" }\n }\n\n linebreak()\n edu.location\n linebreak()\n\n edu.additionalInformation\n }]\n}\n\n#if languages != none {\n [= Languages\n\n #for lang in languages {\n [- #lang]\n }]\n}\n\n#if interests != none {\n [= Interests\n\n #for interest in interests {\n [- #interest]\n }]\n}"; public static TypstTemplate Default => new("default", DefaultContent) { Description = "Default template" }; - public TypstTemplate(string name, string content) + public TypstTemplate(string? name, string content) { Name = name; Content = content; } /// - public override string ToString() => string.IsNullOrWhiteSpace(Description) ? Name : $"{Name} - {Description}"; + public override string? ToString() => string.IsNullOrWhiteSpace(Description) ? Name : $"{Name} - {Description}"; /// /// Test if the template is valid typst file