Commits

Luke Plant committed d593125

Implemented the ability to have double/triple/etc width columns

Schema change - need to do ./manage.py reset semanticeditor

  • Participants
  • Parent commits 7936ba8

Comments (0)

Files changed (4)

File semanticeditor/models.py

                                         "it can be applied to layout rows or columns ",
                                         default="h1 h2 h3 h4 h5 h6 p blockquote ul li row column")
 
+    column_equiv = models.IntegerField("Column count equivalent", null=True, blank=True,
+                                       help_text="For classes designed to be applied to "
+                                       "columns only, this is the number of columns this "
+                                       "be considered as equivalent too.  This can be "
+                                       "useful for generating double width columns etc. "
+                                       "within a column layout.")
     def __unicode__(self):
         return self.verbose_name
 

File semanticeditor/tests.py

         self.assertEqual(outh, format_html(html, {'newrow_h1_1':[NEWROW],
                                                   'newcol_h1_2':[NEWCOL]}))
 
+    def test_columns_with_double_width(self):
+        html = "<h1>1</h1><p>para 1</p><h1>2</h1>"
+        outh = "<div class=\"row columns3\"><div class=\"column firstcolumn doublewidth\"><h1>1</h1><p>para 1</p></div><div class=\"column lastcolumn\"><h1>2</h1></div></div>"
+        self.assertEqual(outh, format_html(html, {'newrow_h1_1':[NEWROW],
+                                                  'newcol_h1_1':[NEWCOL, PC('doublewidth', column_equiv=2)],
+                                                  'newcol_h1_2':[NEWCOL]}))
+
+    def test_columns_with_double_width_2(self):
+        html = "<h1>1</h1><p>para 1</p><h1>2</h1>"
+        outh = "<div class=\"row columns3\"><div class=\"column firstcolumn\"><h1>1</h1><p>para 1</p></div><div class=\"column lastcolumn doublewidth\"><h1>2</h1></div></div>"
+        self.assertEqual(outh, format_html(html, {'newrow_h1_1':[NEWROW],
+                                                  'newcol_h1_1':[NEWCOL],
+                                                  'newcol_h1_2':[NEWCOL, PC('doublewidth', column_equiv=2)]}))
+
     def test_max_cols(self):
+        """
+        Check we can't exceed max cols
+        """
         html = "<h1>1</h1><h1>2</h1><h1>3</h1><h1>4</h1><h1>5</h1>"
         self.assertRaises(TooManyColumns, format_html, html, {'newrow_h1_1':[NEWROW],
                                                               'newcol_h1_2':[NEWCOL],
                                                               'newcol_h1_4':[NEWCOL],
                                                               'newcol_h1_5':[NEWCOL]
                                                             })
+    def test_max_cols_2(self):
+        """
+        Check we can't exceed max cols with double width cols
+        """
+        html = "<h1>1</h1><h1>2</h1><h1>3</h1>"
+        self.assertRaises(TooManyColumns, format_html, html, {'newrow_h1_1':[NEWROW],
+                                                              'newcol_h1_1':[NEWCOL, PC('doublewidth', column_equiv=2)],
+                                                              'newcol_h1_2':[NEWCOL, PC('doublewidth', column_equiv=2)],
+                                                              'newcol_h1_3':[NEWCOL],
+
+                                                            })
+
 
     def test_columns_2(self):
         html = \

File semanticeditor/utils/presentation.py

 
     # Public interface:
     max_columns = 4
-    def row_classes(self, column_count):
+    def row_classes(self, logical_column_count, actual_column_count):
         """
         Returns a list of CSS classes to be used for a row containing
-        column_count columns
+        logical_column_count 'logical' columns, actual_column_count 'actual'
+        columns.  'actual' columns are present in the HTML structure, but some
+        might be e.g. double width, so are counted as two logical columns.
         """
         retval = [self.ROW_CLASS]
-        if column_count > 1:
-            retval.append("columns%d" % column_count)
+        if actual_column_count > 1:
+            retval.append("columns%d" % logical_column_count)
         return retval
 
-    def column_classes(self, column_num, column_count):
+    def column_classes(self, logical_column_num, actual_column_num, logical_column_count, actual_column_count):
         """
         Returns a list of CSS classes to be used for a column which is number
         column_num out of column_count.
         """
-        if column_count == 1:
+        if actual_column_count == 1:
             # No classes
             return []
         retval = [self.COLUMN_CLASS]
-        if column_num == 1:
+        if actual_column_num == 1:
             retval.append("firstcolumn")
-        if column_num == column_count:
+        if actual_column_num == actual_column_count:
             retval.append("lastcolumn")
         return retval
 
     """
     Encapsulates a piece of presentation information.
     """
-    def __init__(self, prestype=None, name=None, verbose_name="", description="", allowed_elements=None):
+    def __init__(self, prestype=None, name=None, verbose_name="", description="", allowed_elements=None, column_equiv=None):
         self.prestype = prestype
         self.name = name
         # verbose_name, description and allowed_elements are additional pieces
         if allowed_elements is None:
             allowed_elements = []
         self.allowed_elements = allowed_elements
+        self.column_equiv = column_equiv
 
     def __eq__(self, other):
         return self.prestype == other.prestype and self.name == other.name
     def __repr__(self):
         return "PresentationInfo(prestype=\"%s\", name=\"%s\")" % (self.prestype, self.name)
 
-def PresentationClass(name, verbose_name="", description="", allowed_elements=None):
+def PresentationClass(name, verbose_name="", description="", allowed_elements=None, column_equiv=None):
     """
     Shortcut for creating CSS classes
     """
     return PresentationInfo(prestype="class",  name=name,
                             verbose_name=verbose_name, description=description,
-                            allowed_elements=allowed_elements)
+                            allowed_elements=allowed_elements,
+                            column_equiv=column_equiv)
 
 def PresentationCommand(name, verbose_name="", description=""):
     """
 
 #### Layout related ####
 
+# Some dumb container structures
 Layout = struct("Layout", (object,), dict(rows=list))
-LayoutRow = struct("LayoutRow", (object,), dict(columns=list, styles=list))
-LayoutColumn = struct("LayoutColumn", (object,), dict(nodes=list, styles=list))
+LayoutRow = struct("LayoutRow", (object,), dict(columns=list, presinfo=list))
+LayoutColumn = struct("LayoutColumn", (object,), dict(nodes=list, presinfo=list))
 
 _NEWROW_PREFIX = 'newrow_'
 _NEWCOL_PREFIX = 'newcol_'
 
+def _layout_column_width(col):
+    """
+    Returns the logical column width of a column
+    """
+    column_equivs = [pi.column_equiv for pi in col.presinfo if pi.column_equiv is not None]
+    if len(column_equivs) > 0:
+        # assume user has not done something silly like put
+        # *2* column_equiv classes on a column
+        return column_equivs[0]
+    else:
+        return 1
+
+def _layout_column_count(row):
+    """
+    Get the number of logical columns in a LayoutRow
+    """
+    return sum(_layout_column_width(c) for c in row.columns)
+
 def _find_layout_commands(root, structure, styleinfo):
     # Layout commands are not stored against normal sections,
     # but have their own entry in the section list, using an id
                if row.columns:
                    layout.rows.append(row)
                # Start new row with styles
-               row = LayoutRow(styles=_get_classes_from_presinfo(row_presinfo))
+               row = LayoutRow(presinfo=row_presinfo)
                # Start new col
                col = LayoutColumn()
 
                if col.nodes:
                    row.columns.append(col)
                # Start new col with styles
-               col = LayoutColumn(styles=_get_classes_from_presinfo(col_presinfo))
+               col = LayoutColumn(presinfo=col_presinfo)
 
         # Now deal with content itself
         col.nodes.append(node)
     sect_dict = dict((si.node, si) for si in structure)
     max_cols = layout_strategy.max_columns
     for row in layout.rows:
-        if len(row.columns) > max_cols:
-            # Look at first node in first bad column
-            node = row.columns[max_cols - 1].nodes[0]
+        if _layout_column_count(row) > max_cols:
+            # Because columns can be multiple width, we can't easily work out
+            # which column needs to be moved, so just refer user to whole
+            # section.
+            node = row.columns[0].nodes[0]
             sect = sect_dict[node]
             raise TooManyColumns("The maximum number of columns is %(max)d. "
-                                 "Please move section '%(name)s' into a new "
-                                 "row." % dict(max=max_cols, name=sect.name))
+                                 "Please adjust columns in section '%(name)s'." %
+                                 dict(max=max_cols, name=sect.name))
 
 def _render_layout(layout, layout_strategy):
     root = ET.fromstring("<html></html>")
     for row in layout.rows:
-        column_count = len(row.columns)
+        # Row
+        logical_column_count = _layout_column_count(row)
+        actual_column_count = len(row.columns)
         rowdiv = ET.Element('div')
-        classes = layout_strategy.row_classes(column_count) + row.styles
+        classes = layout_strategy.row_classes(logical_column_count, actual_column_count) + _get_classes_from_presinfo(row.presinfo)
         if classes:
             rowdiv.set('class', ' '.join(classes))
+
+        # Columns
+        logical_column_num = 1
         for i, col in  enumerate(row.columns):
             coldiv = ET.Element('div')
-            classes = layout_strategy.column_classes(i + 1, column_count) + col.styles
+            classes = layout_strategy.column_classes(logical_column_num,
+                                                     i + 1,
+                                                     logical_column_count,
+                                                     actual_column_count) + \
+                    _get_classes_from_presinfo(col.presinfo)
             if classes:
                 coldiv.set('class', ' '.join(classes))
             for n in col.nodes:
                 coldiv.append(n)
             rowdiv.append(coldiv)
+
+            logical_column_num += _layout_column_width(col)
         root.append(rowdiv)
     return root
 
     root, structure = format_html(html, pres, return_tree=True)
     known_nodes = dict((si.node, si) for si in structure)
     _create_preview(root, structure, known_nodes)
-    print _html_extract(root)
     return _html_extract(root)
 
 def _create_preview(node, structure, known_nodes):

File semanticeditor/views.py

     """
     return pi.__dict__
 
-def dict_to_PI(d):
+def dict_to_PI(d, classes):
     """
-    Convert a dictionary to a PresentationInfo
+    Convert a dictionary to a PresentationInfo,
+    using a pre-fetched dictionary of CssClass objects
     """
-    return PresentationInfo(prestype=d['prestype'], name=d['name'])
+    if d['prestype'] == 'command':
+        return PresentationInfo(prestype=d['prestype'], name=d['name'])
+    else:
+        c = classes.get(d['name'])
+        if c is None:
+            return None
+        else:
+            return css_class_to_presentation_class(c)
+
+def css_class_to_presentation_class(c):
+    return PresentationClass(c.name,
+                             verbose_name=c.verbose_name,
+                             description=c.description,
+                             allowed_elements=c.allowed_elements.lower().split(' '),
+                             column_equiv=c.column_equiv)
 
 @json_view
 def retrieve_styles(request):
-    retval = [PresentationClass(c.name,
-                                verbose_name=c.verbose_name,
-                                description=c.description,
-                                allowed_elements=c.allowed_elements.lower().split(' '))
-              for c in CssClass.objects.all().order_by('verbose_name')]
+    retval = map(css_class_to_presentation_class,
+                 CssClass.objects.all().order_by('verbose_name'))
     return success(map(PI_to_dict,retval))
 
 @json_view
     return graceful_errors(AllUserErrors, _handled)
 
 def _convert_pres(pres):
-    # Convert dictionaries into PresentationInfo classes
+    # Convert dictionaries into PresentationInfo classes. We need actual
+    # CssClass instances in order to be able to restore column_equiv and
+    # allowed_elements info
+    classes = dict((c.name, c) for c in CssClass.objects.all())
+    retval = {}
     for k, v in pres.items():
         # v is list of PI dicts
+        newlist = []
         for i, item in enumerate(v):
-            v[i] = dict_to_PI(item)
+            pi = dict_to_PI(item, classes)
+            # if CssClass was removed from DB, pi can be None
+            if pi is not None:
+                newlist.append(pi)
+        retval[k]= newlist
+    return retval
 
 @json_view
 def combine_presentation(request):
     html = request.POST.get('html', '').encode("utf-8")
     presentation = request.POST.get('presentation', '{}')
     presentation = simplejson.loads(presentation)
-    _convert_pres(presentation)
+    presentation = _convert_pres(presentation)
 
     return graceful_errors(AllUserErrors, lambda: dict(html=format_html(html, presentation, pretty_print=True)))
 
     html = request.POST.get('html', '').encode("utf-8")
     presentation = request.POST.get('presentation', '{}')
     presentation = simplejson.loads(presentation)
-    _convert_pres(presentation)
+    presentation = _convert_pres(presentation)
 
     return graceful_errors(AllUserErrors, lambda: dict(html=preview_html(html, presentation)))