Merge pull request #7 from oqtane/master

sync with upstream
This commit is contained in:
Shaun Walker 2019-10-08 15:43:32 -04:00 committed by GitHub
commit dce53e10b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 58 deletions

View File

@ -10,7 +10,7 @@
<label for="Name" class="control-label">Name: </label> <label for="Name" class="control-label">Name: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@name" readonly /> <input class="form-control" @bind="@name" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -18,7 +18,7 @@
<label for="Name" class="control-label">Path: </label> <label for="Name" class="control-label">Path: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@path" readonly /> <input class="form-control" @bind="@path" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -26,7 +26,7 @@
<label for="Name" class="control-label">Parent: </label> <label for="Name" class="control-label">Parent: </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@parentid" readonly> <select class="form-control" @bind="@parentid" disabled>
<option value="">&lt;Select Parent&gt;</option> <option value="">&lt;Select Parent&gt;</option>
@foreach (Page p in PageState.Pages) @foreach (Page p in PageState.Pages)
{ {
@ -40,7 +40,7 @@
<label for="Name" class="control-label">Navigation? </label> <label for="Name" class="control-label">Navigation? </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@isnavigation" readonly> <select class="form-control" @bind="@isnavigation" disabled>
<option value="True">Yes</option> <option value="True">Yes</option>
<option value="False">No</option> <option value="False">No</option>
</select> </select>
@ -51,7 +51,7 @@
<label for="Name" class="control-label">Default Mode? </label> <label for="Name" class="control-label">Default Mode? </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@mode" readonly> <select class="form-control" @bind="@mode" disabled>
<option value="view">View Mode</option> <option value="view">View Mode</option>
<option value="edit">Edit Mode</option> <option value="edit">Edit Mode</option>
</select> </select>
@ -62,7 +62,7 @@
<label for="Name" class="control-label">Theme: </label> <label for="Name" class="control-label">Theme: </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@themetype" readonly> <select class="form-control" @bind="@themetype" disabled>
<option value="">&lt;Select Theme&gt;</option> <option value="">&lt;Select Theme&gt;</option>
@foreach (KeyValuePair<string, string> item in themes) @foreach (KeyValuePair<string, string> item in themes)
{ {
@ -76,7 +76,7 @@
<label for="Name" class="control-label">Layout: </label> <label for="Name" class="control-label">Layout: </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@layouttype" readonly> <select class="form-control" @bind="@layouttype" disabled>
<option value="">&lt;Select Layout&gt;</option> <option value="">&lt;Select Layout&gt;</option>
@foreach (KeyValuePair<string, string> panelayout in panelayouts) @foreach (KeyValuePair<string, string> panelayout in panelayouts)
{ {
@ -90,7 +90,7 @@
<label for="Name" class="control-label">Icon: </label> <label for="Name" class="control-label">Icon: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@icon" readonly /> <input class="form-control" @bind="@icon" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -98,7 +98,7 @@
<label for="Name" class="control-label">Is Deleted? </label> <label for="Name" class="control-label">Is Deleted? </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@isdeleted" readonly> <select class="form-control" @bind="@isdeleted" disabled>
<option value="True">Yes</option> <option value="True">Yes</option>
<option value="False">No</option> <option value="False">No</option>
</select> </select>

View File

@ -16,7 +16,7 @@ else
<label for="Name" class="control-label">Name: </label> <label for="Name" class="control-label">Name: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@name" readonly /> <input class="form-control" @bind="@name" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -24,7 +24,7 @@ else
<label for="Name" class="control-label">Logo: </label> <label for="Name" class="control-label">Logo: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@logo" readonly /> <input class="form-control" @bind="@logo" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -32,7 +32,7 @@ else
<label for="Name" class="control-label">Default Theme: </label> <label for="Name" class="control-label">Default Theme: </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@themetype" readonly> <select class="form-control" @bind="@themetype" disabled>
<option value="">&lt;Select Theme&gt;</option> <option value="">&lt;Select Theme&gt;</option>
@foreach (KeyValuePair<string, string> item in themes) @foreach (KeyValuePair<string, string> item in themes)
{ {
@ -46,7 +46,7 @@ else
<label for="Name" class="control-label">Default Layout: </label> <label for="Name" class="control-label">Default Layout: </label>
</td> </td>
<td> <td>
<select class="form-control" @bind="@layouttype" readonly> <select class="form-control" @bind="@layouttype" disabled>
<option value="">&lt;Select Layout&gt;</option> <option value="">&lt;Select Layout&gt;</option>
@foreach (KeyValuePair<string, string> panelayout in panelayouts) @foreach (KeyValuePair<string, string> panelayout in panelayouts)
{ {
@ -55,9 +55,23 @@ else
</select> </select>
</td> </td>
</tr> </tr>
<tr>
<td>
<label for="Name" class="control-label">Is Deleted? </label>
</td>
<td>
<select class="form-control" @bind="@isdeleted" disabled>
<option value="True">Yes</option>
<option value="False">No</option>
</select>
</td>
</tr>
</table> </table>
<button type="button" class="btn btn-success" @onclick="DeleteSite">Delete</button> <button type="button" class="btn btn-success" @onclick="DeleteSite">Delete</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@code { @code {
@ -70,6 +84,13 @@ else
string logo = ""; string logo = "";
string themetype; string themetype;
string layouttype; string layouttype;
string createdby;
DateTime createdon;
string modifiedby;
DateTime modifiedon;
string deletedby;
DateTime? deletedon;
string isdeleted;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -79,6 +100,14 @@ else
logo = PageState.Site.Logo; logo = PageState.Site.Logo;
themetype = PageState.Site.DefaultThemeType; themetype = PageState.Site.DefaultThemeType;
layouttype = PageState.Site.DefaultLayoutType; layouttype = PageState.Site.DefaultLayoutType;
createdby = PageState.Site.CreatedBy;
createdon = PageState.Site.CreatedOn;
modifiedby = PageState.Site.ModifiedBy;
modifiedon = PageState.Site.ModifiedOn;
deletedby = PageState.Site.DeletedBy;
deletedon = PageState.Site.DeletedOn;
isdeleted = PageState.Site.IsDeleted.ToString();
} }
private async Task DeleteSite() private async Task DeleteSite()

View File

@ -55,9 +55,23 @@ else
</select> </select>
</td> </td>
</tr> </tr>
<tr>
<td>
<label for="Name" class="control-label">Is Deleted? </label>
</td>
<td>
<select class="form-control" @bind="@isdeleted">
<option value="True">Yes</option>
<option value="False">No</option>
</select>
</td>
</tr>
</table> </table>
<button type="button" class="btn btn-success" @onclick="SaveSite">Save</button> <button type="button" class="btn btn-success" @onclick="SaveSite">Save</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@code { @code {
@ -71,6 +85,14 @@ else
string themetype; string themetype;
string layouttype; string layouttype;
string createdby;
DateTime createdon;
string modifiedby;
DateTime modifiedon;
string deletedby;
DateTime? deletedon;
string isdeleted;
protected override void OnInitialized() protected override void OnInitialized()
{ {
themes = ThemeService.GetThemeTypes(PageState.Themes); themes = ThemeService.GetThemeTypes(PageState.Themes);
@ -79,6 +101,14 @@ else
logo = PageState.Site.Logo; logo = PageState.Site.Logo;
themetype = PageState.Site.DefaultThemeType; themetype = PageState.Site.DefaultThemeType;
layouttype = PageState.Site.DefaultLayoutType; layouttype = PageState.Site.DefaultLayoutType;
createdby = PageState.Site.CreatedBy;
createdon = PageState.Site.CreatedOn;
modifiedby = PageState.Site.ModifiedBy;
modifiedon = PageState.Site.ModifiedOn;
deletedby = PageState.Site.DeletedBy;
deletedon = PageState.Site.DeletedOn;
isdeleted = PageState.Site.IsDeleted.ToString();
} }
private async Task SaveSite() private async Task SaveSite()
@ -88,6 +118,8 @@ else
site.Logo = (logo == null ? "" : logo); site.Logo = (logo == null ? "" : logo);
site.DefaultThemeType = themetype; site.DefaultThemeType = themetype;
site.DefaultLayoutType = (layouttype == null ? "" : layouttype); site.DefaultLayoutType = (layouttype == null ? "" : layouttype);
site.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
site = await SiteService.UpdateSiteAsync(site); site = await SiteService.UpdateSiteAsync(site);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -100,8 +100,10 @@
user.Username = username; user.Username = username;
user.Password = password; user.Password = password;
user.Email = email; user.Email = email;
user.DisplayName = displayname; user.DisplayName = string.IsNullOrWhiteSpace(user.DisplayName) ? user.Username : user.DisplayName;
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(settings, user.UserId);

View File

@ -5,7 +5,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@if (profiles != null) @if (!string.IsNullOrWhiteSpace(username))
{ {
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
@ -13,7 +13,7 @@
<label for="Name" class="control-label">Username: </label> <label for="Name" class="control-label">Username: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@username" readonly /> <input class="form-control" @bind="@username" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -21,7 +21,7 @@
<label for="Name" class="control-label">Email: </label> <label for="Name" class="control-label">Email: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@email" readonly /> <input class="form-control" @bind="@email" disabled />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -29,34 +29,26 @@
<label for="Name" class="control-label">Full Name: </label> <label for="Name" class="control-label">Full Name: </label>
</td> </td>
<td> <td>
<input class="form-control" @bind="@displayname" readonly /> <input class="form-control" @bind="@displayname" disabled />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">Is Deleted? </label>
</td>
<td>
<select class="form-control" @bind="@isdeleted" disabled>
<option value="True">Yes</option>
<option value="False">No</option>
</select>
</td> </td>
</tr> </tr>
@foreach (Profile profile in profiles)
{
var p = profile;
if (p.Category != category)
{
<tr>
<th colspan="2" style="text-align: center;">
@p.Category
</th>
</tr>
category = p.Category;
}
<tr>
<td>
<label for="@p.Name" class="control-label">@p.Title: </label>
</td>
<td>
<input class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" readonly />
</td>
</tr>
}
</table> </table>
<button type="button" class="btn btn-primary" @onclick="DeleteUser">Delete</button> <button type="button" class="btn btn-primary" @onclick="DeleteUser">Delete</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@code { @code {
@ -66,16 +58,19 @@
string username = ""; string username = "";
string email = ""; string email = "";
string displayname = ""; string displayname = "";
List<Profile> profiles;
Dictionary<string, string> settings;
string category = ""; string category = "";
string createdby;
DateTime createdon;
string modifiedby;
DateTime modifiedon;
string deletedby;
DateTime? deletedon;
string isdeleted;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
userid = Int32.Parse(PageState.QueryString["id"]); userid = Int32.Parse(PageState.QueryString["id"]);
User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
if (user != null) if (user != null)
@ -83,7 +78,13 @@
username = user.Username; username = user.Username;
email = user.Email; email = user.Email;
displayname = user.DisplayName; displayname = user.DisplayName;
settings = await SettingService.GetUserSettingsAsync(user.UserId); createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
isdeleted = user.IsDeleted.ToString();
} }
} }
catch (Exception ex) catch (Exception ex)
@ -92,11 +93,6 @@
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue)
{
return SettingService.GetSetting(settings, SettingName, DefaultValue);
}
private async Task DeleteUser() private async Task DeleteUser()
{ {
try try

View File

@ -62,9 +62,23 @@
</td> </td>
</tr> </tr>
} }
<tr>
<td>
<label for="Name" class="control-label">Is Deleted? </label>
</td>
<td>
<select class="form-control" @bind="@isdeleted">
<option value="True">Yes</option>
<option value="False">No</option>
</select>
</td>
</tr>
</table> </table>
<button type="button" class="btn btn-primary" @onclick="SaveUser">Save</button> <button type="button" class="btn btn-primary" @onclick="SaveUser">Save</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@code { @code {
@ -78,6 +92,13 @@
List<Profile> profiles; List<Profile> profiles;
Dictionary<string, string> settings; Dictionary<string, string> settings;
string category = ""; string category = "";
string createdby;
DateTime createdon;
string modifiedby;
DateTime modifiedon;
string deletedby;
DateTime? deletedon;
string isdeleted;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -93,6 +114,13 @@
email = user.Email; email = user.Email;
displayname = user.DisplayName; displayname = user.DisplayName;
settings = await SettingService.GetUserSettingsAsync(user.UserId); settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
isdeleted = user.IsDeleted.ToString();
} }
} }
catch (Exception ex) catch (Exception ex)
@ -115,7 +143,9 @@
user.Username = username; user.Username = username;
user.Password = password; user.Password = password;
user.Email = email; user.Email = email;
user.DisplayName = displayname; user.DisplayName = string.IsNullOrWhiteSpace(user.DisplayName) ? user.Username : user.DisplayName;
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(settings, user.UserId);

View File

@ -152,9 +152,19 @@ namespace Oqtane.Controllers
// DELETE api/<controller>/5?siteid=x // DELETE api/<controller>/5?siteid=x
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize(Roles = Constants.AdminRole)] [Authorize(Roles = Constants.AdminRole)]
public void Delete(int id) public async Task Delete(int id)
{ {
Users.DeleteUser(id); IdentityUser identityuser = await IdentityUserManager.FindByNameAsync(Users.GetUser(id).Username);
if (identityuser != null)
{
var result = await IdentityUserManager.DeleteAsync(identityuser);
if (result != null)
{
Users.DeleteUser(id);
}
}
} }
// POST api/<controller>/login // POST api/<controller>/login

View File

@ -14,6 +14,9 @@ CREATE TABLE [dbo].[Site](
[CreatedOn] [datetime] NOT NULL, [CreatedOn] [datetime] NOT NULL,
[ModifiedBy] [nvarchar](256) NOT NULL, [ModifiedBy] [nvarchar](256) NOT NULL,
[ModifiedOn] [datetime] NOT NULL, [ModifiedOn] [datetime] NOT NULL,
[DeletedBy] [nvarchar](256) NULL,
[DeletedOn] [datetime] NULL,
[IsDeleted][bit] NOT NULL
CONSTRAINT [PK_Site] PRIMARY KEY CLUSTERED CONSTRAINT [PK_Site] PRIMARY KEY CLUSTERED
( (
[SiteId] ASC [SiteId] ASC
@ -91,6 +94,9 @@ CREATE TABLE [dbo].[User](
[CreatedOn] [datetime] NOT NULL, [CreatedOn] [datetime] NOT NULL,
[ModifiedBy] [nvarchar](256) NOT NULL, [ModifiedBy] [nvarchar](256) NOT NULL,
[ModifiedOn] [datetime] NOT NULL, [ModifiedOn] [datetime] NOT NULL,
[DeletedBy] [nvarchar](256) NULL,
[DeletedOn] [datetime] NULL,
[IsDeleted][bit] NOT NULL
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
( (
[UserId] ASC [UserId] ASC

View File

@ -2,7 +2,7 @@
namespace Oqtane.Models namespace Oqtane.Models
{ {
public class Site : IAuditable public class Site : IAuditable, IDeletable
{ {
public int SiteId { get; set; } public int SiteId { get; set; }
public string Name { get; set; } public string Name { get; set; }
@ -15,5 +15,8 @@ namespace Oqtane.Models
public DateTime CreatedOn { get; set; } public DateTime CreatedOn { get; set; }
public string ModifiedBy { get; set; } public string ModifiedBy { get; set; }
public DateTime ModifiedOn { get; set; } public DateTime ModifiedOn { get; set; }
public string DeletedBy { get; set; }
public DateTime? DeletedOn { get; set; }
public bool IsDeleted { get; set; }
} }
} }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models namespace Oqtane.Models
{ {
public class User : IAuditable public class User : IAuditable, IDeletable
{ {
public int UserId { get; set; } public int UserId { get; set; }
public string Username { get; set; } public string Username { get; set; }
@ -19,6 +19,9 @@ namespace Oqtane.Models
public DateTime CreatedOn { get; set; } public DateTime CreatedOn { get; set; }
public string ModifiedBy { get; set; } public string ModifiedBy { get; set; }
public DateTime ModifiedOn { get; set; } public DateTime ModifiedOn { get; set; }
public string DeletedBy { get; set; }
public DateTime? DeletedOn { get; set; }
public bool IsDeleted { get; set; }
[NotMapped] [NotMapped]
public string Password { get; set; } public string Password { get; set; }

View File

@ -11,7 +11,7 @@ Oqtane uses Blazor, a new web framework for .NET Core that lets you build intera
2.&nbsp;Install the latest edition of [Visual Studio 2019](https://visualstudio.com/vs/) with the **ASP.NET and web development** workload. Installing the latest edition will also install the latest version of .NET Core 3.0. 2.&nbsp;Install the latest edition of [Visual Studio 2019](https://visualstudio.com/vs/) with the **ASP.NET and web development** workload. Installing the latest edition will also install the latest version of .NET Core 3.0.
3.&nbsp;Download or Clone the Oqtane source code to your local system. Open the **Oqtane.sln** solution file. If you want to develop using **server-side** Blazor ( which includes a full debugging experience in Visual Studio ) you should choose to Build the solution using the default Debug configuration. If you want to develop using **client-side** Blazor ( WebAssembly ) you should first choose the "Wasm" configuration option in the Visual Studio toolbar and then Build. 3.&nbsp;Download or Clone the Oqtane source code to your local system. Open the **Oqtane.sln** solution file. If you want to develop using **server-side** Blazor (which includes a full debugging experience in Visual Studio) you should choose to Build the solution using the default Debug configuration. If you want to develop using **client-side** Blazor (WebAssembly) you should first choose the "Wasm" configuration option in the Visual Studio toolbar and then Build.
NOTE: If you have already installed a previous version of Oqtane and you wish to install a newer version, there is currently no upgrade path from one version to the next. The recommended upgrade approach is to get the latest code and build it, and then reset the DefaultConnection value to "" in the appsettings.json file in the Oqtane.server project. This will trigger a re-install when you run the application which will execute the latest database scripts. NOTE: If you have already installed a previous version of Oqtane and you wish to install a newer version, there is currently no upgrade path from one version to the next. The recommended upgrade approach is to get the latest code and build it, and then reset the DefaultConnection value to "" in the appsettings.json file in the Oqtane.server project. This will trigger a re-install when you run the application which will execute the latest database scripts.
@ -27,7 +27,6 @@ Design
- Need to cleanly separate site.css - Need to cleanly separate site.css
Admin Admin
- Need fully functional administrative modules for all core entities ( user, role, site, etc… )
- Need ability to soft delete core entities - Need ability to soft delete core entities
- Drag and Drop modules - Drag and Drop modules
@ -40,7 +39,7 @@ Database
- Need ability to run on SQLite - Need ability to run on SQLite
# Background # Background
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology. It is a modular framework offering a fully dynamic page compositing model, multi-site support, designer friendly templates ( skins ), and extensibility via third party modules. Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology. It is a modular framework offering a fully dynamic page compositing model, multi-site support, designer friendly templates (skins), and extensibility via third party modules.
At this point Oqtane offers a minimum of desired functionality and is not recommended for production usage. The expectation is that Oqtane will rapidly evolve as a community driven open source project. At this point in time we do not promise any upgrade path from one version to the next, and developers should expect breaking changes as the framework stabilizes. At this point Oqtane offers a minimum of desired functionality and is not recommended for production usage. The expectation is that Oqtane will rapidly evolve as a community driven open source project. At this point in time we do not promise any upgrade path from one version to the next, and developers should expect breaking changes as the framework stabilizes.