From 7ffd0bab3c6f397936f8ef9f6829cdf33b850efa Mon Sep 17 00:00:00 2001 From: Roy Ben-Shabat Date: Sun, 19 Jan 2020 16:10:55 +0200 Subject: Implemented new ms user creation + PASSWORD_CHANGE_REQUIRED. Improved ms login design + password change. Improved ms main menu and current user design. --- .../Views/MachineCreationDialog.xaml | 2 +- .../Images/login.png | Bin 0 -> 6401 bytes .../Tango.MachineStudio.UsersAndRoles.csproj | 21 +++- .../ViewModels/MainViewVM.cs | 31 +++-- .../ViewModels/UserCreationDialogVM.cs | 74 +++++++++++ .../Views/UserCreationDialog.xaml | 53 ++++++++ .../Views/UserCreationDialog.xaml.cs | 28 +++++ .../Views/UserManagementView.xaml | 2 +- .../Views/UserView.xaml | 2 - .../packages.config | 1 + .../Web/LoginResponse.cs | 1 + .../DefaultAuthenticationProvider.cs | 8 ++ .../Tango.MachineStudio.UI/Images/login_white.png | Bin 0 -> 3308 bytes .../Tango.MachineStudio.UI.csproj | 1 + .../ViewModels/LoginViewVM.cs | 135 ++++++++++++++++++++- .../Tango.MachineStudio.UI/Views/LoginView.xaml | 45 ++++++- .../Tango.MachineStudio.UI/Views/MainView.xaml | 75 ++++++------ 17 files changed, 417 insertions(+), 62 deletions(-) create mode 100644 Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Images/login.png create mode 100644 Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/UserCreationDialogVM.cs create mode 100644 Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml create mode 100644 Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml.cs create mode 100644 Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Images/login_white.png (limited to 'Software/Visual_Studio/MachineStudio') diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.MachineDesigner/Views/MachineCreationDialog.xaml b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.MachineDesigner/Views/MachineCreationDialog.xaml index e3ba1bff4..2d380c0d2 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.MachineDesigner/Views/MachineCreationDialog.xaml +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.MachineDesigner/Views/MachineCreationDialog.xaml @@ -8,7 +8,7 @@ xmlns:vm="clr-namespace:Tango.MachineStudio.MachineDesigner.ViewModels" xmlns:local="clr-namespace:Tango.MachineStudio.MachineDesigner.Views" mc:Ignorable="d" - d:DesignHeight="400" d:DesignWidth="700" Height="400" Width="700" Background="White" d:DataContext="{d:DesignInstance Type=vm:MachineCreationDialogVM, IsDesignTimeCreatable=False}"> + d:DesignHeight="400" d:DesignWidth="700" Height="400" Width="700" Background="{StaticResource WhiteBackgroundBrush}" d:DataContext="{d:DesignInstance Type=vm:MachineCreationDialogVM, IsDesignTimeCreatable=False}"> diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Images/login.png b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Images/login.png new file mode 100644 index 000000000..9f7d0b9ba Binary files /dev/null and b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Images/login.png differ diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Tango.MachineStudio.UsersAndRoles.csproj b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Tango.MachineStudio.UsersAndRoles.csproj index 82376b751..035cb2b9d 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Tango.MachineStudio.UsersAndRoles.csproj +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Tango.MachineStudio.UsersAndRoles.csproj @@ -49,6 +49,9 @@ ..\..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + ..\..\..\packages\SimpleValidator.0.6.1.0\lib\net40\SimpleValidator.dll + @@ -80,6 +83,7 @@ + AddressView.xaml @@ -95,6 +99,9 @@ OrganizationSelectionView.xaml + + UserCreationDialog.xaml + UserManagementView.xaml @@ -120,7 +127,9 @@ ResXFileCodeGenerator Resources.Designer.cs - + + Designer + SettingsSingleFileGenerator @@ -186,6 +195,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -210,11 +223,13 @@ - + + + - + \ No newline at end of file diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/MainViewVM.cs b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/MainViewVM.cs index 2f4e5c9a3..81ef04dd4 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/MainViewVM.cs +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/MainViewVM.cs @@ -370,20 +370,19 @@ namespace Tango.MachineStudio.UsersAndRoles.ViewModels } } - private async void AddNewUser() + private void AddNewUser() { - String email = _notification.ShowTextInput("Enter user email", "email"); - - if (!String.IsNullOrWhiteSpace(email)) + _notification.ShowModalDialog(async (vm) => { User user = new User(); - user.Email = email; - user.Password = "1111"; + user.Email = vm.Email; + user.Password = User.GetPasswordHash(vm.Password); + user.PasswordChangeRequired = true; user.Contact = new Contact() { - FirstName = "Twine", - LastName = "User", - Email = email, + FirstName = vm.FirstName, + LastName = vm.LastName, + Email = vm.Email, }; user.Address = new Address(); @@ -394,6 +393,18 @@ namespace Tango.MachineStudio.UsersAndRoles.ViewModels Role = _manageContext.Roles.SingleOrDefault(x => x.Code == (int)BL.Enumerations.Roles.User) }); + user.UsersRoles.Add(new UsersRole() + { + User = user, + Role = _manageContext.Roles.SingleOrDefault(x => x.Code == (int)BL.Enumerations.Roles.MachineStudioUser) + }); + + user.UsersRoles.Add(new UsersRole() + { + User = user, + Role = _manageContext.Roles.SingleOrDefault(x => x.Code == (int)BL.Enumerations.Roles.PPCUser) + }); + try { user.Validate(_manageContext); @@ -413,7 +424,7 @@ namespace Tango.MachineStudio.UsersAndRoles.ViewModels await LoadOrganizations(); SelectedOrganization = Organizations.SingleOrDefault(x => x.Guid == ManagedOrganization.Guid); } - } + }); } private void SetUserPlace(Place place) diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/UserCreationDialogVM.cs b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/UserCreationDialogVM.cs new file mode 100644 index 000000000..08762ac96 --- /dev/null +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/ViewModels/UserCreationDialogVM.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SimpleValidator.Extensions; +using Tango.SharedUI; + +namespace Tango.MachineStudio.UsersAndRoles.ViewModels +{ + public class UserCreationDialogVM : DialogViewVM + { + private static Random rnd = new Random(); + + private String _email; + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Please provide a valid email")] + public String Email + { + get { return _email; } + set { _email = value; RaisePropertyChangedAuto(); InvalidateRelayCommands(); } + } + + private String _password; + public String Password + { + get { return _password; } + set { _password = value; RaisePropertyChangedAuto(); } + } + + private String _firstName; + [Required(ErrorMessage = "First name is required")] + public String FirstName + { + get { return _firstName; } + set { _firstName = value; RaisePropertyChangedAuto(); InvalidateRelayCommands(); } + } + + private String _lastName; + [Required(ErrorMessage = "Last name is required")] + public String LastName + { + get { return _lastName; } + set { _lastName = value; RaisePropertyChangedAuto(); InvalidateRelayCommands(); } + } + + protected override void Accept() + { + if (Validate()) + { + base.Accept(); + } + } + + public override void OnShow() + { + base.OnShow(); + Password = GetRandomPassword(4); + } + + private String GetRandomPassword(int count) + { + String pass = String.Empty; + + for (int i = 0; i < count; i++) + { + pass += rnd.Next(0, 9).ToString(); + } + + return pass; + } + } +} diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml new file mode 100644 index 000000000..7433d3768 --- /dev/null +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + NEW USER + + + + + + + + + + + + + + EMAIL + + FIRST NAME + + LAST NAME + + + + + Provide this password to the user + + + + + diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml.cs b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml.cs new file mode 100644 index 000000000..cfa389ed1 --- /dev/null +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserCreationDialog.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Tango.MachineStudio.UsersAndRoles.Views +{ + /// + /// Interaction logic for UserCreationDialog.xaml + /// + public partial class UserCreationDialog : UserControl + { + public UserCreationDialog() + { + InitializeComponent(); + } + } +} diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserManagementView.xaml b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserManagementView.xaml index 5246ae09c..3964abfc8 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserManagementView.xaml +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserManagementView.xaml @@ -50,7 +50,7 @@ LOGIN - + diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserView.xaml b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserView.xaml index 0858d7e08..37f649a7a 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserView.xaml +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/Views/UserView.xaml @@ -16,8 +16,6 @@ - - diff --git a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/packages.config b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/packages.config index fe4f26e87..8696cb880 100644 --- a/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/packages.config +++ b/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.UsersAndRoles/packages.config @@ -7,4 +7,5 @@ + \ No newline at end of file diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.Common/Web/LoginResponse.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.Common/Web/LoginResponse.cs index 4ae22fa93..3515c32d1 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.Common/Web/LoginResponse.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.Common/Web/LoginResponse.cs @@ -14,5 +14,6 @@ namespace Tango.MachineStudio.Common.Web public DataSource DataSource { get; set; } public bool VersionChangeRequired { get; set; } public String RequiredVersion { get; set; } + public bool PasswordChangeRequired { get; set; } } } diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Authentication/DefaultAuthenticationProvider.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Authentication/DefaultAuthenticationProvider.cs index 0131cd209..26938b203 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Authentication/DefaultAuthenticationProvider.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Authentication/DefaultAuthenticationProvider.cs @@ -147,6 +147,14 @@ namespace Tango.MachineStudio.UI.Authentication }; } + if (response.PasswordChangeRequired) + { + return new AuthenticationLoginResult() + { + Response = response + }; + } + try { ObservablesStaticCollections.Instance.Initialize((x) => diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Images/login_white.png b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Images/login_white.png new file mode 100644 index 000000000..10a054147 Binary files /dev/null and b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Images/login_white.png differ diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj index 13f22dfda..0525c2351 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Tango.MachineStudio.UI.csproj @@ -363,6 +363,7 @@ TCC\template.bmp Always + diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoginViewVM.cs b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoginViewVM.cs index dce469dbd..c00caf72a 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoginViewVM.cs +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/ViewModels/LoginViewVM.cs @@ -22,6 +22,9 @@ using Tango.MachineStudio.UI.Messages; using Tango.Settings; using Tango.SharedUI; using Tango.Web; +using SimpleValidator.Extensions; +using Tango.BL.Entities; +using System.Data.Entity; namespace Tango.MachineStudio.UI.ViewModels { @@ -38,6 +41,7 @@ namespace Tango.MachineStudio.UI.ViewModels private Rfc2898Cryptographer cryptographer; private MachineStudioSettings _settings; private MachineStudioWebClient _machineStudioWebClient; + private TaskCompletionSource _updatePasswordCompletionSource; private String _email; /// @@ -82,6 +86,14 @@ namespace Tango.MachineStudio.UI.ViewModels set { _isLogging = value; RaisePropertyChangedAuto(); } } + private bool _showLogginDetails; + public bool ShowLoggingDetails + { + get { return _showLogginDetails; } + set { _showLogginDetails = value; RaisePropertyChangedAuto(); } + } + + private bool _rememberMe; /// /// Gets or sets a value indicating whether to remember the last user email and password. @@ -123,11 +135,37 @@ namespace Tango.MachineStudio.UI.ViewModels set { _progressLog = value; RaisePropertyChangedAuto(); } } + private bool _isChangingPassword; + public bool IsChangingPassword + { + get { return _isChangingPassword; } + set { _isChangingPassword = value; RaisePropertyChangedAuto(); } + } + + private String _newPassword1; + public String NewPassword1 + { + get { return _newPassword1; } + set { _newPassword1 = value; RaisePropertyChangedAuto(); } + } + + private String _newPassword2; + public String NewPassword2 + { + get { return _newPassword2; } + set { _newPassword2 = value; RaisePropertyChangedAuto(); } + } + /// /// Gets or sets the login command. /// public RelayCommand LoginCommand { get; set; } + /// + /// Gets or sets the update password command. + /// + public RelayCommand UpdatePasswordCommand { get; set; } + /// /// Initializes a new instance of the class. /// @@ -137,6 +175,7 @@ namespace Tango.MachineStudio.UI.ViewModels public LoginViewVM(MachineStudioWebClient machineStudioWebClient, IAuthenticationProvider authenticationProvider, INavigationManager navigationManager, INotificationProvider notificationProvider, IEventLogger eventLogger) { EnableSlotSelection = true; + ShowLoggingDetails = true; _machineStudioWebClient = machineStudioWebClient; _settings = SettingsManager.Default.GetOrCreate(); @@ -146,6 +185,7 @@ namespace Tango.MachineStudio.UI.ViewModels _authenticationProvider = authenticationProvider; _eventLogger = eventLogger; LoginCommand = new RelayCommand(Login, () => !IsLogging); + UpdatePasswordCommand = new RelayCommand(UpdatePassword, () => IsChangingPassword); cryptographer = new Rfc2898Cryptographer(); Email = _settings.LastLoginEmail; @@ -181,6 +221,10 @@ namespace Tango.MachineStudio.UI.ViewModels try { IsLogging = true; + ShowLoggingDetails = false; + NewPassword1 = String.Empty; + NewPassword2 = String.Empty; + InvalidateRelayCommands(); LoginMethod loginMethod = IsActiveDirectory ? LoginMethod.ActiveDirectory : LoginMethod.StandardUser; @@ -190,9 +234,9 @@ namespace Tango.MachineStudio.UI.ViewModels _settings.DeploymentSlot = DeploymentSlot; LoginResponse result = _authenticationProvider.Login(Email, Password, loginMethod, _settings.ByPassEnvironmentVersionCheck, (progress) => - { - ProgressLog = progress; - }).Response; + { + ProgressLog = progress; + }).Response; if (result.VersionChangeRequired && !_settings.ByPassEnvironmentVersionCheck) { @@ -211,6 +255,14 @@ namespace Tango.MachineStudio.UI.ViewModels return; } + if (result.PasswordChangeRequired) + { + StartUpdatePassword().Task.GetAwaiter().GetResult(); + Password = NewPassword1; + Login(); + return; + } + _eventLogger.Log(EventTypes.APPLICATION_STARTED, "Application Started!"); _navigationManager.NavigateTo(NavigationView.MainView); @@ -224,19 +276,94 @@ namespace Tango.MachineStudio.UI.ViewModels _eventLogger.Log("User logged in."); EnableSlotSelection = false; + + IsLogging = false; + ShowLoggingDetails = true; + IsChangingPassword = false; + InvalidateRelayCommands(); }); } catch (Exception ex) { + IsLogging = false; + ShowLoggingDetails = true; + IsChangingPassword = false; + InvalidateRelayCommands(); LogManager.Log(ex, "Login Error."); _notificationProvider.ShowError($"An error occurred while trying to perform the log-in operation.\n{ex.FlattenMessage()}"); } - finally + } + } + + private TaskCompletionSource StartUpdatePassword() + { + _updatePasswordCompletionSource = new TaskCompletionSource(); + + IsChangingPassword = true; + ShowLoggingDetails = false; + IsLogging = false; + InvalidateRelayCommands(); + + return _updatePasswordCompletionSource; + } + + private async void UpdatePassword() + { + await Task.Factory.StartNew(() => + { + try + { + if (!Validate()) + { + return; + } + + ProgressLog = "Updating your password..."; + IsChangingPassword = false; + IsLogging = true; + InvalidateRelayCommands(); + + using (var db = ObservablesContext.CreateDefault()) + { + var user = db.Users.SingleOrDefault(x => x.Email == Email); + user.PasswordChangeRequired = false; + user.Password = User.GetPasswordHash(NewPassword1); + db.SaveChanges(); + } + + _updatePasswordCompletionSource.SetResult(true); + } + catch (Exception ex) { IsLogging = false; + IsChangingPassword = false; + ShowLoggingDetails = true; + InvalidateRelayCommands(); + _updatePasswordCompletionSource.SetException(ex); + } + finally + { InvalidateRelayCommands(); } + }); + } + + protected override void OnValidating() + { + if (IsChangingPassword) + { + if (!NewPassword1.IsBetweenLength(6, 8)) + { + InsertError(nameof(NewPassword1), "Password must be 6 to 8 characters long"); + } + + if (NewPassword1 != NewPassword2) + { + InsertError(nameof(NewPassword2), "Passwords do not match"); + } } + + base.OnValidating(); } } } diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/LoginView.xaml b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/LoginView.xaml index ff13ec2c7..9a3b3405e 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/LoginView.xaml +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/LoginView.xaml @@ -47,7 +47,7 @@ - + @@ -57,10 +57,36 @@ - + + + + + - + Login to your account @@ -97,6 +123,19 @@ + + + Password change required + + + + + + + + + + diff --git a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml index 48f7b46d3..2d5a5c3aa 100644 --- a/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml +++ b/Software/Visual_Studio/MachineStudio/Tango.MachineStudio.UI/Views/MainView.xaml @@ -50,48 +50,19 @@ + + - - - - - - - - , - - - - - - - - - - - - - - - ... - - - - - - - - - - - - Home - - + MODULES @@ -148,8 +119,36 @@ - + + + + + + + + + , + + + + + + + + + + + + + + + ... + + + +