diff --git a/.drone.yml b/.drone.yml index 78af971..05189d6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,12 @@ type: docker name: CI Workflow steps: - - name: Build + - name: Test image: hub.codefirst.iut.uca.fr/marc.chevaldonne/codefirst-dotnet7-maui:latest commands: - dotnet test --framework net7.0 'Tests\\\\Tests.csproj' + + - name: Deploy doc + image: hub.codefirst.iut.uca.fr/maxime.batista/codefirst-docdeployer:latest + commands: + - /entrypoint.sh -t doxygen -l . -d sourcecode_documentation \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8852f14 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# CS0618: Le type ou le membre est obsolète +dotnet_diagnostic.CS0618.severity = silent + +# CS1998: Async method lacks 'await' operators and will run synchronously +dotnet_diagnostic.CS1998.severity = none \ No newline at end of file diff --git a/App.xaml.cs b/App.xaml.cs index c873ce2..4e4d06b 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,31 +1,34 @@ namespace ShoopNCook; using Models; -using Endpoint; -using LocalEndpoint; +using Services; +using LocalServices; + +// Classe principale de l'application qui implémente l'interface ConnectionObserver et IApp public partial class App : Application, ConnectionObserver, IApp { - + // Initialisation de l'interface IEndpoint avec une nouvelle instance de LocalEndpoint private IEndpoint Endpoint = new LocalEndpoint(); - public IUserNotifier Notifier => new ConsoleUserNotifier(); - public App() { InitializeComponent(); - ForceLogin(); //start in login shell + ForceLogin(); // Commencer l'application avec l'écran de connexion } + + // Méthode appelée lorsque l'utilisateur se connecte avec succès public void OnAccountConnected(Account account) { Shell shell = new MainAppShell(account, Endpoint, this); - shell.GoToAsync("//Home"); + shell.GoToAsync("//Home"); MainPage = shell; } + // Méthode pour forcer l'utilisateur à se connecter public void ForceLogin() { - Shell shell = new ConnectAppShell(this, Endpoint.AuthService, Notifier); + Shell shell = new ConnectAppShell(this, Endpoint.AuthService); shell.GoToAsync("//Splash"); MainPage = shell; } diff --git a/ConnectAppShell.xaml.cs b/ConnectAppShell.xaml.cs index dd44352..bbd3198 100644 --- a/ConnectAppShell.xaml.cs +++ b/ConnectAppShell.xaml.cs @@ -1,17 +1,24 @@ namespace ShoopNCook; using Microsoft.Maui.Controls; using Models; -using Endpoint; +using Services; using ShoopNCook.Controllers; using ShoopNCook.Pages; + +// Shell pour la phase de connexion de l'application public partial class ConnectAppShell : Shell { - public ConnectAppShell(ConnectionObserver observer, IAuthService accounts, IUserNotifier notifier) + // Constructeur qui prend un observateur de connexion et un service d'authentification en argument + + public ConnectAppShell(ConnectionObserver observer, IAuthService accounts) { - ConnectionController controller = new ConnectionController(observer, accounts, notifier); + // Création d'un nouveau contrôleur de connexion + ConnectionController controller = new ConnectionController(observer, accounts); InitializeComponent(); - LoginPage.ContentTemplate = new DataTemplate(() => new LoginPage(controller)); + + // Initialisation des pages de connexion et d'inscription avec le contrôleur de connexion + LoginPage.ContentTemplate = new DataTemplate(() => new LoginPage(controller)); RegisterPage.ContentTemplate = new DataTemplate(() => new RegisterPage(controller)); } } diff --git a/ConnectionObserver.cs b/ConnectionObserver.cs index 6cd2772..ec4edd3 100644 --- a/ConnectionObserver.cs +++ b/ConnectionObserver.cs @@ -2,6 +2,8 @@ namespace ShoopNCook { + // Interface définissant un observateur de connexion. + // Tout objet implémentant cette interface doit définir la méthode OnAccountConnected(). public interface ConnectionObserver { public void OnAccountConnected(Account account); diff --git a/ConsoleUserNotifier.cs b/ConsoleUserNotifier.cs deleted file mode 100644 index 1a06c28..0000000 --- a/ConsoleUserNotifier.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace ShoopNCook -{ - /// - /// A notice reporter implementation that prints in console the applications's user notices. - /// - public class ConsoleUserNotifier : - IUserNotifier - { - - public void Success(string message) - { - Console.WriteLine(" Success: " + message); - } - - - public void Error(string message) - { - Console.WriteLine(" Error: " + message); - } - - public void Notice(string message) - { - Console.WriteLine(" Notice: " + message); - } - - public void Warn(string message) - { - Console.WriteLine(" Warn: " + message); - } - } -} diff --git a/Controllers/ConnectionController.cs b/Controllers/ConnectionController.cs index 0e59bb7..23903b7 100644 --- a/Controllers/ConnectionController.cs +++ b/Controllers/ConnectionController.cs @@ -1,4 +1,4 @@ -using Endpoint; +using Services; using Models; namespace ShoopNCook.Controllers @@ -7,19 +7,27 @@ namespace ShoopNCook.Controllers { private readonly ConnectionObserver observer; private readonly IAuthService accounts; - private readonly IUserNotifier notifier; - public ConnectionController(ConnectionObserver observer, IAuthService accounts, IUserNotifier notifier) { + public ConnectionController(ConnectionObserver observer, IAuthService accounts) { this.observer = observer; this.accounts = accounts; - this.notifier = notifier; } public void Login(string email, string password) { + if (email == null) + { + UserNotifier.Notice("Please provide an email address"); + return; + } + if (password == null) + { + UserNotifier.Notice("Please provide your password"); + return; + } Account? acc = accounts.Login(email, password); if (acc == null) { - notifier.Error("Email or password invalid."); + UserNotifier.Error("Email or password invalid."); return; } observer.OnAccountConnected(acc); @@ -27,10 +35,25 @@ namespace ShoopNCook.Controllers public void Register(string username, string email, string password) { + if (email == null) + { + UserNotifier.Notice("Please provide an email address"); + return; + } + if (password == null) + { + UserNotifier.Notice("Please provide your password"); + return; + } + if (username == null) + { + UserNotifier.Notice("Please provide an username"); + return; + } Account? acc = accounts.Register(username, email, password); if (acc == null) { - notifier.Error("Invalid credentials."); + UserNotifier.Error("Invalid credentials."); return; } observer.OnAccountConnected(acc); diff --git a/Controllers/MorePageController.cs b/Controllers/MorePageController.cs index c8ecee8..d92b5d1 100644 --- a/Controllers/MorePageController.cs +++ b/Controllers/MorePageController.cs @@ -1,4 +1,4 @@ -using Endpoint; +using Services; using Models; using ShoopNCook.Pages; @@ -20,18 +20,18 @@ namespace ShoopNCook.Controllers public void Logout() { - app.Notifier.Notice("You have been loged out."); + UserNotifier.Notice("You have been loged out."); app.ForceLogin(); } - public void GoToMyRecipesPage() + public async void GoToMyRecipesPage() { - Shell.Current.Navigation.PushAsync(new MyRecipesPage(account, endpoint.RecipesService, app.Notifier)); + await Shell.Current.Navigation.PushAsync(new MyRecipesPage(account, endpoint.RecipesService)); } - public void GoToProfilePage() + public async void GoToProfilePage() { - Shell.Current.Navigation.PushAsync(new ProfilePage(account)); + await Shell.Current.Navigation.PushAsync(new ProfilePage(account)); } } } diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..6cda83d --- /dev/null +++ b/Doxyfile @@ -0,0 +1,429 @@ +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "ShopNCook" +PROJECT_NUMBER = 1.0.0 +PROJECT_BRIEF = "A Cook application" +PROJECT_LOGO = Resources/appicon.svg +OUTPUT_DIRECTORY = /docs/doxygen +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = NO +JAVADOC_BANNER = NO +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +PYTHON_DOCSTRING = YES +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 4 +ALIASES = +OPTIMIZE_OUTPUT_FOR_C = NO +# Well... the one for Java looks so similar to the one for C#... +OPTIMIZE_OUTPUT_JAVA = YES +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +OPTIMIZE_OUTPUT_SLICE = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +TOC_INCLUDE_HEADINGS = 5 +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = NO +GROUP_NESTED_COMPOUNDS = NO +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 0 +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +EXTRACT_ALL = YES +# I do not like other members to see my private members... but you can set it to YES if you prefer. +EXTRACT_PRIVATE = NO +EXTRACT_PRIV_VIRTUAL = NO +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +RESOLVE_UNNAMED_PARAMS = YES +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = NO +HIDE_SCOPE_NAMES = NO +HIDE_COMPOUND_REFERENCE= NO +SHOW_HEADERFILE = YES +SHOW_INCLUDE_FILES = YES +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = NO +SORT_BRIEF_DOCS = NO +SORT_MEMBERS_CTORS_1ST = NO +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = YES +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_IF_INCOMPLETE_DOC = YES +WARN_NO_PARAMDOC = NO +WARN_AS_ERROR = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +INPUT = . +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.l \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice +RECURSIVE = YES +EXCLUDE = +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = */Tests/* +EXCLUDE_PATTERNS += */bin/* +EXCLUDE_PATTERNS += */obj/* +EXCLUDE_PATTERNS += documentation/* +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = * +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +CLANG_ASSISTED_PARSING = NO +CLANG_ADD_INC_PATHS = YES +CLANG_OPTIONS = +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +ALPHABETICAL_INDEX = YES +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = images/CodeFirst.png images/clubinfo.png +HTML_COLORSTYLE_HUE = 215 +HTML_COLORSTYLE_SAT = 45 +HTML_COLORSTYLE_GAMMA = 240 +HTML_TIMESTAMP = NO +HTML_DYNAMIC_MENUS = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_FEEDURL = +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = NO +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = NO +FULL_SIDEBAR = NO +ENUM_VALUES_PER_LINE = 4 +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +OBFUSCATE_EMAILS = YES +HTML_FORMULA_FORMAT = png +FORMULA_FONTSIZE = 10 +FORMULA_TRANSPARENT = YES +FORMULA_MACROFILE = +USE_MATHJAX = NO +MATHJAX_VERSION = MathJax_2 +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = YES +SERVER_BASED_SEARCH = NO +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +GENERATE_LATEX = NO +LATEX_OUTPUT = latex +LATEX_CMD_NAME = +MAKEINDEX_CMD_NAME = makeindex +LATEX_MAKEINDEX_CMD = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4 +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_STYLESHEET = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_BIB_STYLE = plain +LATEX_TIMESTAMP = NO +LATEX_EMOJI_DIRECTORY = + +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- + +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- + +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_SUBDIR = +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- + +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +XML_NS_MEMB_FILE_SCOPE = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- + +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook + +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- + +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +DIA_PATH = +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = NO +DOT_NUM_THREADS = 0 +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +DOT_UML_DETAILS = NO +DOT_WRAP_THRESHOLD = 17 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DIR_GRAPH_MAX_DEPTH = 1 +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +MSCFILE_DIRS = +DIAFILE_DIRS = +PLANTUML_JAR_PATH = +PLANTUML_CFG_FILE = +PLANTUML_INCLUDE_PATH = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 0 +DOT_TRANSPARENT = NO +DOT_MULTI_TARGETS = NO +GENERATE_LEGEND = YES +DOT_CLEANUP = YES \ No newline at end of file diff --git a/IApp.cs b/IApp.cs index dad1336..765ec6c 100644 --- a/IApp.cs +++ b/IApp.cs @@ -7,10 +7,10 @@ using System.Threading.Tasks; namespace ShoopNCook { + // Interface définissant une application. + // Tout objet implémentant cette interface doit définir la méthode ForceLogin(). public interface IApp { - public IUserNotifier Notifier { get; } - public void ForceLogin(); } } diff --git a/IUserNotifier.cs b/IUserNotifier.cs deleted file mode 100644 index 4576a70..0000000 --- a/IUserNotifier.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ShoopNCook -{ - public interface IUserNotifier - { - public void Success(string message); - - public void Notice(string message); - - public void Error(string message); - - public void Warn(string message); - } -} diff --git a/LocalServices/AccountOwnedRecipes.cs b/LocalServices/AccountOwnedRecipes.cs index 633d96e..34a27b2 100644 --- a/LocalServices/AccountOwnedRecipes.cs +++ b/LocalServices/AccountOwnedRecipes.cs @@ -1,19 +1,19 @@ -using LocalEndpoint; -using LocalEndpoint.Data; +using Services; +using LocalServices.Data; using Models; using System.Collections.Immutable; -namespace Endpoint +namespace Services { - internal class AccountOwnedRecipes : IAccountOwnedRecipes + public class AccountOwnedRecipes : IAccountOwnedRecipesService { public Account Account { get; init; } private readonly Dictionary ownedRecipes = new Dictionary(); - private readonly Database db; + private readonly IDatabase db; - public AccountOwnedRecipes(Account account, Database db) + public AccountOwnedRecipes(Account account, IDatabase db) { Account = account; this.db = db; @@ -48,7 +48,5 @@ namespace Endpoint { return ownedRecipes.Values.ToImmutableList().ConvertAll(r => r.Info); } - - } } diff --git a/LocalServices/AccountRecipesPreferences.cs b/LocalServices/AccountRecipesPreferences.cs index 520c767..052acfd 100644 --- a/LocalServices/AccountRecipesPreferences.cs +++ b/LocalServices/AccountRecipesPreferences.cs @@ -1,15 +1,15 @@ -using Endpoint; -using LocalEndpoint.Data; +using Services; +using LocalServices.Data; using Models; using System.Collections.Immutable; -namespace LocalEndpoint +namespace LocalServices { - internal class AccountRecipesPreferences : IAccountRecipesPreferences + public class AccountRecipesPreferences : IAccountRecipesPreferencesService { - private readonly Database db; - public AccountRecipesPreferences(Account account, Database db) + private readonly IDatabase db; + public AccountRecipesPreferences(Account account, IDatabase db) { Account = account; this.db = db; @@ -56,7 +56,7 @@ namespace LocalEndpoint public RecipeRate GetRate(RecipeInfo info) { - RecipeRate rate = null; + RecipeRate? rate = null; var ratings = db.ListRatesOf(Account.User.Id); if (!ratings.TryGetValue(info.Id, out rate)) @@ -102,7 +102,6 @@ namespace LocalEndpoint public void AddToFavorites(RecipeInfo info) { Guid userId = Account.User.Id; - var ratings = db.ListRatesOf(userId); RecipeRate rate = GetRate(info); db.InsertRate(userId, info.Id, new RecipeRate(true, rate.Rate)); @@ -111,7 +110,6 @@ namespace LocalEndpoint public void RemoveFromFavorites(RecipeInfo info) { Guid userId = Account.User.Id; - var ratings = db.ListRatesOf(userId); RecipeRate rate = GetRate(info); db.InsertRate(userId, info.Id, new RecipeRate(false, rate.Rate)); @@ -120,10 +118,9 @@ namespace LocalEndpoint public void SetReviewScore(RecipeInfo info, uint score) { Guid userId = Account.User.Id; - var ratings = db.ListRatesOf(userId); RecipeRate rate = GetRate(info); - db.InsertRate(userId, info.Id, new RecipeRate(rate.IsFavorite, score)); + db.InsertRate(userId, info.Id, new RecipeRate(rate.IsFavorite, Math.Min(score, 5))); } } } diff --git a/LocalServices/AccountServices.cs b/LocalServices/AccountServices.cs index 02d4d6a..656f3b7 100644 --- a/LocalServices/AccountServices.cs +++ b/LocalServices/AccountServices.cs @@ -1,6 +1,6 @@ -using Endpoint; +using Services; -namespace LocalEndpoint +namespace LocalServices { - internal record AccountServices(IAccountOwnedRecipes Recipes, IAccountRecipesPreferences Preferences); + internal record AccountServices(IAccountOwnedRecipesService Recipes, IAccountRecipesPreferencesService Preferences); } diff --git a/LocalServices/AuthService.cs b/LocalServices/AuthService.cs index 47ca4fb..a229afa 100644 --- a/LocalServices/AuthService.cs +++ b/LocalServices/AuthService.cs @@ -1,21 +1,15 @@ using Models; -using Endpoint; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using LocalEndpoint.Data; -using System.Security.Cryptography; +using Services; +using LocalServices.Data; -namespace LocalEndpoint +namespace LocalServices { - internal class AuthService : IAuthService + public class AuthService : IAuthService { - private readonly Database db; + private readonly IDatabase db; - public AuthService(Database db) + public AuthService(IDatabase db) { this.db = db; } diff --git a/LocalServices/Constants.cs b/LocalServices/Constants.cs index 75f80d1..cfb7bf8 100644 --- a/LocalServices/Constants.cs +++ b/LocalServices/Constants.cs @@ -1,8 +1,8 @@ using Models; -namespace LocalEndpoint +namespace LocalServices { - internal class Constants + public class Constants { public static readonly Uri DEFAULT_ACCOUNT_IMAGE = new Uri("https://www.pngkey.com/png/full/115-1150152_default-profile-picture-avatar-png-green.png"); } diff --git a/LocalServices/Data/AccountData.cs b/LocalServices/Data/AccountData.cs index 332c7e9..7199fa4 100644 --- a/LocalServices/Data/AccountData.cs +++ b/LocalServices/Data/AccountData.cs @@ -1,12 +1,6 @@ -using Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.Serialization; -namespace LocalEndpoint.Data +namespace LocalServices.Data { [DataContract] internal record AccountData( diff --git a/LocalServices/Data/CatastrophicPerformancesDatabase.cs b/LocalServices/Data/CatastrophicPerformancesDatabase.cs index 32c48c1..755a348 100644 --- a/LocalServices/Data/CatastrophicPerformancesDatabase.cs +++ b/LocalServices/Data/CatastrophicPerformancesDatabase.cs @@ -1,160 +1,185 @@ -using Models; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace LocalEndpoint.Data -{ - /// - /// Database implementation with catastrophic performances. - /// This database implementation persists data in xml and will save all the data in their files on each mutable requests. - /// - internal class CatastrophicPerformancesDatabase : Database - { - - private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - - private static readonly string RECIPES_FILENAME = "recipes_data.xml"; - private static readonly string USERS_FILENAME = "users_data.xml"; - private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; - - private readonly Dictionary recipesData; - private readonly Dictionary usersData; - private readonly Dictionary accountsData; - - private readonly string dbPath; - - - public CatastrophicPerformancesDatabase(string folderPath) - { - dbPath = folderPath; - if (!Directory.Exists(folderPath)) - Directory.CreateDirectory(folderPath); - - usersData = Load(USERS_FILENAME, USERS_SERIALIZER); - recipesData = Load(RECIPES_FILENAME, RECIPES_SERIALIZER); - accountsData = Load(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER); - } - - public bool IsEmpty() - { - return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; - } - - public Account? GetAccount(string email, string passwordHash) - { - if (!accountsData.TryGetValue(email, out AccountData? data)) - return null; - - if (data.PasswordHash != passwordHash) return null; - return new Account(usersData[data.UserId].User, data.Email); - } - - public void InsertAccount(Account account, string passwordHash) - { - accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); - Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData); - } - - public Recipe GetRecipe(Guid id) - { - return ConvertRecipeDataToRecipe(recipesData[id]); - } - - public RecipeRate GetRecipeRate(Guid user, Guid recipe) - { - return usersData[user].Rates[recipe]; - } - - public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) - { - usersData[userId].RecipesList[recipeId] = persAmount; - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public void RemoveFromUserList(Guid userId, Guid recipeId) - { - usersData[userId].RecipesList.Remove(recipeId); - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - - public void InsertRecipe(Recipe recipe) - { - recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); - Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); - } - - public void InsertUser(User user) - { - usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary()); - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) - { - usersData[userId].Rates[recipeId] = rate; - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public void RemoveRecipe(Guid id) - { - recipesData.Remove(id); - Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); - } - - public ImmutableList ListAllRecipes() - { - return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); - } - - - public ImmutableDictionary ListRatesOf(Guid user) - { - return usersData[user].Rates.ToImmutableDictionary(); - } - - public ImmutableDictionary GetRecipeListOf(Guid user) - { - return usersData[user].RecipesList.ToImmutableDictionary(); - } - - private Recipe ConvertRecipeDataToRecipe(RecipeData rd) - { - var owner = usersData[rd.OwnerID].User; - return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); - } - - private Dictionary Load(string fileName, DataContractSerializer deserializer) - { - var file = dbPath + "/" + fileName; - var fileInfo = new FileInfo(file); - - if (!fileInfo.Exists) - fileInfo.Create(); - - if (fileInfo.Length == 0) - return new Dictionary(); //file is empty thus there is nothing to deserialize - Console.WriteLine(File.ReadAllText(file)); - - using (var stream = File.OpenRead(file)) - return deserializer.ReadObject(stream) as Dictionary ?? throw new Exception("object read from " + file + " is not a dictionnary"); - } - - private void Save(string fileName, DataContractSerializer serializer, Dictionary dict) - { - using (var stream = File.OpenWrite(dbPath + "/" + fileName)) - { - serializer.WriteObject(stream, dict); - stream.Flush(); - } - } - - } -} +using Models; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; +using System.Text.Json; + +namespace LocalServices.Data +{ + /// + /// Database implementation with catastrophic performances. + /// This database implementation persists data in json and will save all the data in their files for each mutable requests. + /// + public class CatastrophicPerformancesDatabase : IDatabase + { + + private static readonly string RECIPES_FILENAME = "recipes_data.xml"; + private static readonly string USERS_FILENAME = "users_data.xml"; + private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; + + private readonly Dictionary recipesData; + private readonly Dictionary usersData; + private readonly Dictionary accountsData; + + private readonly string dbPath; + + + public CatastrophicPerformancesDatabase(string folderPath) + { + dbPath = folderPath; + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + usersData = Load(USERS_FILENAME); + recipesData = Load(RECIPES_FILENAME); + accountsData = Load(ACCOUNTS_FILENAME); + } + + public bool IsEmpty() + { + return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; + } + + public Account? GetAccount(string email, string passwordHash) + { + if (!accountsData.TryGetValue(email, out AccountData? data)) + return null; + + if (data.PasswordHash != passwordHash) return null; + return new Account(usersData[data.UserId].User, data.Email); + } + + public void InsertAccount(Account account, string passwordHash) + { + accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); + Save(ACCOUNTS_FILENAME, accountsData); + InsertUser(account.User); + } + + public Recipe? GetRecipe(Guid id) + { + if (recipesData.TryGetValue(id, out RecipeData? data)) + return ConvertRecipeDataToRecipe(data); + return null; + } + + public RecipeRate GetRecipeRate(Guid user, Guid recipe) + { + return usersData[user].Rates[recipe]; + } + + public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) + { + usersData[userId].RecipesList[recipeId] = persAmount; + Save(USERS_FILENAME, usersData); + } + + public void RemoveFromUserList(Guid userId, Guid recipeId) + { + usersData[userId].RecipesList.Remove(recipeId); + Save(USERS_FILENAME, usersData); + } + + + public void InsertRecipe(Recipe recipe) + { + recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); + Save(RECIPES_FILENAME, recipesData); + } + + public void InsertUser(User user) + { + usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary()); + Save(USERS_FILENAME, usersData); + } + + public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) + { + usersData[userId].Rates[recipeId] = rate; + Save(USERS_FILENAME, usersData); + } + + public void RemoveRecipe(Guid id) + { + recipesData.Remove(id); + Save(RECIPES_FILENAME, recipesData); + } + + public ImmutableList ListAllRecipes() + { + return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); + } + + + public ImmutableDictionary ListRatesOf(Guid user) + { + return usersData[user].Rates.ToImmutableDictionary(); + } + + public ImmutableDictionary GetRecipeListOf(Guid user) + { + return usersData[user].RecipesList.ToImmutableDictionary(); + } + + private Recipe ConvertRecipeDataToRecipe(RecipeData rd) + { + var owner = usersData[rd.OwnerID].User; + return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); + } + + private Dictionary Load(string fileName) + { + var file = dbPath + "/" + fileName; + var fileInfo = new FileInfo(file); + + if (!fileInfo.Exists) + fileInfo.Create(); + + if (fileInfo.Length == 0) + return new Dictionary(); //file is empty thus there is nothing to deserialize + + string text = File.ReadAllText(file); + + return JsonSerializer.Deserialize>(text); + } + + private async void Save(string fileName, Dictionary dict) + { + string json = JsonSerializer.Serialize(dict); + using (var stream = WaitForFile(dbPath + "/" + fileName, FileMode.Open, FileAccess.Write, FileShare.Write)) + { + var bytes = Encoding.ASCII.GetBytes(json); + await stream.WriteAsync(bytes, 0, bytes.Length); + } + } + + // This is a workaround function to wait for a file to be released before opening it. + // This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus. + // Simply wait until the file is released and return it. This function will never return until + private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share) + { + for (int attempt = 0 ; attempt < 40; attempt++) + { + FileStream? fs = null; + try + { + fs = new FileStream(fullPath, mode, access, share); + return fs; + } + catch (FileNotFoundException e) + { + throw e; + } + catch (IOException e) + { + Debug.WriteLine(e.Message + " in thread " + Thread.CurrentThread.Name + " " + Thread.CurrentThread.ManagedThreadId); + if (fs != null) + fs.Dispose(); + + Thread.Sleep(200); + } + } + throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached."); + } + } +} diff --git a/LocalServices/Data/Database.cs b/LocalServices/Data/Database.cs deleted file mode 100644 index 130093a..0000000 --- a/LocalServices/Data/Database.cs +++ /dev/null @@ -1,36 +0,0 @@ - -using Models; -using System.Collections.Immutable; - -namespace LocalEndpoint.Data -{ - - // The database interface defines all the different kinds of requests the LocalEndpoint needs to store and retrieve data. - public interface Database - { - - public Recipe GetRecipe(Guid id); - - public RecipeRate GetRecipeRate(Guid user, Guid recipe); - - public Account? GetAccount(string email, string passwordHash); - - public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount); - public void RemoveFromUserList(Guid userId, Guid recipeId); - public void InsertAccount(Account account, string passwordHash); - - public void InsertRecipe(Recipe recipe); - - public void InsertUser(User user); - - public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate); - - public void RemoveRecipe(Guid id); - - public ImmutableList ListAllRecipes(); - - public ImmutableDictionary ListRatesOf(Guid user); - - public ImmutableDictionary GetRecipeListOf(Guid user); - } -} diff --git a/LocalServices/Data/IDatabase.cs b/LocalServices/Data/IDatabase.cs new file mode 100644 index 0000000..4e6c1e3 --- /dev/null +++ b/LocalServices/Data/IDatabase.cs @@ -0,0 +1,103 @@ + +using Models; +using System.Collections.Immutable; + +namespace LocalServices.Data +{ + /// + /// The database interface defines all the different kinds of requests the LocalEndpoint needs to store and retrieve data. + /// + public interface IDatabase + { + /// + /// Get a Recipe from its identifier + /// + /// + /// The recipe if the identifier is registered in the database + + public Recipe? GetRecipe(Guid id); + /// + /// Get the rate of a user for a given recipe + /// + /// The user identifier + /// The recipe identifier + /// Returns a rate + public RecipeRate GetRecipeRate(Guid user, Guid recipe); + + /// + /// Gets an account from an email and a password hash + /// + /// + /// + /// some account if the email and the hash is found in the database + public Account? GetAccount(string email, string passwordHash); + + /// + /// Insert a recipe in user's weekly list + /// + /// + /// + /// + public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount); + + /// + /// Remove a recipe from user's weekly list + /// + /// + /// + public void RemoveFromUserList(Guid userId, Guid recipeId); + + /// + /// Inserts an account in the database, with the given passwordhash + /// + /// + /// + public void InsertAccount(Account account, string passwordHash); + + /// + /// Inserts a recipe + /// + /// + public void InsertRecipe(Recipe recipe); + + /// + /// Inserts a user + /// + /// + public void InsertUser(User user); + + /// + /// Inserts a user rate over the given recipe identifier + /// + /// + /// + /// + public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate); + + /// + /// Removes a recipe + /// + /// + public void RemoveRecipe(Guid id); + + /// + /// Lists all recipes in the database + /// + /// + public ImmutableList ListAllRecipes(); + + /// + /// List the ratings of an user + /// + /// + /// + public ImmutableDictionary ListRatesOf(Guid user); + + /// + /// Get the weekly list of given user + /// + /// + /// + public ImmutableDictionary GetRecipeListOf(Guid user); + } +} diff --git a/LocalServices/Data/RecipeData.cs b/LocalServices/Data/RecipeData.cs index 431f9b6..852c56d 100644 --- a/LocalServices/Data/RecipeData.cs +++ b/LocalServices/Data/RecipeData.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; using System.Runtime.Serialization; -namespace LocalEndpoint.Data +namespace LocalServices.Data { [DataContract] internal record RecipeData( diff --git a/LocalServices/Data/StubDatabase.cs b/LocalServices/Data/StubDatabase.cs new file mode 100644 index 0000000..156f3ba --- /dev/null +++ b/LocalServices/Data/StubDatabase.cs @@ -0,0 +1,72 @@ +using LocalServices.Data; +using Models; +using System.Collections.Immutable; + +namespace LocalServices.Data +{ + /// + /// stub implementation for IDatabase + /// + internal class StubDatabase : IDatabase + { + private readonly User User = new User(new Uri("https://images.pexels.com/photos/37546/woman-portrait-face-studio-37546.jpeg?cs=srgb&dl=beauty-face-headshot-37546.jpg&fm=jpg"), "David", Guid.NewGuid()); + public Account? GetAccount(string email, string passwordHash) + { + return new Account(User, "david@gmail.com"); + } + + public Recipe? GetRecipe(Guid id) + { + return new Recipe(new RecipeInfo("Foo", 4, 5, new Uri("https://th.bing.com/th/id/R.e57009c4044bef3e86f8a7b18a7cf36a?rik=K3dSu3KoTgeRSw&pid=ImgRaw&r=0"), 4.5F, id), User, new List { new Ingredient("Chocolate", 4, "g") }.ToImmutableList(), new List { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList()); + } + + public ImmutableDictionary GetRecipeListOf(Guid user) + { + return new Dictionary().ToImmutableDictionary(); + } + + public RecipeRate GetRecipeRate(Guid user, Guid recipe) + { + return new RecipeRate(true, 4); + } + + public void InsertAccount(Account account, string passwordHash) + { + + } + + public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) + { + } + + public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) + { + } + + public void InsertRecipe(Recipe recipe) + { + } + + public void InsertUser(User user) + { + } + + public ImmutableList ListAllRecipes() + { + return new List().ToImmutableList(); + } + + public ImmutableDictionary ListRatesOf(Guid user) + { + return new Dictionary().ToImmutableDictionary(); + } + + public void RemoveFromUserList(Guid userId, Guid recipeId) + { + } + + public void RemoveRecipe(Guid id) + { + } + } +} diff --git a/LocalServices/Data/UserData.cs b/LocalServices/Data/UserData.cs index 847d659..7b5a9e1 100644 --- a/LocalServices/Data/UserData.cs +++ b/LocalServices/Data/UserData.cs @@ -3,7 +3,7 @@ using Models; using System.Runtime.Serialization; -namespace LocalEndpoint.Data +namespace LocalServices.Data { [DataContract] internal record UserData( diff --git a/LocalServices/LocalEndpoint.cs b/LocalServices/LocalEndpoint.cs index c8f4533..3e524e2 100644 --- a/LocalServices/LocalEndpoint.cs +++ b/LocalServices/LocalEndpoint.cs @@ -1,14 +1,14 @@ -using Endpoint; -using LocalEndpoint.Data; +using Services; +using LocalServices.Data; using Models; using System.Collections.Immutable; -namespace LocalEndpoint +namespace LocalServices { /// - /// The local endpoint is an implementation of the Endpoint API definition. - /// + /// The local endpoint is an implementation of the Services API. + /// This class is the _entry point_ class of the implementation /// public class LocalEndpoint : IEndpoint { @@ -20,6 +20,7 @@ namespace LocalEndpoint { var db = new CatastrophicPerformancesDatabase(Environment.GetFolderPath(Environment.SpecialFolder.Personal)); + if (db.IsEmpty()) PrepareDatabase(db); @@ -31,7 +32,12 @@ namespace LocalEndpoint public IRecipesService RecipesService => recipesService; - private static void PrepareDatabase(Database db) + + /// + /// Inserts sample data in the local database + /// + /// + private static void PrepareDatabase(IDatabase db) { User USER1 = new User(new Uri("https://i.ibb.co/L6t6bGR/DALL-E-2023-05-10-20-27-31-cook-looking-at-the-camera-with-a-chef-s-hat-laughing-in-an-exaggerated-w.png"), "The Funny Chief", MakeGuid(1)); User USER2 = new User(Constants.DEFAULT_ACCOUNT_IMAGE, "Yanis", MakeGuid(2)); @@ -45,14 +51,19 @@ namespace LocalEndpoint db.InsertAccount(new Account(USER2, "yanis@google.com"), "123456"); db.InsertAccount(new Account(USER3, "leo@google.com"), "123456"); - db.InsertRecipe(new Recipe(new RecipeInfo("Chicken Salad", 500, 20, new Uri("https://healthyfitnessmeals.com/wp-content/uploads/2021/04/Southwest-chicken-salad-7-500x500.jpg"), 4, Guid.NewGuid()), USER1, new List { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); - db.InsertRecipe(new Recipe(new RecipeInfo("Chocolate Cake", 2500, 10, new Uri("https://bakewithshivesh.com/wp-content/uploads/2022/08/IMG_0248-scaled.jpg"), 3, Guid.NewGuid()), USER2, new List { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); - db.InsertRecipe(new Recipe(new RecipeInfo("Salmon", 20, 10, new Uri("https://www.wholesomeyum.com/wp-content/uploads/2021/06/wholesomeyum-Pan-Seared-Salmon-Recipe-13.jpg"), 4, Guid.NewGuid()), USER1, new List { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); - db.InsertRecipe(new Recipe(new RecipeInfo("Fish", 50, 30, new Uri("https://www.ciaanet.org/wp-content/uploads/2022/07/Atlantic-and-Pacific-whole-salmon-1024x683.jpg"), 4.5F, Guid.NewGuid()), USER3, new List { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); - db.InsertRecipe(new Recipe(new RecipeInfo("Space Cake", 800, 5, new Uri("https://static.youmiam.com/images/recipe/1500x1000/space-cake-22706?placeholder=web_recipe&sig=f14a7a86da837c6b8cc678cde424d6d5902f99ec&v3"), 5, Guid.NewGuid()), USER3, new List { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); - db.InsertRecipe(new Recipe(new RecipeInfo("Cupcake", 500, 12, new Uri("https://www.mycake.fr/wp-content/uploads/2015/12/rs_cupcake_4x3.jpg"), 4.2F, Guid.NewGuid()), USER1, new List { new Ingredient("Chocolate", 4) }.ToImmutableList(), new List { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Chicken Salad", 500, 20, new Uri("https://healthyfitnessmeals.com/wp-content/uploads/2021/04/Southwest-chicken-salad-7-500x500.jpg"), 4, Guid.NewGuid()), USER1, new List { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Chocolate Cake", 2500, 10, new Uri("https://bakewithshivesh.com/wp-content/uploads/2022/08/IMG_0248-scaled.jpg"), 3, Guid.NewGuid()), USER2, new List { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Salmon", 20, 10, new Uri("https://www.wholesomeyum.com/wp-content/uploads/2021/06/wholesomeyum-Pan-Seared-Salmon-Recipe-13.jpg"), 4, Guid.NewGuid()), USER1, new List { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Fish", 50, 30, new Uri("https://www.ciaanet.org/wp-content/uploads/2022/07/Atlantic-and-Pacific-whole-salmon-1024x683.jpg"), 4.5F, Guid.NewGuid()), USER3, new List { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Space Cake", 800, 5, new Uri("https://static.youmiam.com/images/recipe/1500x1000/space-cake-22706?placeholder=web_recipe&sig=f14a7a86da837c6b8cc678cde424d6d5902f99ec&v3"), 5, Guid.NewGuid()), USER3, new List { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList())); + db.InsertRecipe(new Recipe(new RecipeInfo("Cupcake", 500, 12, new Uri("https://www.mycake.fr/wp-content/uploads/2015/12/rs_cupcake_4x3.jpg"), 4.2F, Guid.NewGuid()), USER1, new List { new Ingredient("Chocolate", 4, "g") }.ToImmutableList(), new List { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList())); } + /// + /// helper function to Create a Guid from a given seed + /// + /// the seed to use for the generation + /// private static Guid MakeGuid(int seed) { var r = new Random(seed); diff --git a/LocalServices/RecipesService.cs b/LocalServices/RecipesService.cs index 8969e9e..89404a3 100644 --- a/LocalServices/RecipesService.cs +++ b/LocalServices/RecipesService.cs @@ -1,17 +1,17 @@ -using Endpoint; -using LocalEndpoint.Data; +using Services; +using LocalServices.Data; using Models; using System.Collections.Immutable; -namespace LocalEndpoint +namespace LocalServices { - internal class RecipesService : IRecipesService + public class RecipesService : IRecipesService { - private readonly Database db; + private readonly IDatabase db; private readonly Dictionary accountsData = new Dictionary(); - public RecipesService(Database db) + public RecipesService(IDatabase db) { this.db = db; } @@ -21,17 +21,17 @@ namespace LocalEndpoint return db.ListAllRecipes().Take(4).ToImmutableList().ConvertAll(v => v.Info); } - public Recipe GetRecipe(RecipeInfo info) + public Recipe? GetRecipe(RecipeInfo info) { return db.GetRecipe(info.Id); } - public IAccountOwnedRecipes GetRecipesOf(Account account) + public IAccountOwnedRecipesService GetRecipesOf(Account account) { return GetOrInitData(account).Recipes; } - public IAccountRecipesPreferences GetPreferencesOf(Account account) + public IAccountRecipesPreferencesService GetPreferencesOf(Account account) { return GetOrInitData(account).Preferences; } @@ -51,5 +51,11 @@ namespace LocalEndpoint return data; } + public ImmutableList SearchRecipes(string prompt) + { + return db.ListAllRecipes() + .ConvertAll(r => r.Info) + .FindAll(i => i.Name.ToLower().Contains(prompt.ToLower())); + } } } diff --git a/MainAppShell.xaml.cs b/MainAppShell.xaml.cs index d3f6bc1..1b7399c 100644 --- a/MainAppShell.xaml.cs +++ b/MainAppShell.xaml.cs @@ -3,15 +3,20 @@ using Microsoft.Maui.Controls; using Models; using ShoopNCook.Controllers; using ShoopNCook.Pages; -using Endpoint; +using Services; + +// Shell principale de l'application après connexion de l'utilisateur public partial class MainAppShell : Shell { - public MainAppShell(Account account, IEndpoint endpoint, IApp app) + // Constructeur qui prend en argument un compte, un endpoint et une application + public MainAppShell(Account account, IEndpoint endpoint, IApp app) { InitializeComponent(); - HomeTab.ContentTemplate = new DataTemplate(() => new HomePage(account, app.Notifier, endpoint)); - FavoritesTab.ContentTemplate = new DataTemplate(() => new FavoritesPage(account, app.Notifier, endpoint.RecipesService)); - MyListTab.ContentTemplate = new DataTemplate(() => new MyListPage(account, app.Notifier, endpoint.RecipesService)); + + // Initialisation de chaque onglet avec sa page respective + HomeTab.ContentTemplate = new DataTemplate(() => new HomePage(account, endpoint)); + FavoritesTab.ContentTemplate = new DataTemplate(() => new FavoritesPage(account, endpoint.RecipesService)); + MyListTab.ContentTemplate = new DataTemplate(() => new MyListPage(account, endpoint.RecipesService)); MoreTab.ContentTemplate = new DataTemplate(() => new MorePage(account, new MorePageController(account, endpoint, app))); } } diff --git a/MauiProgram.cs b/MauiProgram.cs index 7dc9f23..420e008 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using CommunityToolkit.Maui; +using Microsoft.Extensions.Logging; namespace ShoopNCook; @@ -9,6 +10,7 @@ public static class MauiProgram var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() + .UseMauiCommunityToolkit() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); diff --git a/Models/Account.cs b/Models/Account.cs index 5a9973b..142141e 100644 --- a/Models/Account.cs +++ b/Models/Account.cs @@ -1,4 +1,9 @@ namespace Models { + /// + /// Contains the informations of an account. + /// + /// The account's public information + /// The account's email address public record Account(User User, string Email); } diff --git a/Models/Ingredient.cs b/Models/Ingredient.cs index 10e7655..f0e14ee 100644 --- a/Models/Ingredient.cs +++ b/Models/Ingredient.cs @@ -3,7 +3,11 @@ namespace Models { - + /// + /// An ingredient + /// + /// The ingredient's name + /// The ingredient's amount (in kilograms or liters) [DataContract] - public record Ingredient([property: DataMember] string Name, [property: DataMember] float Amount); + public record Ingredient([property: DataMember] string Name, [property: DataMember] float Amount, [property: DataMember] string Unit); } diff --git a/Models/PreparationStep.cs b/Models/PreparationStep.cs index 35e4734..d14aea2 100644 --- a/Models/PreparationStep.cs +++ b/Models/PreparationStep.cs @@ -2,6 +2,11 @@ namespace Models { + /// + /// A step of preparation + /// + /// The step's name + /// The step's instructions / description [DataContract] public record PreparationStep([property: DataMember] string Name, [property: DataMember] string Description); } diff --git a/Models/Recipe.cs b/Models/Recipe.cs index 021d99a..71f44e3 100644 --- a/Models/Recipe.cs +++ b/Models/Recipe.cs @@ -3,6 +3,13 @@ using System.Runtime.Serialization; namespace Models { + /// + /// A Recipe + /// + /// The essential information of the recipe + /// The creator of the recipe + /// The needed ingredients of the recipe + /// The preparation steps [DataContract] public record Recipe( [property: DataMember] RecipeInfo Info, @@ -10,5 +17,4 @@ namespace Models [property: DataMember] ImmutableList Ingredients, [property: DataMember] ImmutableList Steps ); - } \ No newline at end of file diff --git a/Models/RecipeBuilder.cs b/Models/RecipeBuilder.cs index 047a4c6..ce49872 100644 --- a/Models/RecipeBuilder.cs +++ b/Models/RecipeBuilder.cs @@ -7,6 +7,9 @@ using System.Threading.Tasks; namespace Models { + /// + /// A Simple builder to create a recipe + /// public class RecipeBuilder { private readonly string name; diff --git a/Models/RecipeInfo.cs b/Models/RecipeInfo.cs index fec0ebb..d29f89d 100644 --- a/Models/RecipeInfo.cs +++ b/Models/RecipeInfo.cs @@ -2,12 +2,21 @@ namespace Models { + /// + /// The essential information about a recipe + /// + /// The recipe's name + /// The energy input + /// Estimated time of preparation in minutes + /// An illustrative image of the recipe + /// The average rate of the recipe + /// An unique identifier [DataContract] public record RecipeInfo( [property: DataMember] string Name, [property: DataMember] uint CalPerPers, [property: DataMember] uint CookTimeMins, - [property: DataMember] Uri? Image, + [property: DataMember] Uri Image, [property: DataMember] float AverageNote, [property: DataMember] Guid Id ); diff --git a/Models/RecipeRate.cs b/Models/RecipeRate.cs index b4dc3a3..a8e82c2 100644 --- a/Models/RecipeRate.cs +++ b/Models/RecipeRate.cs @@ -3,6 +3,11 @@ using System.Runtime.Serialization; namespace Models { + /// + /// The rate of a recipe, usually, the instances are bound with an account. + /// + /// + /// a rate between 0 and 5 [DataContract] public record RecipeRate( [property: DataMember] bool IsFavorite = false, diff --git a/Models/User.cs b/Models/User.cs index b70f7b1..7e18db1 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -2,13 +2,25 @@ namespace Models { + /// + /// Publics informations of a user + /// [DataContract] public class User { + /// + /// The profile picture + /// [DataMember] public Uri ProfilePicture { get; init; } + /// + /// The username + /// [DataMember] public string Name { get; init; } + /// + /// An unique identifier + /// [DataMember] public Guid Id { get; init; } diff --git a/Resources/Images/error.png b/Resources/Images/error.png new file mode 100644 index 0000000..57c47ae Binary files /dev/null and b/Resources/Images/error.png differ diff --git a/Resources/Images/notice.png b/Resources/Images/notice.png new file mode 100644 index 0000000..85c3bfe Binary files /dev/null and b/Resources/Images/notice.png differ diff --git a/Resources/Images/success.png b/Resources/Images/success.png new file mode 100644 index 0000000..1bd8d58 Binary files /dev/null and b/Resources/Images/success.png differ diff --git a/Resources/Images/warning.png b/Resources/Images/warning.png new file mode 100644 index 0000000..fe35146 Binary files /dev/null and b/Resources/Images/warning.png differ diff --git a/Services/IAccountOwnedRecipes.cs b/Services/IAccountOwnedRecipes.cs deleted file mode 100644 index 69d6b7b..0000000 --- a/Services/IAccountOwnedRecipes.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Models; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LocalEndpoint -{ - public interface IAccountOwnedRecipes - { - - public Account Account { get; } - - public bool UploadRecipe(Recipe recipe); - - public bool RemoveRecipe(RecipeInfo info); - - - public ImmutableList GetAccountRecipes(); - } -} diff --git a/Services/IAccountOwnedRecipesService.cs b/Services/IAccountOwnedRecipesService.cs new file mode 100644 index 0000000..c6b375e --- /dev/null +++ b/Services/IAccountOwnedRecipesService.cs @@ -0,0 +1,38 @@ +using Models; +using System.Collections.Immutable; + +namespace Services +{ + /// + /// This service handles the recipes created by an account + /// + public interface IAccountOwnedRecipesService + { + /// + /// This service's bound account + /// + public Account Account { get; } + + /// + /// Upload a new recipe, ensuring the recipe's owner matches the service's bound account user. + /// + /// The recipe to upload + /// true if the recipe could be uploaded, false instead + public bool UploadRecipe(Recipe recipe); + + /// + /// Removes a recipe + /// + /// The informations about the recipe to remove + /// true if the recipe could be removed, false instead + public bool RemoveRecipe(RecipeInfo info); + + /// + /// The living recipes created by this account. + /// If the user removes a recipe (using ) it'll no longer apear in the + /// next invocations of this recipe + /// + /// the list of all the living recipes of the account + public ImmutableList GetAccountRecipes(); + } +} diff --git a/Services/IAccountRecipesPreferences.cs b/Services/IAccountRecipesPreferences.cs deleted file mode 100644 index 7f138db..0000000 --- a/Services/IAccountRecipesPreferences.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Models; -using System.Collections.Immutable; - -namespace Endpoint -{ - public interface IAccountRecipesPreferences - { - public Account Account { get; } - - - public void AddToFavorites(RecipeInfo info); - public void RemoveFromFavorites(RecipeInfo info); - public void SetReviewScore(RecipeInfo info, uint score); - public bool AddToWeeklyList(RecipeInfo info, uint persAmount); - - public bool RemoveFromWeeklyList(RecipeInfo info); - - public bool IsInWeeklyList(RecipeInfo info); - - public RecipeRate GetRate(RecipeInfo info); - - public ImmutableList GetFavorites(); - - public ImmutableList GetRecommendedRecipes(); - - public ImmutableList<(RecipeInfo, uint)> GetWeeklyList(); - - } -} diff --git a/Services/IAccountRecipesPreferencesService.cs b/Services/IAccountRecipesPreferencesService.cs new file mode 100644 index 0000000..57d14df --- /dev/null +++ b/Services/IAccountRecipesPreferencesService.cs @@ -0,0 +1,68 @@ +using Models; +using System.Collections.Immutable; + +namespace Services +{ + /// + /// This service handles the preferences of a bound account + /// + public interface IAccountRecipesPreferencesService + { + /// + /// The bound account + /// + public Account Account { get; } + + /// + /// Adds a recipe in the favorites of the bound account + /// + /// The information about the recipe to add in favorites + public void AddToFavorites(RecipeInfo info); + /// + /// Removes a recipe from the favorites of the bound account + /// + /// The information about the recipe to remove from favorites + public void RemoveFromFavorites(RecipeInfo info); + /// + /// Sets a score for the specified recipe + /// + /// The information about the targeted recipe + /// The score to set + public void SetReviewScore(RecipeInfo info, uint score); + /// + /// Adds a recipe to the weekly list, specifying the amount of persons that must be fed for the week + /// + /// The information about the targeted recipe + /// The amount of guests that needs to be fed by the recipe for the week + /// + public bool AddToWeeklyList(RecipeInfo info, uint persAmount); + + /// + /// Retrieves the rate of the targeted recipe + /// The rate contains the user's score and whether if the recipe is in the favorites list. + /// + /// + /// The information about the targeted recipe + /// + public RecipeRate GetRate(RecipeInfo info); + + /// + /// The favorites recipes of the account + /// + /// A list containing all the recipe info that are marked as favorite by the bound account + public ImmutableList GetFavorites(); + + /// + /// The recommended recipes for the user based on his preferences. + /// + /// A list of the recommended recipes based on the preferences of the bound account + public ImmutableList GetRecommendedRecipes(); + + /// + /// The weekly list of the bound account + /// + /// The weekly list of the bound account, containing tuples that binds a recipe to the number of guests to feed for the week + public ImmutableList<(RecipeInfo, uint)> GetWeeklyList(); + + } +} diff --git a/Services/IAuthService.cs b/Services/IAuthService.cs index 0e450e2..ad12946 100644 --- a/Services/IAuthService.cs +++ b/Services/IAuthService.cs @@ -1,11 +1,34 @@ -using Models; -namespace Endpoint -{ - public interface IAuthService - { - public Account? Login(string email, string password); - - public Account? Register(string email, string username, string password); - } -} - +using Models; + +namespace Services +{ + /// + /// Service for account authentification + /// Passwords are being passed in this service clearly as the hash function used for passwords is implementation specific + /// + public interface IAuthService + { + /// + /// Tries to login to an account using its mail address and password. + /// + /// The mail address which acts as an identifier for the targeted account + /// The (clear) password used to login. + /// + /// Returns an instance of Account representing the account that got logged in. + /// If the login credentials are invalid to log in the targeted acccount, this method returns null. + /// + public Account? Login(string email, string password); + /// + /// Tries to register to a new account, defining its mail address, username and password. + /// + /// The mail address which acts as an identifier for the targeted account + /// The username of the account + /// The (clear) password used to login on next connections attempt. + /// + /// Returns an instance of Account representing the account that got newly registered. + /// If the register credentials are invalid, or if the account already exists, this method returns null. + /// + public Account? Register(string email, string username, string password); + } +} + diff --git a/Services/IEndpoint.cs b/Services/IEndpoint.cs index b928557..a8aba6f 100644 --- a/Services/IEndpoint.cs +++ b/Services/IEndpoint.cs @@ -1,13 +1,16 @@ - - -namespace Endpoint -{ - public interface IEndpoint - { - public IAuthService AuthService { get; } - - public IRecipesService RecipesService { get; } - - } -} - + + +namespace Services +{ + /// + /// The endpoint is the central element of the 'Services' assembly. + /// + public interface IEndpoint + { + public IAuthService AuthService { get; } + + public IRecipesService RecipesService { get; } + + } +} + diff --git a/Services/IRecipesService.cs b/Services/IRecipesService.cs index 88e8b65..f2fc18c 100644 --- a/Services/IRecipesService.cs +++ b/Services/IRecipesService.cs @@ -1,18 +1,48 @@ -using LocalEndpoint; -using Models; -using System.Collections.Immutable; - -namespace Endpoint -{ - public interface IRecipesService - { - public ImmutableList PopularRecipes(); - - public Recipe GetRecipe(RecipeInfo info); - - public IAccountOwnedRecipes GetRecipesOf(Account account); - public IAccountRecipesPreferences GetPreferencesOf(Account account); - - - } -} +using Models; +using System.Collections.Immutable; + +namespace Services +{ + /// + /// The services that is in charge of handling the application's recipes. + /// + public interface IRecipesService + { + /// + /// + /// + /// A list containg the popular recipes of the week + public ImmutableList PopularRecipes(); + + /// + /// performs a search over all the recipes + /// + /// A list containg the recipes that matches the prompt + public ImmutableList SearchRecipes(string prompt); + + /// + /// Retrieves a recipe from given RecipeInfo + /// + /// the informations about the recipe that we want to retrieve + /// some recipe if the recipe was found, null else + public Recipe? GetRecipe(RecipeInfo info); + + /// + /// Gets the service that is in charge of handling the account's owned recipes. + /// The account's owned recipes are the recipes that the account created. + /// + /// The account logged in + /// The service that handles the given account's recipes + public IAccountOwnedRecipesService GetRecipesOf(Account account); + /// + /// Gets the service that handles all the preferences of the given account + /// + /// The account logged in + /// + /// The service that handles the given account's preferences + /// + public IAccountRecipesPreferencesService GetPreferencesOf(Account account); + + + } +} diff --git a/ShoopNCook.csproj b/ShoopNCook.csproj index 8a80861..1310c37 100644 --- a/ShoopNCook.csproj +++ b/ShoopNCook.csproj @@ -6,16 +6,17 @@ Exe + ShoopNCook true true enable - ShoopNCook + ShopNCook - com.companyname.shoopncook + com.companyname.shopncook bf17e1fe-a722-42f6-a24d-3327d351c924 @@ -48,26 +49,31 @@ + + + + + @@ -113,6 +119,7 @@ + diff --git a/ShoopNCook.sln b/ShoopNCook.sln index 3b6c720..17d11be 100644 --- a/ShoopNCook.sln +++ b/ShoopNCook.sln @@ -10,7 +10,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalServices", "LocalServices\LocalServices.csproj", "{57732316-93B9-4DA0-A212-F8892D3D968B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{C976BDD8-710D-4162-8A42-973B634491F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "Services\Services.csproj", "{C976BDD8-710D-4162-8A42-973B634491F9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6DEA92EF-71CD-4A21-9CC0-67F228E1155D}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Tests/AccountOwnedRecipesTest.cs b/Tests/AccountOwnedRecipesTest.cs new file mode 100644 index 0000000..00e6a28 --- /dev/null +++ b/Tests/AccountOwnedRecipesTest.cs @@ -0,0 +1,26 @@ +using LocalServices.Data; +using Models; +using Services; +using System.Collections.Immutable; + +namespace Tests +{ + public class AccountOwnedRecipesTest + { + private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid()); + private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail"); + private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build(); + + [Fact] + public void UploadRemove() + { + var db = new Mock(); + db.Setup(x => x.ListAllRecipes()).Returns(() => new List().ToImmutableList()); + var owned = new AccountOwnedRecipes(SAMPLE_ACC, db.Object); + owned.UploadRecipe(SAMPLE_RECIPE); + Assert.Contains(SAMPLE_RECIPE.Info, owned.GetAccountRecipes()); + owned.RemoveRecipe(SAMPLE_RECIPE.Info); + Assert.DoesNotContain(SAMPLE_RECIPE.Info, owned.GetAccountRecipes()); + } + } +} diff --git a/Tests/AccountRecipesPreferencesTests.cs b/Tests/AccountRecipesPreferencesTests.cs new file mode 100644 index 0000000..cb3f499 --- /dev/null +++ b/Tests/AccountRecipesPreferencesTests.cs @@ -0,0 +1,46 @@ +using LocalServices; +using LocalServices.Data; +using Models; +using System.Collections.Immutable; + +namespace Tests +{ + public class AccountRecipesPreferencesTests + { + private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid()); + private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail"); + private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build(); + + [Fact] + public void Review() + { + var fav_inserted = false; + var rate_inserted = false; + var dict = new Dictionary(); + var db = new Mock(); + db.Setup(x => x.ListRatesOf(SAMPLE_USER.Id)).Returns(() => dict.ToImmutableDictionary()); + db.Setup(x => x.InsertRate(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, new RecipeRate(true, 0))).Callback(() => fav_inserted = true); + db.Setup(x => x.InsertRate(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, new RecipeRate(false, 3))).Callback(() => rate_inserted = true); + var pref = new AccountRecipesPreferences(SAMPLE_ACC, db.Object); + pref.AddToFavorites(SAMPLE_RECIPE.Info); + pref.SetReviewScore(SAMPLE_RECIPE.Info, 3); + Assert.True(fav_inserted); + Assert.True(rate_inserted); + } + + public void AddWeeklyList() + { + var inserted = false; + + var dict = new Dictionary { }; + dict.Add(SAMPLE_RECIPE.Info.Id, 88); + var db = new Mock(); + db.Setup(x => x.GetRecipeListOf(SAMPLE_USER.Id)).Returns(() => dict); + db.Setup(x => x.InsertInUserList(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, 88)).Callback(() => inserted = true); + var pref = new AccountRecipesPreferences(SAMPLE_ACC, db.Object); + pref.AddToWeeklyList(SAMPLE_RECIPE.Info, 88); + Assert.True(inserted); + Assert.True(pref.GetWeeklyList().Contains((SAMPLE_RECIPE.Info, 88))); + } + } +} diff --git a/Tests/AuthServiceTests.cs b/Tests/AuthServiceTests.cs new file mode 100644 index 0000000..0747e62 --- /dev/null +++ b/Tests/AuthServiceTests.cs @@ -0,0 +1,35 @@ + +using Models; +using LocalServices.Data; +using Moq; +using LocalServices; +using Services; + +namespace Tests +{ + public class AuthServiceTests + { + private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid()); + private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail"); + + [Fact] + public void TestLogin() + { + var database = new Mock(); + database.Setup(x => x.GetAccount("mail", "1234")).Returns(SAMPLE_ACC); + var service = new AuthService(database.Object); + var acc = service.Login("mail", "1234"); + Assert.Equal(acc, SAMPLE_ACC); + } + + [Fact] + public void TestRegister() + { + var database = new Mock(); + database.Setup(x => x.GetAccount("mail", "1234")).Returns(SAMPLE_ACC); + var service = new AuthService(database.Object); + var acc = service.Register("mail", "foo", "1234"); + Assert.Equal(acc, new Account(new User(Constants.DEFAULT_ACCOUNT_IMAGE, "foo", acc.User.Id), "mail")); + } + } +} diff --git a/Tests/RecipeServicesTests.cs b/Tests/RecipeServicesTests.cs new file mode 100644 index 0000000..b598009 --- /dev/null +++ b/Tests/RecipeServicesTests.cs @@ -0,0 +1,25 @@ +using LocalServices.Data; +using Models; +using LocalServices; +using Services; +using System.Collections.Immutable; + +namespace Tests +{ + public class RecipeServicesTests + { + private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid()); + private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail"); + private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build(); + + [Fact] + public void GetRecipe() + { + var database = new Mock(); + database.Setup(x => x.GetRecipe(SAMPLE_RECIPE.Info.Id)).Returns(SAMPLE_RECIPE); + var service = new RecipesService(database.Object); + Assert.Equal(service.GetRecipe(SAMPLE_RECIPE.Info), SAMPLE_RECIPE); + } + + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 42ff44d..a268461 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,25 +1,32 @@ - - - - net7.0 - enable - enable - - false - true - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/Tests/UnitTest1.cs b/Tests/UnitTest1.cs deleted file mode 100644 index f5782ed..0000000 --- a/Tests/UnitTest1.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } - -} - diff --git a/Tests/Usings.cs b/Tests/Usings.cs index 8c927eb..7a8920d 100644 --- a/Tests/Usings.cs +++ b/Tests/Usings.cs @@ -1 +1,2 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using Moq; \ No newline at end of file diff --git a/UserNotifier.cs b/UserNotifier.cs new file mode 100644 index 0000000..8432fc8 --- /dev/null +++ b/UserNotifier.cs @@ -0,0 +1,42 @@ +using CommunityToolkit.Maui.Alerts; +using CommunityToolkit.Maui.Core; + + +namespace ShoopNCook +{ + internal class UserNotifier + { + private static async Task Show(string message, string messageType) + { + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + // Vous pouvez configurer la durée et la taille de police ici. + ToastDuration duration = ToastDuration.Short; + double fontSize = 14; + + var toast = Toast.Make(message, duration, fontSize); + + await toast.Show(cancellationTokenSource.Token); + } + + + + public static void Error(string message) + { + Show(message, "Error"); + } + public static void Warn(string message) + { + Show(message, "Warning"); + } + public static void Notice(string message) + { + Show(message, "Notice"); + } + public static void Success(string message) + { + Show(message, "Success"); + } + } + +} diff --git a/Views/Components/CounterView.xaml.cs b/Views/Components/CounterView.xaml.cs index 9882cb2..9262ccb 100644 --- a/Views/Components/CounterView.xaml.cs +++ b/Views/Components/CounterView.xaml.cs @@ -41,3 +41,4 @@ public partial class CounterView : ContentView Count -= 1; } } + diff --git a/Views/Components/HeadedButton.xaml.cs b/Views/Components/HeadedButton.xaml.cs index 1c00ef2..1c22ef6 100644 --- a/Views/Components/HeadedButton.xaml.cs +++ b/Views/Components/HeadedButton.xaml.cs @@ -1,26 +1,29 @@ namespace ShoopNCook.Views; using Microsoft.Maui.Graphics; +// Classe représentant un bouton avec une tête (une image en préfixe) public partial class HeadedButton : ContentView { - + // Texte du bouton public string Text { set => BtnLabel.Text = value; } + // Couleur de l'image en préfixe public string HeadColor { set => PrefixBorder.BackgroundColor = Color.FromArgb(value); } + // Source de l'image en préfixe public string HeadSource { set => PrefixImage.Source = ImageSource.FromFile(value); } public HeadedButton() - { - InitializeComponent(); - } -} \ No newline at end of file + { + InitializeComponent(); + } +} diff --git a/Views/Components/IngredientEntry.xaml b/Views/Components/IngredientEntry.xaml index 0a39bdc..769bd12 100644 --- a/Views/Components/IngredientEntry.xaml +++ b/Views/Components/IngredientEntry.xaml @@ -22,7 +22,7 @@ Style="{StaticResource UserInput}" Placeholder="Ingredient Name" HeightRequest="40" - x:Name="NameEntry"/> + Text="{Binding NameText, Mode=OneWayToSource}"/> + Text="{Binding QuantityText, Mode=OneWayToSource}"/> + RowDefinitions="*, Auto, Auto"> @@ -40,7 +40,8 @@ + Placeholder="Search here..." + x:Name="SearchPrompt"/> - { - Recipe recipe = service.GetRecipe(info); - Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, 1)); + { + Recipe recipe = recipes.GetRecipe(info); + if (recipe != null) + Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, 1)); + else + { + UserNotifier.Error("Could not find recipe"); + } })); } - service.PopularRecipes().ForEach(recipe => PushRecipe(PopularsList, recipe)); + recipes.PopularRecipes().ForEach(recipe => PushRecipe(PopularsList, recipe)); preferences.GetRecommendedRecipes().ForEach(recipe => PushRecipe(RecommendedList, recipe)); - - ProfilePictureImage.Source = ImageSource.FromUri(account.User.ProfilePicture); } - - - - private void OnSyncButtonClicked(object sender, EventArgs e) + private async void OnSyncButtonClicked(object sender, EventArgs e) { - Shell.Current.Navigation.PushAsync(new SearchPage()); + string prompt = SearchPrompt.Text; + if (string.IsNullOrEmpty(prompt)) + return; + + var searchPage = new SearchPage(recipes, preferences); + await Shell.Current.Navigation.PushAsync(searchPage); + searchPage.MakeSearch(SearchPrompt.Text); } } \ No newline at end of file diff --git a/Views/LoginPage.xaml.cs b/Views/LoginPage.xaml.cs index 6d2293a..31f87f7 100644 --- a/Views/LoginPage.xaml.cs +++ b/Views/LoginPage.xaml.cs @@ -1,4 +1,4 @@ -using Endpoint; +using Services; using ShoopNCook.Controllers; namespace ShoopNCook.Pages; @@ -11,7 +11,7 @@ public partial class LoginPage : ContentPage InitializeComponent(); this.controller = controller; } - private async void OnLoginButtonClicked(object sender, EventArgs e) + private void OnLoginButtonClicked(object sender, EventArgs e) { string email = EmailEntry.Text; string password = PasswordEntry.Text; diff --git a/Views/MyListPage.xaml.cs b/Views/MyListPage.xaml.cs index 87cddb5..7e51831 100644 --- a/Views/MyListPage.xaml.cs +++ b/Views/MyListPage.xaml.cs @@ -1,5 +1,5 @@ -using Endpoint; -using LocalEndpoint; +using Services; +using Services; using Models; using ShoopNCook.Views; @@ -8,16 +8,14 @@ namespace ShoopNCook.Pages; public partial class MyListPage : ContentPage { - private readonly IAccountRecipesPreferences preferences; - private readonly IUserNotifier notifier; + private readonly IAccountRecipesPreferencesService preferences; private readonly IRecipesService service; - public MyListPage(Account account, IUserNotifier notifier, IRecipesService service) + public MyListPage(Account account, IRecipesService service) { InitializeComponent(); this.preferences = service.GetPreferencesOf(account); - this.notifier = notifier; this.service = service; UpdateMyList(); @@ -32,12 +30,12 @@ public partial class MyListPage : ContentPage RecipesLayout.Children.Add(new StoredRecipeView(info, tuple.Item2, amount => { Recipe recipe = service.GetRecipe(info); - Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, amount)); + Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, amount)); })); }); } - private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) + private async void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { UpdateMyList(); } diff --git a/Views/MyRecipesPage.xaml.cs b/Views/MyRecipesPage.xaml.cs index 952d786..b4210ba 100644 --- a/Views/MyRecipesPage.xaml.cs +++ b/Views/MyRecipesPage.xaml.cs @@ -1,5 +1,5 @@ -using Endpoint; -using LocalEndpoint; +using Services; +using Services; using Models; using ShoopNCook.Views; @@ -8,18 +8,15 @@ namespace ShoopNCook.Pages; public partial class MyRecipesPage : ContentPage { - private IUserNotifier notifier; private IRecipesService service; private Account account; public MyRecipesPage( Account account, - IRecipesService service, - IUserNotifier notifier) + IRecipesService service) { InitializeComponent(); - this.notifier = notifier; this.service = service; this.account = account; @@ -34,8 +31,8 @@ public partial class MyRecipesPage : ContentPage RecipesLayout.Children.Add(new OwnedRecipeView(info, () => { Recipe recipe = service.GetRecipe(info); - IAccountRecipesPreferences preferences = service.GetPreferencesOf(account); - Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, 1)); + IAccountRecipesPreferencesService preferences = service.GetPreferencesOf(account); + Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, 1)); }, () => RemoveRecipe(info) )); @@ -43,11 +40,11 @@ public partial class MyRecipesPage : ContentPage private void RemoveRecipe(RecipeInfo info) { - IAccountOwnedRecipes recipes = service.GetRecipesOf(account); + IAccountOwnedRecipesService recipes = service.GetRecipesOf(account); if (!recipes.RemoveRecipe(info)) { - notifier.Error("Could not remove recipe"); + UserNotifier.Error("Could not remove recipe"); return; } foreach (OwnedRecipeView view in RecipesLayout.Children) @@ -58,25 +55,25 @@ public partial class MyRecipesPage : ContentPage break; } } - notifier.Success("Recipe successfully removed"); + UserNotifier.Success("Recipe successfully removed"); } - private void OnBackButtonClicked(object sender, EventArgs e) + private async void OnBackButtonClicked(object sender, EventArgs e) { - Navigation.PopAsync(); + await Navigation.PopAsync(); } - private void OnAddRecipeButtonClicked(object sender, EventArgs e) + private async void OnAddRecipeButtonClicked(object sender, EventArgs e) { - IAccountOwnedRecipes recipes = service.GetRecipesOf(account); + IAccountOwnedRecipesService recipes = service.GetRecipesOf(account); - var page = new CreateRecipePage(account.User, notifier, recipe => + var page = new CreateRecipePage(account.User, recipe => { if (!recipes.UploadRecipe(recipe)) { - notifier.Error("Could not upload recipe."); + UserNotifier.Error("Could not upload recipe."); return; } - notifier.Success("Recipe Successfuly uploaded !"); + UserNotifier.Success("Recipe Successfuly uploaded !"); AddRecipeView(recipe.Info); Shell.Current.Navigation.PopAsync(); //go back to current recipe page. }); diff --git a/Views/RecipePage.xaml b/Views/RecipePage.xaml index 38449fb..cc77351 100644 --- a/Views/RecipePage.xaml +++ b/Views/RecipePage.xaml @@ -154,6 +154,7 @@ Grid.Column="2" Style="{StaticResource UserButton}" BackgroundColor="Gray" + Text="Add To Weekly List" x:Name="MyListStateButton" Clicked="OnFooterButtonClicked"/> diff --git a/Views/RecipePage.xaml.cs b/Views/RecipePage.xaml.cs index de43776..2dbcfea 100644 --- a/Views/RecipePage.xaml.cs +++ b/Views/RecipePage.xaml.cs @@ -1,26 +1,23 @@ using ShoopNCook.Views; using System.Windows.Input; using Models; -using LocalEndpoint; -using Endpoint; +using Services; namespace ShoopNCook.Pages; public partial class RecipePage : ContentPage { - private readonly IAccountRecipesPreferences preferences; - private readonly IUserNotifier notifier; + private readonly IAccountRecipesPreferencesService preferences; private uint note; private bool isFavorite; - private bool isInMyList; public Recipe Recipe { get; init; } - + public ICommand StarCommand => new Command(count => SetNote(uint.Parse(count))); - public RecipePage(Recipe recipe, IUserNotifier notifier, IAccountRecipesPreferences preferences, uint amount) + public RecipePage(Recipe recipe, IAccountRecipesPreferencesService preferences, uint amount) { Recipe = recipe; @@ -29,7 +26,6 @@ public partial class RecipePage : ContentPage InitializeComponent(); this.preferences = preferences; - this.notifier = notifier; RecipeRate rate = preferences.GetRate(recipe.Info); @@ -38,7 +34,6 @@ public partial class RecipePage : ContentPage SetFavorite(isFavorite); SetNote(note); - SetIsInMyListState(preferences.IsInWeeklyList(recipe.Info)); Counter.Count = amount; @@ -49,7 +44,8 @@ public partial class RecipePage : ContentPage var styles = Application.Current.Resources.MergedDictionaries.ElementAt(1); int count = 0; - foreach (PreparationStep step in recipe.Steps) { + foreach (PreparationStep step in recipe.Steps) + { //TODO display name of PreparationSteps. Label label = new Label(); label.Style = (Style)styles["Small"]; @@ -58,19 +54,6 @@ public partial class RecipePage : ContentPage } } - private void SetIsInMyListState(bool isInMyList) - { - this.isInMyList = isInMyList; - if (isInMyList) - { - MyListStateButton.Text = "Remove From My List"; - } - else - { - MyListStateButton.Text = "Add To My List"; - } - } - private void SetNote(uint note) { this.note = note; @@ -99,40 +82,20 @@ public partial class RecipePage : ContentPage private void OnSubmitReviewClicked(object o, EventArgs e) { preferences.SetReviewScore(Recipe.Info, note); - notifier.Success("Your review has been successfuly submited"); + UserNotifier.Success("Your review has been successfuly submited"); } private void OnFooterButtonClicked(object o, EventArgs e) { - SetIsInMyListState(!isInMyList); - if (isInMyList) - RemoveFromMyList(); - else - AddToMyList(); - } - - private void RemoveFromMyList() - { - if (!preferences.RemoveFromWeeklyList(Recipe.Info)) - { - notifier.Notice("This recipe does not figures in your personnal list"); - } - else - { - notifier.Success("Recipe added to your weekly list."); - } + AddToMyList(); } private void AddToMyList() { if (!preferences.AddToWeeklyList(Recipe.Info, Counter.Count)) - { - notifier.Notice("You already added this recipe to you weekly list!"); - } + UserNotifier.Notice("You already added this recipe to you weekly list!"); else - { - notifier.Success("Recipe added to your weekly list."); - } + UserNotifier.Success("Recipe added to your weekly list."); } private void SetFavorite(bool isFavorite) @@ -147,5 +110,4 @@ public partial class RecipePage : ContentPage { Navigation.PopAsync(); } - } \ No newline at end of file diff --git a/Views/RegisterPage.xaml.cs b/Views/RegisterPage.xaml.cs index b8d1929..65c3b2c 100644 --- a/Views/RegisterPage.xaml.cs +++ b/Views/RegisterPage.xaml.cs @@ -14,7 +14,7 @@ public partial class RegisterPage : ContentPage { await Shell.Current.GoToAsync("//Login"); } - private void RegisterTapped(object sender, EventArgs e) + private async void RegisterTapped(object sender, EventArgs e) { string email = EmailEntry.Text; string password = PasswordEntry.Text; diff --git a/Views/SearchPage.xaml b/Views/SearchPage.xaml index 6d2dce6..da8978b 100644 --- a/Views/SearchPage.xaml +++ b/Views/SearchPage.xaml @@ -49,18 +49,28 @@ Style="{StaticResource SecondaryBorderShadow}"> + Placeholder="Cake, Lasagna, Vegetarian..." + x:Name="SearchPrompt"/> - - + WidthRequest="40"> + + + + @@ -69,34 +79,10 @@