serenity-is / Serenity

Business Apps Made Simple with Asp.Net Core MVC / TypeScript
https://serenity.is
MIT License
2.6k stars 802 forks source link

Record Not Found alert #386

Closed c9482 closed 8 years ago

c9482 commented 8 years ago

Probably something simple as usual... I am getting this alert when trying to update a row while logged in as admin (so should have all permissions). I have similar entity that works just fine. Many thanks in advance for any hints or suggestions.

namespace LCBudgets.LCBudgets.Entities { using Modules.LCBudgets.Department; using Serenity.ComponentModel; using Serenity.Data; using Serenity.Data.Mapping; using System; using System.ComponentModel;

[ConnectionKey("Default"), DisplayName("Department"), InstanceName("department"), TwoLevelCached]
[ReadPermission(Administration.PermissionKeys.Department.View)]
[ModifyPermission(Administration.PermissionKeys.Department.Modify)]
[InsertPermission(Administration.PermissionKeys.Department.Insert)]
[DeletePermission(Administration.PermissionKeys.Department.Delete)]
[LookupScript("LCBudgets.Department")]
public sealed class DepartmentRow : Row, IIdRow, INameRow, IMultiDepartmentRow
{
    [DisplayName("Nbr"), Column("dept_nbr"), Size(32), PrimaryKey, QuickSearch, Updatable(false)]
    public String Nbr
    {
        get { return Fields.Nbr[this]; }
        set { Fields.Nbr[this] = value; }
    }

    [DisplayName("Name"), Column("dept_name"), Size(80), NotNull, QuickSearch]
    public String Name
    {
        get { return Fields.Name[this]; }
        set { Fields.Name[this] = value; }
    }

    [DisplayName("Default Appropriation Increase"), Column("default_appropriation_increase"), Size(4), NotNull, Hint("Enter % value")]
    public Double? DefaultAppropriationIncrease
    {
        get { return Fields.DefaultAppropriationIncrease[this]; }
        set { Fields.DefaultAppropriationIncrease[this] = value; }
    }

    [DisplayName("Default Payroll Increase"), Column("default_payroll_increase"), Size(4), NotNull, Hint("Enter % value")]
    public Double? DefaultPayrollIncrease
    {
        get { return Fields.DefaultPayrollIncrease[this]; }
        set { Fields.DefaultPayrollIncrease[this] = value; }
    }

    [DisplayName("Full-Time Employees"), Expression("(SELECT count(*) FROM [position_budget] pb WHERE pb.dept_nbr = T0.dept_nbr and pb.position_fulltime = 1)")]
    public Int32? FullTimeEmployees
    {
        get { return Fields.FullTimeEmployees[this]; }
        set { Fields.FullTimeEmployees[this] = value; }

    }

    [DisplayName("Part-Time Employees"), Expression("(SELECT count(*) FROM [position_budget] pb WHERE pb.dept_nbr = T0.dept_nbr and pb.position_fulltime = 0)")]
    public Int32? PartTimeEmployees
    {
        get { return Fields.PartTimeEmployees[this]; }
        set { Fields.PartTimeEmployees[this] = value; }

    }

    IIdField IIdRow.IdField
    {
        get { return Fields.Nbr; }
    }

    StringField INameRow.NameField
    {
        get { return Fields.Name; }
    }

    public StringField DeptNbrField
    {
        get
        {
            return Fields.Nbr;
        }
    }

    public static readonly RowFields Fields = new RowFields().Init();

    public DepartmentRow()
        : base(Fields)
    {
    }

    public class RowFields : RowFieldsBase
    {
        public StringField Nbr;
        public StringField Name;
        public DoubleField DefaultAppropriationIncrease;
        public DoubleField DefaultPayrollIncrease;
        public Int32Field FullTimeEmployees;
        public Int32Field PartTimeEmployees;

        public RowFields()
            : base("[dbo].[department]", "dept_")
        {
            LocalTextPrefix = "LCBudgets.Department";
        }
    }
}

}

2016-03-10_141710 2016-03-10_142116

volkanceylan commented 8 years ago

What is IdProperty on dialog is set to?

c9482 commented 8 years ago

namespace LCBudgets.LCBudgets { using Serenity;

[IdProperty(DepartmentRow.IdProperty), NameProperty(DepartmentRow.NameProperty)]
[FormKey("LCBudgets.Department"), LocalTextPrefix(DepartmentRow.LocalTextPrefix), Service(DepartmentService.BaseUrl)]
public class DepartmentDialog : EntityDialog<DepartmentRow>
{
    protected DepartmentForm form;

    public DepartmentDialog()
    {
        form = new DepartmentForm(this.IdPrefix);
    }
}

}

volkanceylan commented 8 years ago

My guess is you changed Id property name from DeptNbr to Nbr and didn't transform T4 so, DepartmentRow.IdProperty is still DeptNbr

c9482 commented 8 years ago

I transformed T4 and rebuilt the solution but still same problem:

namespace LCBudgets.LCBudgets { using Serenity; using Serenity.ComponentModel; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices;

public partial class DepartmentForm : PrefixedContext
{
    [InlineConstant] public const string FormKey = "LCBudgets.Department";

    public DepartmentForm(string idPrefix) : base(idPrefix) {}

    public StringEditor Nbr { get { return ById<StringEditor>("Nbr"); } }
    public StringEditor Name { get { return ById<StringEditor>("Name"); } }
    public DecimalEditor DefaultAppropriationIncrease { get { return ById<DecimalEditor>("DefaultAppropriationIncrease"); } }
    public DecimalEditor DefaultPayrollIncrease { get { return ById<DecimalEditor>("DefaultPayrollIncrease"); } }
}

}

volkanceylan commented 8 years ago

Show DepartmentRow

volkanceylan commented 8 years ago

Script side

volkanceylan commented 8 years ago

Ok, my last guess is your IMultiDepartmentRow and the way you handle multi tenancy.

volkanceylan commented 8 years ago

This row probably shouldn't be IMultiDepartmentRow etc.

c9482 commented 8 years ago

namespace LCBudgets.LCBudgets { using Serenity; using Serenity.ComponentModel; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices;

[Imported, Serializable, PreserveMemberCase]
public partial class DepartmentRow
{
    [InlineConstant] public const string IdProperty = "Nbr";
    [InlineConstant] public const string NameProperty = "Name";
    [InlineConstant] public const string LocalTextPrefix = "LCBudgets.Department";
    [InlineConstant] public const string LookupKey = "LCBudgets.Department";

    public static Lookup<DepartmentRow> Lookup { [InlineCode("Q.getLookup('LCBudgets.Department')")] get { return null; } }

    public String Nbr { get; set; }
    public String Name { get; set; }
    public Double? DefaultAppropriationIncrease { get; set; }
    public Double? DefaultPayrollIncrease { get; set; }
    public Int32? FullTimeEmployees { get; set; }
    public Int32? PartTimeEmployees { get; set; }

    [Imported, PreserveMemberCase]
    public static class Fields
    {
        [InlineConstant] public const string Nbr = "Nbr";
        [InlineConstant] public const string Name = "Name";
        [InlineConstant] public const string DefaultAppropriationIncrease = "DefaultAppropriationIncrease";
        [InlineConstant] public const string DefaultPayrollIncrease = "DefaultPayrollIncrease";
        [InlineConstant] public const string FullTimeEmployees = "FullTimeEmployees";
        [InlineConstant] public const string PartTimeEmployees = "PartTimeEmployees";
    }
}

}

volkanceylan commented 8 years ago

Remove IMultiDepartmentRow from this one. Your tenant table shouldn't have this interface.

c9482 commented 8 years ago

Department has to be IMultiDepartmentRow because I need user to be able to edit their own department only.

I just noticed that another entity has the same problem. Common theme is that a prefix was used in the ones not working. Here is another one that is NOT working that had the key name shortened to "Nbr". Notice that FundRow does not implement IMultiDepartmentRow .

namespace LCBudgets.LCBudgets.Entities { using Serenity.ComponentModel; using Serenity.Data; using Serenity.Data.Mapping; using System; using System.ComponentModel;

[ConnectionKey("Default"), DisplayName("Fund"), InstanceName("fund"), TwoLevelCached]
[ReadPermission(Administration.PermissionKeys.Fund.View)]
[ModifyPermission(Administration.PermissionKeys.Fund.Modify)]
[InsertPermission(Administration.PermissionKeys.Fund.Insert)]
[DeletePermission(Administration.PermissionKeys.Fund.Delete)]
[LookupScript("LCBudgets.Fund")]
public sealed class FundRow : Row, IIdRow, INameRow
{
    [DisplayName("Nbr"), Column("fund_nbr"), Size(32), PrimaryKey, QuickSearch, Updatable(false)]
    public String Nbr
    {
        get { return Fields.Nbr[this]; }
        set { Fields.Nbr[this] = value; }
    }

    [DisplayName("Name"), Column("fund_name"), Size(255), NotNull, QuickSearch]
    public String Name
    {
        get { return Fields.Name[this]; }
        set { Fields.Name[this] = value; }
    }

    IIdField IIdRow.IdField
    {
        get { return Fields.Nbr; }
    }

    StringField INameRow.NameField
    {
        get { return Fields.Name; }
    }

    public static readonly RowFields Fields = new RowFields().Init();

    public FundRow()
        : base(Fields)
    {
    }

    public class RowFields : RowFieldsBase
    {
        public StringField Nbr;
        public StringField Name;

        public RowFields()
            : base("[dbo].[fund]", "fund_")
        {
            LocalTextPrefix = "LCBudgets.Fund";
        }
    }
}

}

volkanceylan commented 8 years ago

It shouldn't have any relation. Try removing this ID field from form temporarily. I'm out of ideas here.

c9482 commented 8 years ago

Took the ID field from the form, rebuilt, transformed, rebuilt - same problem. All I see different is is:

        public RowFields()
            : base("[dbo].[account_category]") -- > works

       public RowFields()
            : base("[dbo].[department]", "dept_") --> not working

        public RowFields()
            : base("[dbo].[fund]", "fund_") --> not working

Here is an entity that works:

namespace LCBudgets.LCBudgets.Entities { using Serenity.ComponentModel; using Serenity.Data; using Serenity.Data.Mapping; using System; using System.ComponentModel;

[ConnectionKey("Default"), DisplayName("Account Category"), InstanceName("account category"), TwoLevelCached]
[ReadPermission(Administration.PermissionKeys.AccountCategory.View)]
[ModifyPermission(Administration.PermissionKeys.AccountCategory.Modify)]
[InsertPermission(Administration.PermissionKeys.AccountCategory.Insert)]
[DeletePermission(Administration.PermissionKeys.AccountCategory.Delete)]
[LookupScript("LCBudgets.AccountCategory")]
public sealed class AccountCategoryRow : Row, IIdRow, INameRow
{
    [DisplayName("Acct Category"), Column("acct_category"), Size(32), PrimaryKey, QuickSearch, Updatable(false)]
    public String AcctCategory
    {
        get { return Fields.AcctCategory[this]; }
        set { Fields.AcctCategory[this] = value; }
    }

    [DisplayName("Description"), Column("description"), Size(255), NotNull, QuickSearch]
    public String Description
    {
        get { return Fields.Description[this]; }
        set { Fields.Description[this] = value; }
    }

    IIdField IIdRow.IdField
    {
        get { return Fields.AcctCategory; }
    }

    StringField INameRow.NameField
    {
        get { return Fields.Description; }
    }

    public static readonly RowFields Fields = new RowFields().Init();

    public AccountCategoryRow()
        : base(Fields)
    {
    }

    public class RowFields : RowFieldsBase
    {
        public StringField AcctCategory;
        public StringField Description;

        public RowFields()
            : base("[dbo].[account_category]")
        {
            LocalTextPrefix = "LCBudgets.AccountCategory";
        }
    }
}

}

volkanceylan commented 8 years ago

Remove that dept_ etc from constructor, you dont need them.

c9482 commented 8 years ago

Already tried it - it did not help.

volkanceylan commented 8 years ago

Sorry will try to reproduce tomorrow.

c9482 commented 8 years ago

No problem. Thanks for all the effort.

volkanceylan commented 8 years ago

I can't reproduce this. It is probably related to some behavior. Need stack trace. Put compilation debug=true in web.config.

Also <add key="Logging" value="{ File: '~\\App_Data\\Log\\App_{0}_{1}.log', FlushTimeout: 0, Level: 'Debug' }" />

so you'll get a log file containing queries under App_Data\Log folder.

Make sure you update Serenity to latest. Old Serenity versions had problems with string IDs.

c9482 commented 8 years ago

I updated to the latest (was 2 versions behind) - still no joy. Here's the log:

[D] 2016-03-11T10:28:27.371 Dapper.QueryInternal DECLARE name NVARCHAR(4000) = 'budgets';

SELECT * FROM sys.databases WHERE NAME = @name

[D] 2016-03-11T10:28:27.575 Dapper.QueryInternal DECLARE name NVARCHAR(4000) = 'lc911_Northwind';

SELECT * FROM sys.databases WHERE NAME = @name

[D] 2016-03-11T10:28:27.938 ExecuteReader

SELECT TOP 100 T0.[fund_nbr] AS [Nbr], T0.[fund_name] AS [Name] FROM [dbo].[fund] T0 ORDER BY T0.[fund_nbr]; SELECT count(*)
FROM [dbo].[fund] T0

[D] 2016-03-11T10:28:27.940 END - ExecuteReader [D] 2016-03-11T10:28:32.164 ExecuteReader DECLARE @p1 NVARCHAR(4000) = 'admin';

SELECT T0.[UserId] AS [UserId], T0.[Username] AS [Username], T0.[Source] AS [Source], T0.[PasswordHash] AS [PasswordHash], T0.[PasswordSalt] AS [PasswordSalt], T0.[DisplayName] AS [DisplayName], T0.[Email] AS [Email], T0.[LastDirectoryUpdate] AS [LastDirectoryUpdate], T0.[IsActive] AS [IsActive], T0.[DeptNbr] AS [DeptNbr], T0.[InsertUserId] AS [InsertUserId], T0.[InsertDate] AS [InsertDate], T0.[UpdateUserId] AS [UpdateUserId], T0.[UpdateDate] AS [UpdateDate] FROM [Users] T0 WHERE (T0.[Username] = @p1)

[D] 2016-03-11T10:28:32.167 END - ExecuteReader [D] 2016-03-11T10:28:35.799 ExecuteReader

SELECT TOP 100 T0.[fund_nbr] AS [Nbr], T0.[fund_name] AS [Name] FROM [dbo].[fund] T0 ORDER BY T0.[fund_nbr]; SELECT count(*)
FROM [dbo].[fund] T0

[D] 2016-03-11T10:28:36.061 END - ExecuteReader [D] 2016-03-11T10:28:36.504 ExecuteReader

SELECT T0.[Id] AS [Id], T0.[LanguageName] AS [LanguageName], T0.[LanguageId] AS [LanguageId] FROM [Languages] T0 ORDER BY T0.[LanguageName]

[D] 2016-03-11T10:28:36.504 END - ExecuteReader [D] 2016-03-11T10:28:41.460 ExecuteReader DECLARE @p1 NVARCHAR(4000) = '001';

SELECT T0.[fund_nbr] AS [Nbr], T0.[fund_name] AS [Name] FROM [dbo].[fund] T0 WHERE (T0.[fund_nbr] = @p1)

[D] 2016-03-11T10:28:41.464 END - ExecuteReader [D] 2016-03-11T10:28:43.323 ExecuteReader DECLARE @p1 NVARCHAR(4000) = '1';

SELECT T0.[fund_nbr] AS [Nbr], T0.[fund_name] AS [Name] FROM [dbo].[fund] T0 WHERE (T0.[fund_nbr] = @p1)

[D] 2016-03-11T10:28:43.327 END - ExecuteReader

volkanceylan commented 8 years ago

What is your fund number you were editing, why there are a 001 and 1 fund numbers

volkanceylan commented 8 years ago

DECLARE @p1 NVARCHAR(4000) = '001'; DECLARE @p1 NVARCHAR(4000) = '1';

volkanceylan commented 8 years ago

Hmm gotcha, your ID values starts with 0, like 0100, serenity tries to convert them to integer client side, so it gets 100. Related Q.toId function.

c9482 commented 8 years ago

Yes, it appears you are correct - update fails on any records with leading zero ids. It was some lazy programming on my side not to create true identity id. I need to be able to re-import these tables from spreadsheets so identity key would not work. Is there a way around it or will I have to declare an identity key and write custom import code?

volkanceylan commented 8 years ago

It will be resolved with next version, with some breaking changes. Before Serenity only supported integer IDs and code was written for this. Then after some requests i added this feature but it still has such conversion code in many places. You may wait for next version.

As a side note, you would be able to add an identity column to table and use it as ID for only Serenity row, you may leave your Nbr fields as primary key. This wouldn't hurt anything.

c9482 commented 8 years ago

Oh didn't know I could do that... I may use it as workaround. Thanks again for all your help.

c9482 commented 8 years ago

I declared an Identity column and changed the DepartmenRow class as follows and that cleared the record not found alert:

public sealed class DepartmentRow : Row, IIdRow, INameRow, IMultiDepartmentRow
{
    [DisplayName("Id"), Column("dept_id"), Size(32), Identity, Updatable(false)]
    public Int32? DeptId
    {
        get { return Fields.DeptId[this]; }
        set { Fields.DeptId[this] = value; }
    }

    [DisplayName("Nbr"), Column("dept_nbr"), Size(32), PrimaryKey, QuickSearch, Updatable(false)]
    public String Nbr
    {
        get { return Fields.Nbr[this]; }
        set { Fields.Nbr[this] = value; }
    }

    IIdField IIdRow.IdField
    {
        get { return Fields.DeptId; }
    }

    StringField INameRow.NameField
    {
        get { return Fields.Name; }
    }

    public StringField DeptNbrField
    {
        get
        {
            return Fields.Nbr;
        }
    }

But how do I declare [LookupEditor(typeof(DepartmentRow))] which uses Nbr instead of DeptId as the key?

c9482 commented 8 years ago

Created custom editor and declared LookupInclude for the Nbr field in DepartmentRow. Still the lookup key being used is DeptId rather than Nbr. I think I am close but still missing something...

    [DisplayName("Nbr"), Column("dept_nbr"), Size(32), PrimaryKey, QuickSearch, Updatable(false), LookupInclude]
    public String Nbr
    {
        get { return Fields.Nbr[this]; }
        set { Fields.Nbr[this] = value; }
    }

public class DepartmentEditor : LookupEditorBase<DepartmentRow>
{
    public DepartmentEditor(jQueryObject container, LookupEditorOptions options)
        : base(container, options)
    {
    }

    protected override string GetLookupKey()
    {
        return DepartmentRow.LookupKey;
    }

    protected override string GetItemText(DepartmentRow item, Lookup<DepartmentRow> lookup)
    {
        return base.GetItemText(item, lookup) + " [" + item.Nbr + "]";
    }
}
volkanceylan commented 8 years ago

You need to declare a separate lookup server side like in multi tenancy guide and set IdProperty there. But anyway your problem should be resolved in latest version so you dont need that extra id.

c9482 commented 8 years ago

Yes, the text based key works with the latest update. I wanted to make it work with custom lookup editor just so I know how to do it. I followed the example from Northwind's CustomerEditor because it was exactly the same scenario. It didn't work for me - DeptId was still returned instead of Nbr. Is this coded properly? Where do I tell the editor to return DeptId instead of Nbr?

public class DepartmentEditor : LookupEditorBase<DepartmentRow>
{
    public DepartmentEditor(jQueryObject container, LookupEditorOptions options)
        : base(container, options)
    {
    }

    protected override string GetLookupKey()
    {
        return DepartmentRow.LookupKey;
    }

    protected override string GetItemText(DepartmentRow item, Lookup<DepartmentRow> lookup)
    {
        return base.GetItemText(item, lookup) + " [" + item.Nbr + "]";
    }
}
volkanceylan commented 8 years ago

You don't tell editor there. It's defined in Lookup server side:

    [LookupScript("Northwind.Customer")]
    public class CustomerLookup : RowLookupScript<CustomerRow>
    {
        public CustomerLookup()
        {
            IdField = CustomerRow.Fields.CustomerID.PropertyName;
            TextField = CustomerRow.Fields.CompanyName.PropertyName;
        }
    }
volkanceylan commented 8 years ago

Please reopen if didn't resolve your issue.