mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 03:25:23 +08:00
Refining extension methods. Adding a streamlined content manager creation method.
--HG-- extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4041639
This commit is contained in:
@@ -4,7 +4,7 @@ using Orchard.Models.Driver;
|
||||
using Orchard.Models.Records;
|
||||
|
||||
namespace Orchard.Tests.Models.Stubs {
|
||||
public class Gamma : ContentPartForRecord<GammaRecord> {
|
||||
public class Gamma : ContentPart<GammaRecord> {
|
||||
}
|
||||
|
||||
public class GammaRecord : ContentPartRecord {
|
||||
@@ -15,7 +15,7 @@ namespace Orchard.Tests.Models.Stubs {
|
||||
public class GammaProvider : ContentProvider {
|
||||
public GammaProvider(IRepository<GammaRecord> repository){
|
||||
Filters.Add(new ActivatingFilter<Gamma>(x => x == "gamma"));
|
||||
Filters.Add(new StorageFilterForRecord<GammaRecord>(repository));
|
||||
Filters.Add(new StorageFilter<GammaRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ using Orchard.Models;
|
||||
using Orchard.Security;
|
||||
|
||||
namespace Orchard.Core.Common.Models {
|
||||
public class CommonPart : ContentPartForRecord<CommonRecord> {
|
||||
public class CommonPart : ContentPart<CommonRecord> {
|
||||
private readonly Lazy<IUser> _owner = new Lazy<IUser>();
|
||||
private readonly Lazy<IContent> _container = new Lazy<IContent>();
|
||||
|
||||
|
@@ -22,7 +22,7 @@ namespace Orchard.Core.Common.Models {
|
||||
_contentManager = contentManager;
|
||||
|
||||
AddOnCreating<CommonPart>(SetCreateTimesAndAuthor);
|
||||
Filters.Add(new StorageFilterForRecord<CommonRecord>(repository));
|
||||
Filters.Add(new StorageFilter<CommonRecord>(repository));
|
||||
AddOnLoaded<CommonPart>(LoadOwnerModel);
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,6 @@ using Orchard.Core.Common.Records;
|
||||
using Orchard.Models;
|
||||
|
||||
namespace Orchard.Core.Common.Models {
|
||||
public class RoutablePart : ContentPartForRecord<RoutableRecord> {
|
||||
public class RoutablePart : ContentPart<RoutableRecord> {
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ using Orchard.Models.Driver;
|
||||
namespace Orchard.Core.Common.Models {
|
||||
public class RoutablePartProvider : ContentProvider {
|
||||
public RoutablePartProvider(IRepository<RoutableRecord> repository) {
|
||||
Filters.Add(new StorageFilterForRecord<RoutableRecord>(repository));
|
||||
Filters.Add(new StorageFilter<RoutableRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@ using Orchard.Models;
|
||||
using Orchard.Settings;
|
||||
|
||||
namespace Orchard.Core.Settings.Models {
|
||||
public sealed class SiteSettings : ContentPartForRecord<SiteSettingsRecord>, ISite {
|
||||
public sealed class SiteSettings : ContentPart<SiteSettingsRecord>, ISite {
|
||||
public string SiteName {
|
||||
get { return Record.SiteName; }
|
||||
set { Record.SiteName = value; }
|
||||
|
@@ -6,7 +6,7 @@ namespace Orchard.Core.Settings.Models {
|
||||
public class SiteSettingsProvider : ContentProvider {
|
||||
public SiteSettingsProvider(IRepository<SiteSettingsRecord> repository){
|
||||
Filters.Add(new ActivatingFilter<SiteSettings>("site"));
|
||||
Filters.Add(new StorageFilterForRecord<SiteSettingsRecord>(repository));
|
||||
Filters.Add(new StorageFilter<SiteSettingsRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,12 +25,12 @@ namespace Orchard.Core.Settings.Services {
|
||||
string applicationName = HttpContext.Current.Request.ApplicationPath;
|
||||
SiteSettingsRecord record = _siteSettingsRepository.Get(x => x.SiteUrl == applicationName);
|
||||
if (record == null) {
|
||||
SiteSettings site = _contentManager.New("site").As<SiteSettings>();
|
||||
site.Record.SiteUrl = applicationName;
|
||||
_contentManager.Create(site);
|
||||
ISite site = _contentManager.Create<SiteSettings>("site", item => {
|
||||
item.Record.SiteUrl = applicationName;
|
||||
});
|
||||
return site;
|
||||
}
|
||||
return _contentManager.Get(record.Id).As<ISite>();
|
||||
return _contentManager.Get<ISite>(record.Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using Orchard.Models;
|
||||
|
||||
namespace Orchard.Blogs.Models {
|
||||
public class Blog : ContentPartForRecord<BlogRecord> {
|
||||
public class Blog : ContentPart<BlogRecord> {
|
||||
public string Name { get { return Record.Name; } }
|
||||
public string Slug { get { return Record.Slug; } }
|
||||
public bool Enabled { get { return Record.Enabled; } }
|
||||
|
@@ -1,7 +1,7 @@
|
||||
using Orchard.Models;
|
||||
|
||||
namespace Orchard.Blogs.Models {
|
||||
public class BlogPost : ContentPartForRecord<BlogPostRecord> {
|
||||
public class BlogPost : ContentPart<BlogPostRecord> {
|
||||
public string BlogSlug { get { return Record.Blog.Slug; } }
|
||||
public string Title { get { return Record.Title; } }
|
||||
public string Slug { get { return Record.Slug; } }
|
||||
|
@@ -5,7 +5,7 @@ namespace Orchard.Blogs.Models {
|
||||
public class BlogPostProvider : ContentProvider {
|
||||
public BlogPostProvider(IRepository<BlogPostRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<BlogPost>("blogpost"));
|
||||
Filters.Add(new StorageFilterForRecord<BlogPostRecord>(repository));
|
||||
Filters.Add(new StorageFilter<BlogPostRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ namespace Orchard.Blogs.Models {
|
||||
public class BlogProvider : ContentProvider {
|
||||
public BlogProvider(IRepository<BlogRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<Blog>("blog"));
|
||||
Filters.Add(new StorageFilterForRecord<BlogRecord>(repository));
|
||||
Filters.Add(new StorageFilter<BlogRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,20 +20,17 @@ namespace Orchard.Blogs.Services {
|
||||
}
|
||||
|
||||
public IEnumerable<Blog> Get() {
|
||||
IEnumerable<BlogRecord> blogs =_repository.Fetch(br => br.Enabled, br => br.Asc(br2 => br2.Name));
|
||||
IEnumerable<BlogRecord> blogs = _repository.Fetch(br => br.Enabled, br => br.Asc(br2 => br2.Name));
|
||||
|
||||
return blogs.Select(br => _contentManager.Get(br.Id).As<Blog>());
|
||||
return blogs.Select(br => _contentManager.Get<Blog>(br.Id));
|
||||
}
|
||||
|
||||
public Blog CreateBlog(CreateBlogParams parameters) {
|
||||
BlogRecord record = new BlogRecord() {Name = parameters.Name, Slug = parameters.Slug, Enabled = parameters.Enabled};
|
||||
|
||||
//TODO: (erikpo) Need an extension method or something for this default behavior
|
||||
Blog blog = _contentManager.New<Blog>("blog");
|
||||
blog.Record = record;
|
||||
_contentManager.Create(blog);
|
||||
|
||||
return blog;
|
||||
return _contentManager.Create<Blog>("blog", init => {
|
||||
init.Record.Name = parameters.Name;
|
||||
init.Record.Slug = parameters.Slug;
|
||||
init.Record.Enabled = parameters.Enabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ using Orchard.Models.Driver;
|
||||
using Orchard.Models.Records;
|
||||
|
||||
namespace Orchard.Comments.Models {
|
||||
public class CommentSettings : ContentPartForRecord<CommentSettingsRecord> {
|
||||
public class CommentSettings : ContentPart<CommentSettingsRecord> {
|
||||
}
|
||||
|
||||
public class CommentSettingsRecord : ContentPartRecord {
|
||||
@@ -19,7 +19,7 @@ namespace Orchard.Comments.Models {
|
||||
public class CommentSettingsProvider : ContentProvider {
|
||||
public CommentSettingsProvider(IRepository<CommentSettingsRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<CommentSettings>("site"));
|
||||
Filters.Add(new StorageFilterForRecord<CommentSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new StorageFilter<CommentSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new TemplateFilterForRecord<CommentSettingsRecord>("CommentSettings"));
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ using Orchard.Models.Records;
|
||||
using Orchard.UI.Models;
|
||||
|
||||
namespace Orchard.Media.Models {
|
||||
public class MediaSettings : ContentPartForRecord<MediaSettingsRecord> {
|
||||
public class MediaSettings : ContentPart<MediaSettingsRecord> {
|
||||
}
|
||||
|
||||
public class MediaSettingsRecord : ContentPartRecord {
|
||||
@@ -15,7 +15,7 @@ namespace Orchard.Media.Models {
|
||||
public class MediaSettingsProvider : ContentProvider {
|
||||
public MediaSettingsProvider(IRepository<MediaSettingsRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<MediaSettings>("site"));
|
||||
Filters.Add(new StorageFilterForRecord<MediaSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new StorageFilter<MediaSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
}
|
||||
|
||||
protected override void GetEditors(GetEditorsContext context) {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
using Orchard.Security;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public sealed class User : ContentPartForRecord<UserRecord>, IUser {
|
||||
public sealed class User : ContentPart<UserRecord>, IUser {
|
||||
public int Id {
|
||||
get { return ContentItem.Id; }
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ namespace Orchard.Users.Models {
|
||||
public class UserProvider : ContentProvider {
|
||||
public UserProvider(IRepository<UserRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<User>("user"));
|
||||
Filters.Add(new StorageFilterForRecord<UserRecord>(repository));
|
||||
Filters.Add(new StorageFilter<UserRecord>(repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,16 +30,12 @@ namespace Orchard.Users.Services {
|
||||
|
||||
public IUser CreateUser(CreateUserParams createUserParams) {
|
||||
Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email);
|
||||
var record = new UserRecord {
|
||||
UserName = createUserParams.Username,
|
||||
Email = createUserParams.Email
|
||||
};
|
||||
SetPassword(record, createUserParams.Password);
|
||||
|
||||
var user = _contentManager.New("user");
|
||||
user.As<User>().Record = record;
|
||||
_contentManager.Create(user);
|
||||
return user.As<IUser>();
|
||||
|
||||
return _contentManager.Create<User>("user", init => {
|
||||
init.Record.UserName = createUserParams.Username;
|
||||
init.Record.Email = createUserParams.Email;
|
||||
SetPassword(init.Record, createUserParams.Password);
|
||||
});
|
||||
}
|
||||
|
||||
public IUser GetUser(string username) {
|
||||
@@ -47,7 +43,7 @@ namespace Orchard.Users.Services {
|
||||
if (userRecord == null) {
|
||||
return null;
|
||||
}
|
||||
return _contentManager.Get(userRecord.Id).As<IUser>();
|
||||
return _contentManager.Get<IUser>(userRecord.Id);
|
||||
}
|
||||
|
||||
public IUser ValidateUser(string username, string password) {
|
||||
@@ -55,7 +51,7 @@ namespace Orchard.Users.Services {
|
||||
if (userRecord == null || ValidatePassword(userRecord, password) == false)
|
||||
return null;
|
||||
|
||||
return _contentManager.Get(userRecord.Id).As<IUser>();
|
||||
return _contentManager.Get<IUser>(userRecord.Id);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -45,11 +45,11 @@ namespace Orchard.Wikis.Controllers
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Create(PageCreateViewModel model) {
|
||||
var page = _contentManager.New<WikiPage>("wikipage");
|
||||
page.Record.Name = model.Name;
|
||||
page.As<CommonPart>().Container = CurrentSite.ContentItem;
|
||||
_contentManager.Create(page);
|
||||
return RedirectToAction("show", new {page.ContentItem.Id});
|
||||
var page = _contentManager.Create<WikiPage>("wikipage", item => {
|
||||
item.Record.Name = model.Name;
|
||||
item.As<CommonPart>().Container = CurrentSite.ContentItem;
|
||||
});
|
||||
return RedirectToAction("show", new { page.ContentItem.Id });
|
||||
}
|
||||
|
||||
|
||||
|
@@ -2,7 +2,7 @@ using System.Web.Routing;
|
||||
using Orchard.Models;
|
||||
|
||||
namespace Orchard.Wikis.Models {
|
||||
public class WikiPage : ContentPartForRecord<WikiPageRecord>, IContentDisplayInfo {
|
||||
public class WikiPage : ContentPart<WikiPageRecord>, IContentDisplayInfo {
|
||||
|
||||
string IContentDisplayInfo.DisplayText {
|
||||
get { return Record.Name; }
|
||||
|
@@ -9,7 +9,7 @@ namespace Orchard.Wikis.Models {
|
||||
Filters.Add(new ActivatingFilter<CommonPart>("wikipage"));
|
||||
Filters.Add(new ActivatingFilter<RoutablePart>("wikipage"));
|
||||
Filters.Add(new ActivatingFilter<BodyPart>("wikipage"));
|
||||
Filters.Add(new StorageFilterForRecord<WikiPageRecord>(wikiPageRepository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new StorageFilter<WikiPageRecord>(wikiPageRepository) { AutomaticallyCreateMissingRecord = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,8 +14,8 @@ namespace Orchard.Wikis.Models {
|
||||
|
||||
public class WikiSettingsProvider : ContentProvider {
|
||||
public WikiSettingsProvider(IRepository<WikiSettingsRecord> repository) {
|
||||
Filters.Add(new ActivatingFilter<ContentPartForRecord<WikiSettingsRecord>>("site"));
|
||||
Filters.Add(new StorageFilterForRecord<WikiSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new ActivatingFilter<ContentPart<WikiSettingsRecord>>("site"));
|
||||
Filters.Add(new StorageFilter<WikiSettingsRecord>(repository) { AutomaticallyCreateMissingRecord = true });
|
||||
Filters.Add(new TemplateFilterForRecord<WikiSettingsRecord>("WikiSettings"));
|
||||
}
|
||||
}
|
||||
|
@@ -15,39 +15,35 @@ namespace Orchard.Models {
|
||||
return part;
|
||||
}
|
||||
|
||||
public static T Create<T>(this IContentManager manager, string contentType, Action<T> initialize) where T : class, IContent {
|
||||
var content = manager.New<T>(contentType);
|
||||
if (content == null)
|
||||
return null;
|
||||
|
||||
initialize(content);
|
||||
manager.Create(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
public static T Get<T>(this IContentManager manager, int id) where T : class, IContent {
|
||||
var contentItem = manager.Get(id);
|
||||
return contentItem == null ? null : contentItem.Get<T>();
|
||||
}
|
||||
|
||||
public static void Create(this IContentManager manager, IContent part) {
|
||||
manager.Create(part.ContentItem);
|
||||
|
||||
|
||||
public static bool Is<T>(this IContent content) {
|
||||
return content == null ? false : content.ContentItem.Has(typeof(T));
|
||||
}
|
||||
public static T As<T>(this IContent content) where T : class {
|
||||
return content == null ? null : (T)content.ContentItem.Get(typeof(T));
|
||||
}
|
||||
|
||||
public static bool Is<T>(this ContentItem contentItem) {
|
||||
return contentItem == null ? false : contentItem.Has(typeof(T));
|
||||
public static bool Has<T>(this IContent content) {
|
||||
return content == null ? false : content.ContentItem.Has(typeof(T));
|
||||
}
|
||||
public static bool Has<T>(this ContentItem contentItem) {
|
||||
return contentItem == null ? false : contentItem.Has(typeof(T));
|
||||
}
|
||||
public static T As<T>(this ContentItem contentItem) where T : class {
|
||||
return contentItem == null ? null : (T)contentItem.Get(typeof(T));
|
||||
}
|
||||
public static T Get<T>(this ContentItem contentItem) where T : class {
|
||||
return contentItem == null ? null : (T)contentItem.Get(typeof(T));
|
||||
}
|
||||
|
||||
public static bool Is<T>(this IContent part) {
|
||||
return part == null ? false : part.ContentItem.Has(typeof(T));
|
||||
}
|
||||
public static bool Has<T>(this IContent part) {
|
||||
return part == null ? false : part.ContentItem.Has(typeof(T));
|
||||
}
|
||||
public static T As<T>(this IContent part) where T : class {
|
||||
return part == null ? null : (T)part.ContentItem.Get(typeof(T));
|
||||
}
|
||||
public static T Get<T>(this IContent part) where T : class {
|
||||
return part == null ? null : (T)part.ContentItem.Get(typeof(T));
|
||||
public static T Get<T>(this IContent content) where T : class {
|
||||
return content == null ? null : (T)content.ContentItem.Get(typeof(T));
|
||||
}
|
||||
|
||||
|
||||
|
@@ -2,4 +2,9 @@ namespace Orchard.Models {
|
||||
public abstract class ContentPart : IContent {
|
||||
public ContentItem ContentItem { get; set; }
|
||||
}
|
||||
|
||||
public class ContentPart<TRecord> : ContentPart {
|
||||
public TRecord Record { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
namespace Orchard.Models {
|
||||
public class ContentPartForRecord<TRecord> : ContentPart {
|
||||
public TRecord Record { get; set; }
|
||||
}
|
||||
}
|
@@ -2,25 +2,25 @@ using Orchard.Data;
|
||||
using Orchard.Models.Records;
|
||||
|
||||
namespace Orchard.Models.Driver {
|
||||
public class StorageFilterForRecord<TRecord> : StorageFilterBase<ContentPartForRecord<TRecord>> where TRecord : ContentPartRecord,new() {
|
||||
public class StorageFilter<TRecord> : StorageFilterBase<ContentPart<TRecord>> where TRecord : ContentPartRecord,new() {
|
||||
private readonly IRepository<TRecord> _repository;
|
||||
|
||||
public StorageFilterForRecord(IRepository<TRecord> repository) {
|
||||
public StorageFilter(IRepository<TRecord> repository) {
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public bool AutomaticallyCreateMissingRecord { get; set; }
|
||||
|
||||
protected override void Activated(ActivatedContentContext context, ContentPartForRecord<TRecord> instance) {
|
||||
protected override void Activated(ActivatedContentContext context, ContentPart<TRecord> instance) {
|
||||
instance.Record = new TRecord();
|
||||
}
|
||||
|
||||
protected override void Creating(CreateContentContext context, ContentPartForRecord<TRecord> instance) {
|
||||
protected override void Creating(CreateContentContext context, ContentPart<TRecord> instance) {
|
||||
instance.Record.ContentItem = context.ContentItemRecord;
|
||||
_repository.Create(instance.Record);
|
||||
}
|
||||
|
||||
protected override void Loading(LoadContentContext context, ContentPartForRecord<TRecord> instance) {
|
||||
protected override void Loading(LoadContentContext context, ContentPart<TRecord> instance) {
|
||||
instance.Record = _repository.Get(instance.ContentItem.Id);
|
||||
if (instance.Record == null && AutomaticallyCreateMissingRecord) {
|
||||
instance.Record = new TRecord {ContentItem = context.ContentItemRecord};
|
@@ -1,23 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Orchard.Models.Records;
|
||||
using Orchard.Models.Records;
|
||||
using Orchard.UI.Models;
|
||||
|
||||
namespace Orchard.Models.Driver {
|
||||
public class TemplateFilterForRecord<TRecord> : TemplateFilterBase<ContentPartForRecord<TRecord>> where TRecord : ContentPartRecord, new() {
|
||||
public class TemplateFilterForRecord<TRecord> : TemplateFilterBase<ContentPart<TRecord>> where TRecord : ContentPartRecord, new() {
|
||||
private readonly string _prefix;
|
||||
|
||||
public TemplateFilterForRecord(string prefix) {
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
protected override void GetEditors(GetEditorsContext context, ContentPartForRecord<TRecord> part) {
|
||||
protected override void GetEditors(GetEditorsContext context, ContentPart<TRecord> part) {
|
||||
context.Editors.Add(ModelTemplate.For(part.Record, _prefix));
|
||||
}
|
||||
|
||||
protected override void UpdateEditors(UpdateContentContext context, ContentPartForRecord<TRecord> part) {
|
||||
protected override void UpdateEditors(UpdateContentContext context, ContentPart<TRecord> part) {
|
||||
context.Updater.TryUpdateModel(part.Record, _prefix, null, null);
|
||||
context.Editors.Add(ModelTemplate.For(part.Record, _prefix));
|
||||
}
|
||||
|
@@ -148,7 +148,7 @@
|
||||
<Compile Include="Models\Driver\ContentProvider.cs" />
|
||||
<Compile Include="Models\Driver\ActivatingContentContext.cs" />
|
||||
<Compile Include="Models\Driver\GetEditorsContext.cs" />
|
||||
<Compile Include="Models\Driver\StorageFilterForRecord.cs" />
|
||||
<Compile Include="Models\Driver\StorageFilter.cs" />
|
||||
<Compile Include="Models\Driver\StorageFilterBase.cs" />
|
||||
<Compile Include="Models\Driver\TemplateFilterBase.cs" />
|
||||
<Compile Include="Models\Driver\TemplateFilterForRecord.cs" />
|
||||
@@ -158,7 +158,6 @@
|
||||
<Compile Include="Models\ContentExtensions.cs" />
|
||||
<Compile Include="Models\ContentPart.cs" />
|
||||
<Compile Include="Models\IContent.cs" />
|
||||
<Compile Include="Models\ContentPartForRecord.cs" />
|
||||
<Compile Include="Models\Records\ContentPartRecord.cs" />
|
||||
<Compile Include="Models\Records\ContentTypeRecord.cs" />
|
||||
<Compile Include="Models\Records\ContentItemRecord.cs" />
|
||||
|
Reference in New Issue
Block a user