#1175: Enhance dCursorMixin to support (1) editable and (2) compound primary 
keys
--------------------------+-------------------------------------------------
  Reporter:  aecker       |       Owner:  somebody                     
      Type:  enhancement  |      Status:  closed                       
  Priority:  major        |   Milestone:  0.8.3                        
 Component:  db           |     Version:  0.8.4                        
Resolution:  invalid      |    Keywords:  editable multiple compound pk
--------------------------+-------------------------------------------------
Old description:

> First I have to say "'THANK YOU!! ' * 1000"... for to publish dabo to the
> community. Two weeks ago I found dabo via google because I had to make a
> little prototype for a project offer. Luckily I could spent some time to
> discover the dabo universe and I am so excited that I want help you where
> I can to maintain and enhance this fantastic framework.
>
> The prototype I had to make is for a management office where some hobby
> programmers / database administrators need to merge statistics from a
> group of companies. In the moment they enter and report on the data
> directly within the database admin/management tool. This is the main
> reason why they want to have meaningful and editable primary key values
> instead of (autoincrement) numbers, on some of the tables with compound
> PKs. When they buy it, I could publish all code to the community as an
> example dabo application after the project is finished.
>
> After I first created the database structure, run the dabo AppWizard and
> then started the generated applicaten I was really impressed about the
> functionality already built into dabo. All test data was showing fine,
> but when I then started to edit or add new records some strange errors
> occured. So I dived deeper...
>
> First I got the current revision 4664 out of SVN, then tried to fix the
> problems in the classes of my prototype application but it got ugly and I
> had to use a lot of private attributes of dCursorMixin (like
> cursor._mementos/._newRecords/._compoundKey). So finally I patched
> dCursorMixin and the code got more easy to maintain and understand.
> Please feel free to use this code into the code base and also to change
> and critize to fit dabo code conventions. Although I love python I can
> use it only rarely in my job (as Omnis Studio and .NET developer,..). so
> please bare with my python coding.
>

> == PATCHES FOR TO SUPPORT EDITABLE MULTIPLE PRIMARY KEY FIELDS ==
>
> A) Changed in dCursorMixin.setFieldValue() the "if fld == keyField:" code
> block (all until the respective "else:" code block) into this code:
>

> {{{
>                                 if fld == keyField or fld in keyField:
>                                         # Changing the key field(s)
> value, need to key the mementos on the new
>                                         # value, not the old.
> Additionally, need to copy the mementos from the
>                                         # old key value to the new one.
>                                         if self._compoundKey:
>                                                 old_keyFieldValue =
> tuple([rec[k] for k in keyField])
>                                                 keyFieldValue =
> tuple([val if k == fld else rec[k] for k in keyField])
>
> }}}
>                                         else:
>
> {{{
>                                                 old_keyFieldValue =
> old_val
>                                                 keyFieldValue = val
>                                         # new records pk must be unique
> (at least within current dataset, filtered
>                                         # datasets will throw db error if
> not unique)
>                                         if
> (self._newRecords.has_key(old_keyFieldValue) \
>                                                         and
> self._getRowByPk(keyFieldValue) >= 0) \
>                                         or
> self._newRecords.has_key(keyFieldValue):
>                                                 #raise
> dException.MissingPKException, _("The code/id/key value you entered
> already exists in the data set. val=") + str(val)
>                                                 return          # best
> simply RETURN silently without changing cursor, biz.validateField()
> informs user (in most cases)
>
>                                         old_mem =
> self._mementos.get(old_keyFieldValue, None)
>                                         if old_mem is not None:
> self._mementos[keyFieldValue] = old_mem
>                                                 del
> self._mementos[old_keyFieldValue]
>                                         if
> self._newRecords.has_key(old_keyFieldValue):
> self._newRecords[keyFieldValue] = None
>                                                 del
> self._newRecords[old_keyFieldValue]
>

> }}}
>
> B) Inserted into dCursorMixin.getFieldVal() after the code line:
>

> {{{
>                 rec = self._records[row]
>
> }}}
>
>         this code block to support mult. field pk:
>

> {{{
>                 if isinstance(fld, basestring) and fld.find(",") >= 0:
>                         # convert also if multi PK KeyField value passed
> from bizObj layer
>                         fld = [f.strip() for f in fld.split(",")]
>
> }}}
>

> C) To warn user on duplicate pk I also had to add to Base.py bizobj class
> (maybe patch/include into dBizObj, what do you think? ... and what would
> be the right method/exception to notify the user from the framework
> level):
>
> {{{
>    def validateField(self, fld, val):
>     # If the new value is different from the current value, change it and
> also
>     # update the mementos if necessary.
>
>     row = self._CurrentCursor.RowNumber                         # MOVE TO
> dBizObj OR REPLACE PRIVATE ATTRIBUTES
>     rec = self._CurrentCursor._records[row]   # ..rec = self.Record?!?!?
>     keyField = self._CurrentCursor.KeyField             # ..used cursor
> prop. because biz.KeyField is comma sep str?!?!?
>     old_val = rec[fld]
> # ..old_val = rec.getAttr(fld)
>     if old_val != val:
>       if fld == keyField or fld in keyField:
>         # Changing the key field(s) value, need to key the mementos on
> the new
>         # value, not the old. Additionally, need to copy the mementos
> from the
>         # old key value to the new one.
>         if self._CurrentCursor._compoundKey:    # ..if
> isinstance(keyField, tuple):
>           old_keyFieldValue = tuple([rec[k] for k in keyField])
>           keyFieldValue = tuple([val if k == fld else rec[k] for k in
> keyField])
>         else:
>           old_keyFieldValue = old_val
>           keyFieldValue = val
>         # new records pk must be unique (at least within current dataset)
>         if self._CurrentCursor._newRecords.has_key(old_keyFieldValue) \
>             and self._CurrentCursor._getRowByPk(keyFieldValue) >= 0:
>           return "Code/Id/PrimaryKey already exists in the dataset"
>         elif self._CurrentCursor._newRecords.has_key(keyFieldValue):
>           return "Duplicate Code/Id/PrimaryKey in another, new and
> unsaved record. Please enter unique value or delete the other new/unsaved
> record."
>     return ""
> }}}
>
>         With these patches the framework would support editable and
> compound primary keys. I first want to discuss this with you before I
> spend more time working this out, which could include also the following
> features:
>
> o add opt. fld+val arguments to dCursorMixin.pkExpression(self, rec=None,
> fld=None, val=None) and virtually overwrite the return value (or part of
> it) with passed val - to get a preview of the pk if val would be written
> to the database column specified by fld.
>
> o use extended method getPkExpression() in dCursorMixin.setFieldVal() and
> in Base.py (or dBizObj.fieldValidation() after code transfer from
> Base.py/validateField()) to minimize redundant code.
>
> o While investigation more on compound primary keys I found other places
> in dCursorMixin where compound primary keys are not supported. Not sure
> if the following extension of RemoteConnector.save() to support multiple
> field pk will not break the remote protocol.
>
> The extensoion could be done by:
>
> (1) in dCursorMixin.getRecordStatus()/_getNewRecordDiff() replace:
> {{{
>         recs = [r for r in self._records if r[self._keyField] == pk]
> }}}
> with:
> {{{
>         recs = [r for r in self._records if self.pkExpression(r) == pk]
> }}}
>
> (2) replace in dCursorMixin.getDataDiff().rowDiff():
>
> {{{
>     ret[self._keyField] = pk
> }}}
> with:
> {{{
>                 if self._compoundKey:
>                         rec = [r for r in self._records if
> self.pkExpression(r) == pk][0]
>                         for fld in self._keyField:
>                                 ret[fld] = rec[fld]
>                 else:
>                         ret[self._keyField] = pk
> }}}
>
> (3) in dCursorMixin.setDefaults() first remove the following local
> variables at the top:
>
> {{{
>                 keyField = self.KeyField
>                 keyFieldSet = False
> }}}
>
> then replace the last code part of the method:
>
> {{{
>                 if self._nullDefaults:
>                         for field in rec.keys():
>                                 if field == keyField:
>                                         continue
>                                 self.setFieldVal(field, None)
>                 else:
>                         if keyField in vals.keys():
>                                 # Must set the pk default value first,
> for mementos to be filled in
>                                 # correctly.
>                                 setDefault(keyField, vals[keyField])
>                                 keyFieldSet = True
>
>                         for field, val in vals.items():
>                                 if field == keyField and keyFieldSet:
>                                         continue
>                                 setDefault(field, val)
> }}}
>
> with something like:
>
> {{{
>                 if self._compoundKey:
>                         keyFields = [fld for fld in self.KeyField]
>                 else:
>                         keyFields = [self.KeyField]
>                 if self._nullDefaults:
>                         for field in rec.keys():
>                                 if field not in keyFields:
>                                         self.setFieldVal(field, None)
>                 else:
>                         # Must set the pk default value first, for
> mementos to be filled in
>                         # correctly.
>                         for field in keyFields:
>                                 if field in vals:
>                                         setDefault(field, vals[field])
>                         for field, val in vals.items():
>                                 if field not in keyFields:
>                                         setDefault(field, val)
> }}}
>
> (4) not sure if fld[2] in dCursorMixin._setTable() contains True for a pk
> field in all DBs, if yes then we could replace:
>
> {{{
>                                 self._keyField = [fld[0] for fld in
> self.getFields(table)
>                                                 if fld[2] ][0]
> }}}
>
> with:
> {{{
>                                 kf = tuple([fld[0] for fld in
> self.getFields(table)
>                                                 if fld[2] ])
>                                 if len(kf) == 1:
>                                   kf = kf[0]
>                                 self._keyField = kf
> }}}
>
> Since long time I want to participate/support an open source project and
> dabo is exactly what I am looking for. Unfortunately I have still no
> internet access from my home ... far out, only from working place ... but
> my best boss of the world allows us to stay late and use our equipment
> privatly. I also will motivate my collueges as good as I can to use  d a
> b o  .

New description:

 First I have to say "'THANK YOU!! ' * 1000"... for to publish dabo to the
 community. Two weeks ago I found dabo via google because I had to make a
 little prototype for a project offer. Luckily I could spent some time to
 discover the dabo universe and I am so excited that I want help you where
 I can to maintain and enhance this fantastic framework.

 The prototype I had to make is for a management office where some hobby
 programmers / database administrators need to merge statistics from a
 group of companies. In the moment they enter and report on the data
 directly within the database admin/management tool. This is the main
 reason why they want to have meaningful and editable primary key values
 instead of (autoincrement) numbers, on some of the tables with compound
 PKs. When they buy it, I could publish all code to the community as an
 example dabo application after the project is finished.

 After I first created the database structure, run the dabo AppWizard and
 then started the generated applicaten I was really impressed about the
 functionality already built into dabo. All test data was showing fine, but
 when I then started to edit or add new records some strange errors
 occured. So I dived deeper...

 First I got the current revision 4664 out of SVN, then tried to fix the
 problems in the classes of my prototype application but it got ugly and I
 had to use a lot of private attributes of dCursorMixin (like
 cursor._mementos/._newRecords/._compoundKey). So finally I patched
 dCursorMixin and the code got more easy to maintain and understand. Please
 feel free to use this code into the code base and also to change and
 critize to fit dabo code conventions. Although I love python I can use it
 only rarely in my job (as Omnis Studio and .NET developer,..). so please
 bare with my python coding.


 == PATCHES FOR TO SUPPORT EDITABLE MULTIPLE PRIMARY KEY FIELDS ==

 A) Changed in dCursorMixin.setFieldValue() the "if fld == keyField:" code
 block (all until the respective "else:" code block) into this code:


 {{{
                                 if fld == keyField or fld in keyField:
                                         # Changing the key field(s) value,
 need to key the mementos on the new
                                         # value, not the old.
 Additionally, need to copy the mementos from the
                                         # old key value to the new one.
                                         if self._compoundKey:
                                                 old_keyFieldValue =
 tuple([rec[k] for k in keyField])
                                                 keyFieldValue = tuple([val
 if k == fld else rec[k] for k in keyField])

 }}}
                                         else:

 {{{
                                                 old_keyFieldValue =
 old_val
                                                 keyFieldValue = val
                                         # new records pk must be unique
 (at least within current dataset, filtered
                                         # datasets will throw db error if
 not unique)
                                         if
 (self._newRecords.has_key(old_keyFieldValue) \
                                                         and
 self._getRowByPk(keyFieldValue) >= 0) \
                                         or
 self._newRecords.has_key(keyFieldValue):
                                                 #raise
 dException.MissingPKException, _("The code/id/key value you entered
 already exists in the data set. val=") + str(val)
                                                 return          # best
 simply RETURN silently without changing cursor, biz.validateField()
 informs user (in most cases)

                                         old_mem =
 self._mementos.get(old_keyFieldValue, None)
                                         if old_mem is not None:
 self._mementos[keyFieldValue] = old_mem
                                                 del
 self._mementos[old_keyFieldValue]
                                         if
 self._newRecords.has_key(old_keyFieldValue):
 self._newRecords[keyFieldValue] = None
                                                 del
 self._newRecords[old_keyFieldValue]


 }}}

 B) Inserted into dCursorMixin.getFieldVal() after the code line:


 {{{
                 rec = self._records[row]

 }}}

         this code block to support mult. field pk:


 {{{
                 if isinstance(fld, basestring) and fld.find(",") >= 0:
                         # convert also if multi PK KeyField value passed
 from bizObj layer
                         fld = [f.strip() for f in fld.split(",")]

 }}}


 C) To warn user on duplicate pk I also had to add to Base.py bizobj class
 (maybe patch/include into dBizObj, what do you think? ... and what would
 be the right method/exception to notify the user from the framework
 level):

 {{{
    def validateField(self, fld, val):
     # If the new value is different from the current value, change it and
 also
     # update the mementos if necessary.


     row = self._CurrentCursor.RowNumber                         # MOVE TO
 dBizObj OR REPLACE PRIVATE ATTRIBUTES
     rec = self._CurrentCursor._records[row]   # ..rec = self.Record?!?!?
     keyField = self._CurrentCursor.KeyField             # ..used cursor
 prop. because biz.KeyField is comma sep str?!?!?
     old_val = rec[fld]
 # ..old_val = rec.getAttr(fld)
     if old_val != val:
       if fld == keyField or fld in keyField:
         # Changing the key field(s) value, need to key the mementos on the
 new
         # value, not the old. Additionally, need to copy the mementos from
 the
         # old key value to the new one.
         if self._CurrentCursor._compoundKey:    # ..if
 isinstance(keyField, tuple):
           old_keyFieldValue = tuple([rec[k] for k in keyField])
           keyFieldValue = tuple([val if k == fld else rec[k] for k in
 keyField])
         else:
           old_keyFieldValue = old_val
           keyFieldValue = val
         # new records pk must be unique (at least within current dataset)
         if self._CurrentCursor._newRecords.has_key(old_keyFieldValue) \
             and self._CurrentCursor._getRowByPk(keyFieldValue) >= 0:
           return "Code/Id/PrimaryKey already exists in the dataset"
         elif self._CurrentCursor._newRecords.has_key(keyFieldValue):
           return "Duplicate Code/Id/PrimaryKey in another, new and unsaved
 record. Please enter unique value or delete the other new/unsaved record."
     return ""
 }}}

         With these patches the framework would support editable and
 compound primary keys. I first want to discuss this with you before I
 spend more time working this out, which could include also the following
 features:

 o add opt. fld+val arguments to dCursorMixin.pkExpression(self, rec=None,
 fld=None, val=None) and virtually overwrite the return value (or part of
 it) with passed val - to get a preview of the pk if val would be written
 to the database column specified by fld.

 o use extended method getPkExpression() in dCursorMixin.setFieldVal() and
 in Base.py (or dBizObj.fieldValidation() after code transfer from
 Base.py/validateField()) to minimize redundant code.

 o While investigation more on compound primary keys I found other places
 in dCursorMixin where compound primary keys are not supported. Not sure if
 the following extension of RemoteConnector.save() to support multiple
 field pk will not break the remote protocol.

 The extensoion could be done by:

 (1) in dCursorMixin.getRecordStatus()/_getNewRecordDiff() replace:
 {{{
         recs = [r for r in self._records if r[self._keyField] == pk]
 }}}
 with:
 {{{
         recs = [r for r in self._records if self.pkExpression(r) == pk]
 }}}

 (2) replace in dCursorMixin.getDataDiff().rowDiff():

 {{{
     ret[self._keyField] = pk
 }}}
 with:
 {{{
                 if self._compoundKey:
                         rec = [r for r in self._records if
 self.pkExpression(r) == pk][0]
                         for fld in self._keyField:
                                 ret[fld] = rec[fld]
                 else:
                         ret[self._keyField] = pk
 }}}

 (3) in dCursorMixin.setDefaults() first remove the following local
 variables at the top:

 {{{
                 keyField = self.KeyField
                 keyFieldSet = False
 }}}

 then replace the last code part of the method:

 {{{
                 if self._nullDefaults:
                         for field in rec.keys():
                                 if field == keyField:
                                         continue
                                 self.setFieldVal(field, None)
                 else:
                         if keyField in vals.keys():
                                 # Must set the pk default value first, for
 mementos to be filled in
                                 # correctly.
                                 setDefault(keyField, vals[keyField])
                                 keyFieldSet = True

                         for field, val in vals.items():
                                 if field == keyField and keyFieldSet:
                                         continue
                                 setDefault(field, val)
 }}}

 with something like:

 {{{
                 if self._compoundKey:
                         keyFields = [fld for fld in self.KeyField]
                 else:
                         keyFields = [self.KeyField]
                 if self._nullDefaults:
                         for field in rec.keys():
                                 if field not in keyFields:
                                         self.setFieldVal(field, None)
                 else:
                         # Must set the pk default value first, for
 mementos to be filled in
                         # correctly.
                         for field in keyFields:
                                 if field in vals:
                                         setDefault(field, vals[field])
                         for field, val in vals.items():
                                 if field not in keyFields:
                                         setDefault(field, val)
 }}}

 (4) not sure if fld[2] in dCursorMixin._setTable() contains True for a pk
 field in all DBs, if yes then we could replace:

 {{{
                                 self._keyField = [fld[0] for fld in
 self.getFields(table)
                                                 if fld[2] ][0]
 }}}

 with:
 {{{
                                 kf = tuple([fld[0] for fld in
 self.getFields(table)
                                                 if fld[2] ])
                                 if len(kf) == 1:
                                   kf = kf[0]
                                 self._keyField = kf
 }}}

 Since long time I want to participate/support an open source project and
 dabo is exactly what I am looking for. Unfortunately I have still no
 internet access from my home ... far out, only from working place ... but
 my best boss of the world allows us to stay late and use our equipment
 privatly. I also will motivate my collueges as good as I can to use  d a b
 o  .

Comment (by paul):

 I agree with Ed. Meaningless PK fields should be hidden from the users but
 always there, and candidate fields can be exposed to the users to make
 them happy.

 Changing PK values really can't be allowed at the application level, but
 if you really want to do this, do it at your database level and you better
 have all the database triggers set up to update the foreign keys in other
 tables, etcetera. But in my opinion you are shooting yourself in the foot
 by allowing the pk value to change arbitrarily, especially by a user.

 Please don't be discouraged; it is obvious you've done a lot of work
 getting this working for your case. We just can't put it into Dabo.

-- 
Ticket URL: <http://trac.dabodev.com/ticket/1175#comment:5>
Dabo Trac <http://trac.dabodev.com>
Trac Page for Dabo


_______________________________________________
Post Messages to: [email protected]
Subscription Maintenance: http://leafe.com/mailman/listinfo/dabo-dev
Searchable Archives: http://leafe.com/archives/search/dabo-dev
This message: http://leafe.com/archives/byMID/[EMAIL PROTECTED]

Reply via email to