ArchGenXML: Patch to support ordering of ReferenceFields created by associations

"Matt Hahnfeld" <[email protected]>
Newsgroups gmane.comp.web.zope.plone.archetypes.devel
Message-ID <[email protected]>
Hello all!

The attached patch implements support for the "next_field" tagged
value on any attribute or association end.  This is one way to resolve
the infamous issue in UML/ArchGenXML that doesn't allow reference
fields created through associations to be ordered within the generated
schema.

Of course, ReferenceFields can be defined as attributes instead of
associations, but then you lose the visual benefits of having your
references appear on your UML diagram.  When dealing with a large
number of Archetypes-based content types, we've found that using
associations is almost a necessity.  This patch allows you to use
associations and have them ordered into the rest of your fields.

We've looked at many other options (including numeric ordering, using
an ordered array of fields as a tagged value on the class, modifying
argouml to support custom tags for ordering, using "stub" attributes
to match up with associations, etc.) but we found this solution to be 
more logical, maintainable, and better supported by UML editors than
the other solutions.

So, here's an example:

* I have two classes -- Page and Image (of course, image could be a stub
class for the standard ATImage).  I want each Page to contain a body
(text), an Image from our images directory called "myimage" (reference),
and an image caption called "caption" (string).  I want the fields to
appear in that order.

* In my UML diagram, I would create my two classes (Page and Image) and
add "body:text" and "caption:string" to my class attributes for Page.

* I would add an association from Page to Image, and set its field/widget
attributes at the endpoint closest to the Image class.

* If I want my image field to appear after my body field, I would add the
"next_field" tagged value to "body", and give it the value "myimage".

* When I run ArchGenXML to generate my class, the three fields generated
would be "body", then "myimage", then "caption".

This is a patch to the current "trunk" revision of
ArchetypesGenerator.py as of February 27, 2006 (revision 6053).  I am
looking for feedback at this point, and I would like to propose this
patch, or something similar, for future inclusion into ArchGenXML.

I'm patching against svn, so I should be able to provide up-to-date
patches for future revisions of ArchetypesGenerator.py.  I could also
add prev_field support pretty easily if there is a desire for that.

Thanks in advance for feedback/comments.  Enjoy!

Matt Hahnfeld
[email protected]
next_field_support.patch (application/octet-stream, 7.8 KB)
Index: ArchetypesGenerator.py
===================================================================
--- ArchetypesGenerator.py	(revision 6053)
+++ ArchetypesGenerator.py	(working copy)
@@ -1058,10 +1058,10 @@
 
         return widgetcode
 
-    def getFieldFormatted(self,name,fieldtype,map={},doc=None, indent_level=0, rawType='String'):
+    def getFieldFormatted(self,name,fieldtype,map={},doc=None, indent_level=0, rawType='String', array_field=False):
         """Return the formatted field definitions for the schema.
         """
-
+        
         log.debug("Trying to get formatted field. name='%s', fieldtype='%s', "
                   "doc='%s', rawType='%s'.", name, fieldtype, doc, rawType)
         res = ''
@@ -1105,16 +1105,20 @@
 
         res += '\n%s' % utils.indent('),', indent_level) + '\n'
 
+        if array_field:
+            res = "ArrayField(%s)," % utils.indent(res, 2)
+
         return res
 
-    def getFieldString(self, element, classelement, indent_level=0):
+    def getFieldSpec(self, element, classelement, indent_level=0):
         """Gets the schema field code."""
         typename = element.type
         ctype = self.coerceType(typename)
         map = typeMap[ctype]['map'].copy()
-        res = self.getFieldFormatted(element.getCleanName(),
-                                     self.typeMap[ctype]['field'].copy(),
-                                     map, indent_level)
+        res= {'name':element.getCleanName(),
+            'fieldtype':self.typeMap[ctype]['field'].copy(),
+            'map':map,
+            'indent_level':indent_level}
         return res
 
     def addVocabulary(self, element, attr, map):
@@ -1153,7 +1157,7 @@
 
         # end ATVM
 
-    def getFieldStringFromAttribute(self, attr, classelement, indent_level=0):
+    def getFieldSpecFromAttribute(self, attr, classelement, indent_level=0):
         """Gets the schema field code."""
 
         if not hasattr(attr, 'type') or attr.type == 'NoneType':
@@ -1220,17 +1224,17 @@
             if map.has_key('validation_expression_errormsg'):
                 del map['validation_expression_errormsg']
 
-        res = self.getFieldFormatted(attr.getName(), atype, map, doc,
-                                     rawType=attr.getType(),
-                                     indent_level=indent_level)
+        res={'name':attr.getName(),
+            'fieldtype':atype,
+            'map':map,
+            'doc':doc,
+            'indent_level':indent_level,
+            'rawType':attr.getType(),
+            'array_field':attr.getUpperBound() != 1}
 
-        if attr.getUpperBound() != 1:
-            utils.indent(res, 1)
-            res = """ArrayField(%s),""" % utils.indent(res, 1)
-
         return res
 
-    def getFieldStringFromAssociation(self, rel, classelement, indent_level=0):
+    def getFieldSpecFromAssociation(self, rel, classelement, indent_level=0):
         """Return the schema field code."""
 
         log.debug("Getting the field string from an association.")
@@ -1304,10 +1308,14 @@
                                 % rel.getName()})
 
         doc=rel.getDocumentation(striphtml=self.striphtml)
-        res=self.getFieldFormatted(name, field, map, doc, indent_level)
+        res={'name':name,
+            'fieldtype':field,
+            'map':map,
+            'doc':doc,
+            'indent_level':indent_level}
         return res
 
-    def getFieldStringFromBackAssociation(self, rel, classelement, indent_level=0):
+    def getFieldSpecFromBackAssociation(self, rel, classelement, indent_level=0):
         """Gets the schema field code"""
         multiValued = 0
         obj = rel.fromEnd.obj
@@ -1361,9 +1369,35 @@
                 return None
 
         doc = rel.getDocumentation(striphtml=self.striphtml)
-        res = self.getFieldFormatted(name, field, map, doc, indent_level)
+        res={'name':name,
+            'fieldtype':field,
+            'map':map,
+            'doc':doc,
+            'indent_level':indent_level}
         return res
 
+    def reorderFields(self, field_specs):
+        """ reorder fields according to the "next_field" tagged value """
+        # initialize a list to keep track of the ordering and a copy of our array
+        field_names = []
+        next_fields = []
+        for fs in field_specs:
+            field_names.append(fs['name'])
+            if 'map' in fs and 'next_field' in fs['map']:
+                next_fields.append((fs['name'], str(fs['map']['next_field']).strip('"')))
+                del fs['map']['next_field']
+        # figure out new ordering
+        for (first,second) in next_fields:
+            if second in field_names:
+                from_index = field_names.index(second)
+                temp = field_specs[from_index]
+                del field_names[from_index]
+                del field_specs[from_index]
+                to_index = field_names.index(first) + 1
+                field_names.insert(to_index,second)
+                field_specs.insert(to_index,temp)
+        return field_specs
+
     # Generate get/set/add member functions.
     def generateArcheSchema(self, element, base_schema, indent_level=0):
         """ generates the Schema """
@@ -1413,6 +1447,7 @@
                                       or fieldname, element, fieldname)
 
         print >> outfile, SCHEMA_START
+        field_specs = []
         aggregatedClasses = []
 
         for attrDef in element.getAttributeDefs():
@@ -1421,8 +1456,8 @@
             #    continue
             mappedName = utils.mapName(name)
 
-            print >> outfile, self.getFieldStringFromAttribute(attrDef, element,
-                                                    indent_level=indent_level+1)
+            field_specs.append(self.getFieldSpecFromAttribute(attrDef, element,
+                                                    indent_level=indent_level+1))
 
         for child in element.getChildren():
             name = child.getCleanName()
@@ -1433,8 +1468,8 @@
                 aggregatedClasses.append(str(child.getRef()))
 
             if child.isIntrinsicType():
-                print >> outfile, self.getFieldString(child, element,
-                                                    indent_level=indent_level+1)
+                field_specs.append(self.getFieldSpec(child, element,
+                                                    indent_level=indent_level+1))
 
         # only add reference fields if tgv generate_reference_fields
         if utils.toBoolean(
@@ -1448,10 +1483,9 @@
                 #print 'generating from assoc'
                 if name in self.reservedAtts:
                     continue
-                print >> outfile
-                print >> outfile, self.getFieldStringFromAssociation(rel,
+                field_specs.append(self.getFieldSpecFromAssociation(rel,
                                                     element,
-                                                    indent_level=indent_level+1)
+                                                    indent_level=indent_level+1))
 
             #Back References
             for rel in element.getToAssociations():
@@ -1460,13 +1494,16 @@
                 #print "backreference"
                 if name in self.reservedAtts:
                     continue
-                fc=self.getFieldStringFromBackAssociation(rel,
+                fc=self.getFieldSpecFromBackAssociation(rel,
                                                     element,
                                                     indent_level=indent_level+1)
                 if fc:
-                    print >> outfile
-                    print >> outfile, fc
+                    field_specs.append(fc)
 
+        self.reorderFields(field_specs)
+        for field_spec in field_specs:
+            print >> outfile, self.getFieldFormatted(**field_spec)
+            
         print >> outfile,'),'
         marshaller=element.getTaggedValue('marshaller') or element.getTaggedValue('marshall')
         if marshaller:
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.