Merge lp:~milo/linaro-ci-dashboard/xml-to-dict into lp:linaro-ci-dashboard

Proposed by Milo Casagrande
Status: Merged
Approved by: Stevan Radaković
Approved revision: 52
Merged at revision: 41
Proposed branch: lp:~milo/linaro-ci-dashboard/xml-to-dict
Merge into: lp:linaro-ci-dashboard
Diff against target: 960 lines (+722/-23)
13 files modified
HACKING (+1/-1)
dashboard/frontend/android_textfield_loop/models/android_textfield_loop.py (+3/-0)
dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html (+0/-8)
dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py (+13/-4)
dashboard/frontend/models/loop.py (+28/-4)
dashboard/frontend/models/loop_build.py (+13/-0)
dashboard/frontend/models/textfield_loop.py (+38/-5)
dashboard/frontend/tests/__init__.py (+3/-0)
dashboard/frontend/tests/test_models.py (+29/-1)
dashboard/frontend/tests/test_xml_to_dict.py (+293/-0)
dashboard/lib/template.py (+1/-0)
dashboard/lib/xml_fields.py (+47/-0)
dashboard/lib/xml_to_dict.py (+253/-0)
To merge this branch: bzr merge lp:~milo/linaro-ci-dashboard/xml-to-dict
Reviewer Review Type Date Requested Status
Stevan Radaković Approve
Linaro Infrastructure Pending
Данило Шеган Pending
Review via email: mp+123053@code.launchpad.net

Description of the change

Here there is all the logic for XML to dictionary and dictionary to XML conversions. Some tests and their values are provided.

To post a comment you must log in.
Revision history for this message
Stevan Radaković (stevanr) wrote :
Download full text (7.2 KiB)

Hey Milo great work !!1
Before discussing the dict necessity with danilo, I'll do a review here and lets land this, with tendency to refactor things later.

=== modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py'
--- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000
+++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-06 11:09:25 +0000
@@ -24,9 +24,9 @@

     A_NAME = 'a-build'
     B_NAME = 'b-build'
- VALID_VALUES = 'a=2\nb=3'
+ VALID_VALUES = u'a=2\nb=3'
     VALID_LINES = ['a=2', 'b=3']
- NON_VALID_VALUES = 'a:2\nb=3'
+ NON_VALID_VALUES = u'a:2\nb=3'
     NON_VALID_LINES = ['a:2', 'b=3']
     VALID_DICT = {'a': '2', 'b': '3'}

@@ -52,6 +52,7 @@
     def test_valid_values_wrong(self):
         self.assertEqual((False, self.NON_VALID_LINES),
                          AndroidTextFieldLoop.valid_values(
+<<<<<<< TREE
                              self.NON_VALID_VALUES))

     def test_schedule_build(self):
@@ -62,3 +63,27 @@
     def test_schedule_build_invalid(self):
         build = self.non_valid_android_loop.schedule_build()
         self.assertEqual(build.result_xml, {})
+=======
+ self.NON_VALID_VALUES))
+
+ def test_schedule_build(self):
+ build = self.android_loop.schedule_build()
+ expected_out = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ' \
+ 'loop [\n <!ELEMENT loop (description?,fields)>\n' \
+ ' <!ELEMENT description (#PCDATA)>\n ' \
+ '<!ELEMENT fields (field+)>\n <!ELEMENT field ' \
+ '(#PCDATA)>\n <!ATTLIST field name CDATA ' \
+ '#REQUIRED>\n <!ATTLIST field type (text|int|bool) ' \
+ '"text">\n]><loop><fields><field name="a">2</field>' \
+ '<field name="b">3</field><field name="name">a-build' \
+ '</field><field name="is_restricted">False</field>' \
+ '<field name="is_official">False</field>' \
+ '<field name="type">AndroidTextFieldLoop</field>' \
+ '</fields></loop>'
+ self.assertEqual(expected_out, build.result_xml)
+ self.assertEqual(build.status, "success")
+
+ def test_schedule_build_invalid(self):
+ build = self.non_valid_android_loop.schedule_build()
+ self.assertEqual("", build.result_xml)
+>>>>>>> MERGE-SOURCE

What's with all the white spaces in the expected out? Can't we do strip() somewhere in the code and output xml without new lines and white spaces?

=== modified file 'dashboard/frontend/models/loop_build.py'
--- dashboard/frontend/models/loop_build.py 2012-09-05 13:23:05 +0000
+++ dashboard/frontend/models/loop_build.py 2012-09-06 11:09:25 +0000
@@ -18,6 +18,7 @@

 from django.db import models
 from frontend.models.loop import Loop
+from dashboard.lib.xml_to_dict import XmlToDict

 class LoopBuild(models.Model):
@@ -47,3 +48,15 @@
             else:
                 self.build_number = 1
         ...

Read more...

review: Needs Fixing
Revision history for this message
Milo Casagrande (milo) wrote :
Download full text (8.4 KiB)

Hello Stevan,

thanks for the review!

On Thu, Sep 6, 2012 at 3:55 PM, Stevan Radaković
<email address hidden> wrote:
> Review: Needs Fixing
>
> Hey Milo great work !!1
> Before discussing the dict necessity with danilo, I'll do a review here and lets land this, with tendency to refactor things later.
>
>
>
> === modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py'
> --- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000
> +++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-06 11:09:25 +0000
> @@ -24,9 +24,9 @@
>
> A_NAME = 'a-build'
> B_NAME = 'b-build'
> - VALID_VALUES = 'a=2\nb=3'
> + VALID_VALUES = u'a=2\nb=3'
> VALID_LINES = ['a=2', 'b=3']
> - NON_VALID_VALUES = 'a:2\nb=3'
> + NON_VALID_VALUES = u'a:2\nb=3'
> NON_VALID_LINES = ['a:2', 'b=3']
> VALID_DICT = {'a': '2', 'b': '3'}
>
> @@ -52,6 +52,7 @@
> def test_valid_values_wrong(self):
> self.assertEqual((False, self.NON_VALID_LINES),
> AndroidTextFieldLoop.valid_values(
> +<<<<<<< TREE
> self.NON_VALID_VALUES))
>
> def test_schedule_build(self):
> @@ -62,3 +63,27 @@
> def test_schedule_build_invalid(self):
> build = self.non_valid_android_loop.schedule_build()
> self.assertEqual(build.result_xml, {})
> +=======
> + self.NON_VALID_VALUES))
> +
> + def test_schedule_build(self):
> + build = self.android_loop.schedule_build()
> + expected_out = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ' \
> + 'loop [\n <!ELEMENT loop (description?,fields)>\n' \
> + ' <!ELEMENT description (#PCDATA)>\n ' \
> + '<!ELEMENT fields (field+)>\n <!ELEMENT field ' \
> + '(#PCDATA)>\n <!ATTLIST field name CDATA ' \
> + '#REQUIRED>\n <!ATTLIST field type (text|int|bool) ' \
> + '"text">\n]><loop><fields><field name="a">2</field>' \
> + '<field name="b">3</field><field name="name">a-build' \
> + '</field><field name="is_restricted">False</field>' \
> + '<field name="is_official">False</field>' \
> + '<field name="type">AndroidTextFieldLoop</field>' \
> + '</fields></loop>'
> + self.assertEqual(expected_out, build.result_xml)
> + self.assertEqual(build.status, "success")
> +
> + def test_schedule_build_invalid(self):
> + build = self.non_valid_android_loop.schedule_build()
> + self.assertEqual("", build.result_xml)
> +>>>>>>> MERGE-SOURCE
>
>
> What's with all the white spaces in the expected out? Can't we do strip() somewhere in the code and output xml without new lines and white spaces?

The spaces are just in the DTD, that is a copy paste of the DTD as
defined in the HACKING file. That's the only part with spaces and
newlines. The XML output from the "parser" is just a one-...

Read more...

Revision history for this message
Milo Casagrande (milo) wrote :

On Fri, Sep 7, 2012 at 10:34 AM, Milo Casagrande
<email address hidden> wrote:
>
> The spaces are just in the DTD, that is a copy paste of the DTD as
> defined in the HACKING file. That's the only part with spaces and
> newlines. The XML output from the "parser" is just a one-liner.

Forgot to say that I will remove the spaces and newlines from the DTD
and make a one single line.

--
Milo Casagrande
Infrastructure Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs

Revision history for this message
Stevan Radaković (stevanr) wrote :

On 09/07/2012 10:43 AM, Milo Casagrande wrote:
> On Fri, Sep 7, 2012 at 10:34 AM, Milo Casagrande
> <email address hidden> wrote:
>> The spaces are just in the DTD, that is a copy paste of the DTD as
>> defined in the HACKING file. That's the only part with spaces and
>> newlines. The XML output from the "parser" is just a one-liner.
> Forgot to say that I will remove the spaces and newlines from the DTD
> and make a one single line.
>
hmm, you can use textwrap.dedent("""...""") as an alternative...

Revision history for this message
Stevan Radaković (stevanr) wrote :
Download full text (8.8 KiB)

On 09/07/2012 10:37 AM, Milo Casagrande wrote:
> Hello Stevan,
>
> thanks for the review!
>
> On Thu, Sep 6, 2012 at 3:55 PM, Stevan Radaković
> <email address hidden> wrote:
>> Review: Needs Fixing
>>
>> Hey Milo great work !!1
>> Before discussing the dict necessity with danilo, I'll do a review here and lets land this, with tendency to refactor things later.
>>
>>
>>
>> === modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py'
>> --- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000
>> +++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-06 11:09:25 +0000
>> @@ -24,9 +24,9 @@
>>
>> A_NAME = 'a-build'
>> B_NAME = 'b-build'
>> - VALID_VALUES = 'a=2\nb=3'
>> + VALID_VALUES = u'a=2\nb=3'
>> VALID_LINES = ['a=2', 'b=3']
>> - NON_VALID_VALUES = 'a:2\nb=3'
>> + NON_VALID_VALUES = u'a:2\nb=3'
>> NON_VALID_LINES = ['a:2', 'b=3']
>> VALID_DICT = {'a': '2', 'b': '3'}
>>
>> @@ -52,6 +52,7 @@
>> def test_valid_values_wrong(self):
>> self.assertEqual((False, self.NON_VALID_LINES),
>> AndroidTextFieldLoop.valid_values(
>> +<<<<<<< TREE
>> self.NON_VALID_VALUES))
>>
>> def test_schedule_build(self):
>> @@ -62,3 +63,27 @@
>> def test_schedule_build_invalid(self):
>> build = self.non_valid_android_loop.schedule_build()
>> self.assertEqual(build.result_xml, {})
>> +=======
>> + self.NON_VALID_VALUES))
>> +
>> + def test_schedule_build(self):
>> + build = self.android_loop.schedule_build()
>> + expected_out = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ' \
>> + 'loop [\n <!ELEMENT loop (description?,fields)>\n' \
>> + ' <!ELEMENT description (#PCDATA)>\n ' \
>> + '<!ELEMENT fields (field+)>\n <!ELEMENT field ' \
>> + '(#PCDATA)>\n <!ATTLIST field name CDATA ' \
>> + '#REQUIRED>\n <!ATTLIST field type (text|int|bool) ' \
>> + '"text">\n]><loop><fields><field name="a">2</field>' \
>> + '<field name="b">3</field><field name="name">a-build' \
>> + '</field><field name="is_restricted">False</field>' \
>> + '<field name="is_official">False</field>' \
>> + '<field name="type">AndroidTextFieldLoop</field>' \
>> + '</fields></loop>'
>> + self.assertEqual(expected_out, build.result_xml)
>> + self.assertEqual(build.status, "success")
>> +
>> + def test_schedule_build_invalid(self):
>> + build = self.non_valid_android_loop.schedule_build()
>> + self.assertEqual("", build.result_xml)
>> +>>>>>>> MERGE-SOURCE
>>
>>
>> What's with all the white spaces in the expected out? Can't we do strip() somewhere in the code and output xml without new lines and white spaces?
> The spaces are just in the DTD, that is a copy paste of the DTD as
> de...

Read more...

45. By Milo Casagrande

Merged from trunk.

46. By Milo Casagrande

Fixes for review.

47. By Milo Casagrande

Fixed assertion.

48. By Milo Casagrande

PEP8 fixes.

49. By Milo Casagrande

Added some tests.

50. By Milo Casagrande

Added note, fixed some tests.

51. By Milo Casagrande

Fixed reference.

52. By Milo Casagrande

Removed comment.

Revision history for this message
Milo Casagrande (milo) wrote :

On Fri, Sep 7, 2012 at 12:28 PM, Stevan Radaković
<email address hidden> wrote:
>
> Good idea... We could also check if we can somehow make use of the
> 'exclude' option in the Meta subclass for filtering out the fields; or
> create our own option if it's not good match for us.

Unfortunately, we can't. The exclude option in the Meta class is only
available for the class that inhertis from Form, not for the models.
I pushed the last changes into the branch associated with this MP.

There is a "problem" also with using "model_to_dict": not all the
fields will be serialized to a dictionary. I wrote a note in the code.
The problem is with fields set as "editable=False". That parameter is
used with Form display, but since the "model_to_dict" is a function
taken from the "forms" part of Django, that parameter applies here as
well, so we do not get back everything, even if we include it in the
list of fields we want back.

I tried also with the serializers, but we would need to craft the
correct query, since the filter and exlude functions on serializers
need to have actual values to operate (we would end up doing
pk=self.pk, etc...).
The other options I see are or using getattr, or through
obj.__dict__['field_string']. With both approaches we would still need
to pass a list of fields we want or we want to exclude.
Ciao.

--
Milo Casagrande
Infrastructure Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs

Revision history for this message
Stevan Radaković (stevanr) wrote :

Looks much better Milo.
Thanks for addressing all the comments and cleaning up things.
Approve +1.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING'
--- HACKING 2012-09-05 16:50:12 +0000
+++ HACKING 2012-09-07 12:52:19 +0000
@@ -67,7 +67,7 @@
67 <!ELEMENT fields (field+)>67 <!ELEMENT fields (field+)>
68 <!ELEMENT field (#PCDATA)>68 <!ELEMENT field (#PCDATA)>
69 <!ATTLIST field name CDATA #REQUIRED>69 <!ATTLIST field name CDATA #REQUIRED>
70 <!ATTLIST field type (text|int|bool) "text">70 <!ATTLIST field type (str|int|bool) "str">
71]>71]>
72<loop>72<loop>
73 <fields>73 <fields>
7474
=== modified file 'dashboard/frontend/android_textfield_loop/models/android_textfield_loop.py'
--- dashboard/frontend/android_textfield_loop/models/android_textfield_loop.py 2012-09-05 13:07:59 +0000
+++ dashboard/frontend/android_textfield_loop/models/android_textfield_loop.py 2012-09-07 12:52:19 +0000
@@ -23,6 +23,9 @@
23 class Meta:23 class Meta:
24 app_label = 'android_textfield_loop'24 app_label = 'android_textfield_loop'
2525
26 # Fields we want in the XML for chaining.
27 XML_INCLUDE_LIST = ['name', 'is_official', 'is_restricted']
28
26 def save(self, *args, **kwargs):29 def save(self, *args, **kwargs):
27 self.type = self.__class__.__name__30 self.type = self.__class__.__name__
28 super(self.__class__, self).save(*args, **kwargs)31 super(self.__class__, self).save(*args, **kwargs)
2932
=== modified file 'dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html'
--- dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-08-31 15:21:11 +0000
+++ dashboard/frontend/android_textfield_loop/templates/android_textfield_loop_create.html 2012-09-07 12:52:19 +0000
@@ -6,14 +6,6 @@
6 <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post">6 <form name="{{ form_name }}" action="{% url AndroidTextFieldLoopCreate %}" method="post">
7 {% csrf_token %}7 {% csrf_token %}
8{% endblock create_form %}8{% endblock create_form %}
9
10{% if form.non_field_errors %}
11 <div class="form_error">
12 {% for err in form.non_field_errors %}
13 <div class="error_message">{{ err }}</div>
14 {% endfor %}
15 </div>
16{% endif %}
17{{ form.as_p }}9{{ form.as_p }}
18 <div><input type="submit" value="Submit" /></div>10 <div><input type="submit" value="Submit" /></div>
19 </form>11 </form>
2012
=== modified file 'dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py'
--- dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-05 13:59:57 +0000
+++ dashboard/frontend/android_textfield_loop/tests/test_android_textfield_loop_model.py 2012-09-07 12:52:19 +0000
@@ -18,15 +18,16 @@
18from django.test import TestCase18from django.test import TestCase
19from frontend.android_textfield_loop.models.android_textfield_loop \19from frontend.android_textfield_loop.models.android_textfield_loop \
20 import AndroidTextFieldLoop20 import AndroidTextFieldLoop
21from dashboard.lib.xml_fields import XML_START
2122
2223
23class AndroidTextFieldLoopModelTest(TestCase):24class AndroidTextFieldLoopModelTest(TestCase):
2425
25 A_NAME = 'a-build'26 A_NAME = 'a-build'
26 B_NAME = 'b-build'27 B_NAME = 'b-build'
27 VALID_VALUES = 'a=2\nb=3'28 VALID_VALUES = u'a=2\nb=3'
28 VALID_LINES = ['a=2', 'b=3']29 VALID_LINES = ['a=2', 'b=3']
29 NON_VALID_VALUES = 'a:2\nb=3'30 NON_VALID_VALUES = u'a:2\nb=3'
30 NON_VALID_LINES = ['a:2', 'b=3']31 NON_VALID_LINES = ['a:2', 'b=3']
31 VALID_DICT = {'a': '2', 'b': '3'}32 VALID_DICT = {'a': '2', 'b': '3'}
3233
@@ -56,9 +57,17 @@
5657
57 def test_schedule_build(self):58 def test_schedule_build(self):
58 build = self.android_loop.schedule_build()59 build = self.android_loop.schedule_build()
59 self.assertEqual(build.result_xml, self.VALID_DICT)60 expected_out = (XML_START +
61 '<loop><fields>'
62 '<field name="a" type="str">2</field>'
63 '<field name="is_restricted" type="bool">False</field>'
64 '<field name="b" type="str">3</field>'
65 '<field name="is_official" type="bool">False</field>'
66 '<field name="name" type="str">a-build</field>'
67 '</fields></loop>')
68 self.assertEqual(expected_out, build.result_xml)
60 self.assertEqual(build.status, "success")69 self.assertEqual(build.status, "success")
6170
62 def test_schedule_build_invalid(self):71 def test_schedule_build_invalid(self):
63 build = self.non_valid_android_loop.schedule_build()72 build = self.non_valid_android_loop.schedule_build()
64 self.assertEqual(build.result_xml, {})73 self.assertEqual("", build.result_xml)
6574
=== modified file 'dashboard/frontend/models/loop.py'
--- dashboard/frontend/models/loop.py 2012-09-04 14:51:52 +0000
+++ dashboard/frontend/models/loop.py 2012-09-07 12:52:19 +0000
@@ -44,7 +44,7 @@
44 # List of fields to be excluded by default from the base64 encoded config.44 # List of fields to be excluded by default from the base64 encoded config.
45 # If a subclass needs different fields, it is necessary to override this45 # If a subclass needs different fields, it is necessary to override this
46 # variable.46 # variable.
47 EXCLUDE_LIST = ['id', 'name', 'server', 'loop_ptr']47 EXCLUDE_LIST = ['id', 'server', 'loop_ptr', 'next_loop', 'name', 'type']
4848
49 def __init__(self, *args, **kwargs):49 def __init__(self, *args, **kwargs):
50 self.log = Logger.getClassLogger(self)50 self.log = Logger.getClassLogger(self)
@@ -79,13 +79,37 @@
79 pass79 pass
80 return build80 return build
8181
82 def json(self):82 def json(self, include=None, exclude=None):
83 """Return Loop in a format suitable for serialization as JSON. The83 """Return Loop in a format suitable for serialization as JSON. The
84 method is called without underscores to be callabel from Django84 method is called without underscores to be callable from Django
85 templates.85 templates.
86
87 :param include: the list of fields to be included
88 :type include list
89 :param exclude: the list of fields to be excluded
90 :return A dictionary of the model fields
86 """91 """
87 from django.forms.models import model_to_dict92 from django.forms.models import model_to_dict
88 return model_to_dict(self)93 # XXX
94 # This method will not return all the fields, even if they are in the
95 # include list. This is true for fields like 'type', that are set as
96 # 'editable=False'. Since that is a parameter for the form, and we are
97 # using a function from there, it applies here too.
98 return model_to_dict(self, include, exclude)
99
100 def dict_for_xml(self, include=None, exclude=None):
101 """
102 Get a dictionary representation of this loop to be serialized into an
103 XML form for chaining loop.
104
105 :param include: the list of fields to be included
106 :type include list
107 :param exclude: the list of fields to be excluded
108 :return A dictionary with the fields to be serialized.
109 """
110 # Done in this way, calling self.json, since we have it, but if we
111 # change the json representation, this needs to be changed as well.
112 return self.json(include, exclude)
89113
90 @staticmethod114 @staticmethod
91 def can_chain_into():115 def can_chain_into():
92116
=== modified file 'dashboard/frontend/models/loop_build.py'
--- dashboard/frontend/models/loop_build.py 2012-09-05 13:23:05 +0000
+++ dashboard/frontend/models/loop_build.py 2012-09-07 12:52:19 +0000
@@ -18,6 +18,7 @@
1818
19from django.db import models19from django.db import models
20from frontend.models.loop import Loop20from frontend.models.loop import Loop
21from dashboard.lib.xml_to_dict import XmlToDict
2122
2223
23class LoopBuild(models.Model):24class LoopBuild(models.Model):
@@ -47,3 +48,15 @@
47 else:48 else:
48 self.build_number = 149 self.build_number = 1
49 super(LoopBuild, self).save(*args, **kwargs)50 super(LoopBuild, self).save(*args, **kwargs)
51
52 def get_build_result(self):
53 """
54 Returns a Python dictionary representation of the build XML stored.
55
56 :return A dictionary of the XML result.
57 """
58 dict_result = {}
59 if self.result_xml:
60 xml_to_dict = XmlToDict(self.result_xml)
61 dict_result = xml_to_dict.tree_to_dict()
62 return dict_result
5063
=== modified file 'dashboard/frontend/models/textfield_loop.py'
--- dashboard/frontend/models/textfield_loop.py 2012-09-05 13:59:57 +0000
+++ dashboard/frontend/models/textfield_loop.py 2012-09-07 12:52:19 +0000
@@ -17,6 +17,7 @@
1717
18from django.db import models18from django.db import models
19from frontend.models.loop import Loop19from frontend.models.loop import Loop
20from dashboard.lib.xml_to_dict import DictToXml
2021
2122
22# Default delimiter to separate values from keys in the text field.23# Default delimiter to separate values from keys in the text field.
@@ -39,6 +40,11 @@
3940
40 values = models.TextField()41 values = models.TextField()
4142
43 # List of fields to be included and excluded from serialization.
44 # Each subclass should override these two variables.
45 XML_INCLUDE_LIST = None
46 XML_EXCLUDE_LIST = None
47
42 def schedule_build(self, parameters=None):48 def schedule_build(self, parameters=None):
43 from frontend.models.loop_build import LoopBuild49 from frontend.models.loop_build import LoopBuild
44 build = LoopBuild()50 build = LoopBuild()
@@ -46,7 +52,7 @@
46 build.duration = 0.0052 build.duration = 0.00
4753
48 try:54 try:
49 build.result_xml = self.values_to_dict()55 build.result_xml = self.values_to_xml()
50 build.status = 'success'56 build.status = 'success'
51 except:57 except:
52 build.status = 'failure'58 build.status = 'failure'
@@ -54,15 +60,24 @@
54 build.save()60 build.save()
55 return build61 return build
5662
57 def values_to_dict(self):63 def values_to_dict(self, valid=None, lines=None):
58 """64 """
59 Returns a dictionary representation of the values inserted. The65 Returns a dictionary representation of the values inserted. The
60 key<>value pairs are split on DEFAULT_DELIMITER.66 key<>value pairs are split on DEFAULT_DELIMITER.
6167
62 :return A dictionary of the values inserted.68 The default parameters are used for a second iteration of this function
69 if we do not want to parse again the lines, but we only want to get the
70 dictionary.
71
72 :param valid: If the lines are valid or not.
73 :type valid bool
74 :param lines: The list of lines in the text field.
75 :type lines list
76 :return A dictionary of the values inserted in the text field.
63 """77 """
64 text_to_dict = {}78 text_to_dict = {}
65 valid, lines = self.valid_values(self.values)79 if valid is None and lines is None:
80 valid, lines = self.valid_values(self.values)
6681
67 if valid:82 if valid:
68 for line in lines:83 for line in lines:
@@ -79,7 +94,7 @@
7994
80 :param values: the string with all the key<>value pairs, separated with95 :param values: the string with all the key<>value pairs, separated with
81 a newline character.96 a newline character.
82 :type str97 :type values unicode
83 :return a boolean for the validity, and the list of lines.98 :return a boolean for the validity, and the list of lines.
84 """99 """
85 valid = True100 valid = True
@@ -91,3 +106,21 @@
91 valid = False106 valid = False
92 break107 break
93 return valid, lines108 return valid, lines
109
110 def values_to_xml(self):
111 """
112 Converts the necessary values into an XML tree.
113
114 :return The XML tree as a string, or an empty string if the inserted
115 values are not valid.
116 """
117 xml_string = ""
118 valid, lines = self.valid_values(self.values)
119 if valid:
120 values = self.values_to_dict(valid, lines)
121 other_values = self.dict_for_xml(include=self.XML_INCLUDE_LIST,
122 exclude=self.XML_EXCLUDE_LIST)
123 if other_values:
124 values.update(other_values)
125 xml_string = DictToXml(values).dict_to_tree()
126 return xml_string
94127
=== modified file 'dashboard/frontend/tests/__init__.py'
--- dashboard/frontend/tests/__init__.py 2012-09-03 08:18:10 +0000
+++ dashboard/frontend/tests/__init__.py 2012-09-07 12:52:19 +0000
@@ -2,6 +2,7 @@
2from dashboard.frontend.tests.test_models import *2from dashboard.frontend.tests.test_models import *
3from dashboard.frontend.tests.test_clientresponse import *3from dashboard.frontend.tests.test_clientresponse import *
4from dashboard.frontend.tests.test_custom_commands import *4from dashboard.frontend.tests.test_custom_commands import *
5from dashboard.frontend.tests.test_xml_to_dict import *
56
67
7#starts the test suite8#starts the test suite
@@ -14,4 +15,6 @@
14 'LoopTests': LoopTest,15 'LoopTests': LoopTest,
15 'ClientResponseTests': ClientResponseTests,16 'ClientResponseTests': ClientResponseTests,
16 'JenkinsCommandTest': JenkinsCommandTest,17 'JenkinsCommandTest': JenkinsCommandTest,
18 'XmlToDictTest': XmlToDictTest,
19 'DictToXmlTest': DictToXmlTest,
17 }20 }
1821
=== modified file 'dashboard/frontend/tests/test_models.py'
--- dashboard/frontend/tests/test_models.py 2012-09-04 11:56:22 +0000
+++ dashboard/frontend/tests/test_models.py 2012-09-07 12:52:19 +0000
@@ -26,6 +26,7 @@
26 import AndroidTextFieldLoop26 import AndroidTextFieldLoop
27from lib.model_getter import ModelGetter27from lib.model_getter import ModelGetter
2828
29
29class LoopTest(TestCase):30class LoopTest(TestCase):
3031
31 def setUp(self):32 def setUp(self):
@@ -42,6 +43,11 @@
42 self.android_loop.build_type = "build-android"43 self.android_loop.build_type = "build-android"
43 self.android_loop.save()44 self.android_loop.save()
4445
46 self.loop = Loop()
47 self.loop.name = 'a-loop'
48 self.loop.type = Loop.__class__.__name__
49 self.loop.save()
50
45 def test_get_child_object(self):51 def test_get_child_object(self):
46 loop = Loop.objects.get(id=1)52 loop = Loop.objects.get(id=1)
47 self.assertEqual("testjob_integration", loop.get_child_object().name)53 self.assertEqual("testjob_integration", loop.get_child_object().name)
@@ -49,7 +55,6 @@
49 self.assertEqual("IntegrationLoop",55 self.assertEqual("IntegrationLoop",
50 loop.get_child_object().__class__.__name__)56 loop.get_child_object().__class__.__name__)
5157
52
53 def test_get_all_chainable_integration_empty(self):58 def test_get_all_chainable_integration_empty(self):
54 chainable_loops = IntegrationLoop.get_all_chainable()59 chainable_loops = IntegrationLoop.get_all_chainable()
55 # Integration loop does not have any chainable loops60 # Integration loop does not have any chainable loops
@@ -64,3 +69,26 @@
64 def test_model_getter_unique_model(self):69 def test_model_getter_unique_model(self):
65 all_models = [model.__name__ for model in get_models()]70 all_models = [model.__name__ for model in get_models()]
66 self.assertTrue(len(all_models) == len(set(all_models)))71 self.assertTrue(len(all_models) == len(set(all_models)))
72
73 def test_model_json(self):
74 expected_out = {'next_loop': None, 'name': 'a-loop', 'server': None,
75 'is_restricted': False, 'is_official': False, 'id': 3}
76 self.assertEqual(expected_out, self.loop.json())
77
78 def test_model_dict_for_xml(self):
79 expected_out = {'next_loop': None, 'name': 'a-loop', 'server': None,
80 'is_restricted': False, 'is_official': False, 'id': 3}
81 self.assertEqual(expected_out, self.loop.dict_for_xml())
82
83 def test_model_dict_for_xml_with_include(self):
84 include_list = ['name', 'is_restricted']
85 expected_out = {'name': 'a-loop', 'is_restricted': False}
86 self.assertEqual(expected_out,
87 self.loop.dict_for_xml(include=include_list))
88
89 def test_model_dict_for_xml_with_exclude(self):
90 exclude_list = ['is_restricted', 'name']
91 expected_out = {'next_loop': None, 'server': None,
92 'is_official': False, 'id': 3}
93 self.assertEqual(expected_out,
94 self.loop.dict_for_xml(exclude=exclude_list))
6795
=== added file 'dashboard/frontend/tests/test_xml_to_dict.py'
--- dashboard/frontend/tests/test_xml_to_dict.py 1970-01-01 00:00:00 +0000
+++ dashboard/frontend/tests/test_xml_to_dict.py 2012-09-07 12:52:19 +0000
@@ -0,0 +1,293 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17
18from django.test import TestCase
19import os
20from dashboard.lib.xml_to_dict import (
21 XmlToDict,
22 DictToXml
23)
24from dashboard.lib.xml_fields import (
25 LOOP_ELEMENT,
26 FIELDS_ELEMENT,
27 TYPE_ATTR,
28 NAME_ATTR,
29 XML_START,
30 INT_TYPE,
31 BOOL_TYPE,
32 STR_TYPE,
33)
34
35
36# XML tags
37ROOT_TAG_OPEN = '<loop>'
38ROOT_TAG_CLOSE = '</loop>'
39FIELDS_TAG_OPEN = '<fields>'
40FIELDS_TAG_CLOSE = '</fields>'
41VALID_FIELD = '<field name="%(field_name)s">%(field_value)s</field>'
42VALID_FIELD_WITH_TYPE = ('<field name="%(field_name)s" '
43 'type="%(field_type)s">%(field_value)s</field>')
44
45# Field names, types and values used as defaults for the tests.
46LOOP_TYPE_VAL = 'text-build'
47LOOP_NAME_VAL = 'build-test'
48BUILD_TYPE = 'build_type'
49BUILD_TYPE_VAL = 'build-android'
50REPO_QUIET = 'repo_quiet'
51REPO_QUIET_VAL = True
52BUILD_FS_IMAGE = 'build_fs_image'
53BUILD_FS_IMAGE_VAL = False
54MAKE_JOBS = 'make_jobs'
55MAKE_JOBS_VAL = 2
56
57NEWLINE = os.linesep
58# Used to create the needed dictionary for fields creation.
59# The dictionary has to be:
60# {FIELD_NAME: value, FIELD_VALUE: value [, FIELD_TYPE: value]}
61# FIELD_TYPE is optional.
62FIELD_NAME = 'field_name'
63FIELD_VALUE = 'field_value'
64FIELD_TYPE = 'field_type'
65
66FIELDS = [
67 {FIELD_NAME: BUILD_TYPE, FIELD_VALUE: BUILD_TYPE_VAL},
68 {FIELD_NAME: REPO_QUIET, FIELD_TYPE: BOOL_TYPE,
69 FIELD_VALUE: REPO_QUIET_VAL},
70 {FIELD_NAME: BUILD_FS_IMAGE, FIELD_TYPE: BOOL_TYPE,
71 FIELD_VALUE: BUILD_FS_IMAGE_VAL},
72 {FIELD_NAME: MAKE_JOBS, FIELD_TYPE: INT_TYPE,
73 FIELD_VALUE: MAKE_JOBS_VAL},
74 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL},
75 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL}
76]
77
78VALID_START = XML_START + ROOT_TAG_OPEN
79VALID_CLOSE = ROOT_TAG_CLOSE
80
81
82def create_xml_fields(fields_list):
83 """
84 Support functions to create the <fields><field>... hierarchy.
85
86 :param fields_list: the list of fields to create. Each field has to be
87 a dictionary with FIELD_NAME, FIELD_VALUE and optional FIELD_TYPE keys.
88 :type fields_list list
89 """
90 assert isinstance(fields_list, list)
91 fields_xml = FIELDS_TAG_OPEN
92
93 for field in fields_list:
94 if field.get(FIELD_TYPE, None) is not None:
95 fields_xml += VALID_FIELD_WITH_TYPE % field
96 else:
97 fields_xml += VALID_FIELD % field
98 fields_xml += FIELDS_TAG_CLOSE
99 return fields_xml
100
101
102class XmlToDictTest(TestCase):
103 """
104 Test class for the conversion from XML to Python dictionary.
105 The XML specification is taken from the HACKING file.
106 """
107
108 def setUp(self):
109 super(XmlToDictTest, self).setUp()
110 self.xml_string = VALID_START
111 self.xml_string += create_xml_fields(FIELDS)
112 self.xml_string += VALID_CLOSE
113 self.xml_to_dict = XmlToDict(self.xml_string)
114
115 def test_get_element_from_root(self):
116 """
117 Tests retrieval of the elements in the XML tree, but the root.
118 """
119 self.assertEqual(self.xml_to_dict.get_elements_from_root()[0].tag,
120 FIELDS_ELEMENT)
121
122 def test_get_element_by_name_correct(self):
123 """Tests retrieving of an element with a correct name."""
124 self.assertEqual(FIELDS_ELEMENT,
125 self.xml_to_dict.get_element_by_name(
126 FIELDS_ELEMENT).tag)
127
128 def test_get_element_by_name_wrong(self):
129 """ Tests retrieving of an element with a wrong name."""
130 self.assertEqual(None,
131 self.xml_to_dict.get_element_by_name('wrong'))
132
133 def test_elements_to_dict(self):
134 """
135 Tests creation of a dictionary with multiple elements.
136 """
137 expected_output = {MAKE_JOBS: MAKE_JOBS_VAL,
138 BUILD_TYPE: BUILD_TYPE_VAL,
139 BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL,
140 REPO_QUIET: REPO_QUIET_VAL,
141 NAME_ATTR: LOOP_NAME_VAL,
142 TYPE_ATTR: LOOP_TYPE_VAL}
143 element = self.xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
144 self.assertEqual(expected_output,
145 self.xml_to_dict.element_to_dict(element))
146
147 def test_bool_true_conversion(self):
148 """
149 Tests the True bool conversion.
150 """
151 expected_out = {'a-field': True}
152 for value in ['1', 1, 'y', 'Yes', True, 'True']:
153 local_xml = VALID_START
154 fields_list = [{FIELD_NAME: 'a-field',
155 FIELD_TYPE: BOOL_TYPE,
156 FIELD_VALUE: value}]
157 local_xml += create_xml_fields(fields_list)
158 local_xml += VALID_CLOSE
159 local_xml_to_dict = XmlToDict(local_xml)
160 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
161 self.assertEqual(expected_out,
162 local_xml_to_dict.element_to_dict(element))
163
164 def test_bool_false_conversion(self):
165 """
166 Tests the False bool conversion.
167 """
168 expected_out = {'a-field': False}
169 for value in ['n', 0, 42, 'Yep', 'No', False, 'False', '?']:
170 local_xml = VALID_START
171 fields_list = [{FIELD_NAME: 'a-field',
172 FIELD_TYPE: BOOL_TYPE,
173 FIELD_VALUE: value}]
174 local_xml += create_xml_fields(fields_list)
175 local_xml += VALID_CLOSE
176 local_xml_to_dict = XmlToDict(local_xml)
177 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
178 self.assertEqual(expected_out,
179 local_xml_to_dict.element_to_dict(element))
180
181 def test_int_conversion(self):
182 """
183 Tests the int conversion.
184 """
185 expected_out = {'a-field': 42}
186 for value in [42, '42', '42 ', ' 42']:
187 local_xml = VALID_START
188 fields_list = [{FIELD_NAME: 'a-field',
189 FIELD_TYPE: INT_TYPE,
190 FIELD_VALUE: value}]
191 local_xml += create_xml_fields(fields_list)
192 local_xml += VALID_CLOSE
193 local_xml_to_dict = XmlToDict(local_xml)
194 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
195 self.assertEqual(expected_out,
196 local_xml_to_dict.element_to_dict(element))
197
198 def test_str_conversion(self):
199 """
200 Tests the str conversion.
201 """
202 expected_out = {'a-field': '42'}
203 for value in [42, '42', '42 ', ' 42']:
204 local_xml = VALID_START
205 fields_list = [{FIELD_NAME: 'a-field',
206 FIELD_VALUE: value}]
207 local_xml += create_xml_fields(fields_list)
208 local_xml += VALID_CLOSE
209 local_xml_to_dict = XmlToDict(local_xml)
210 element = local_xml_to_dict.get_element_by_name(FIELDS_ELEMENT)
211 self.assertEqual(expected_out,
212 local_xml_to_dict.element_to_dict(element))
213
214 def test_tree_to_dict(self):
215 """
216 Tests the conversion of a dictionary into a full XML tree.
217 """
218 expected_output = {
219 REPO_QUIET: REPO_QUIET_VAL,
220 NAME_ATTR: LOOP_NAME_VAL,
221 MAKE_JOBS: MAKE_JOBS_VAL,
222 TYPE_ATTR: LOOP_TYPE_VAL,
223 BUILD_TYPE: BUILD_TYPE_VAL,
224 BUILD_FS_IMAGE: BUILD_FS_IMAGE_VAL
225 }
226 self.assertEqual(expected_output, self.xml_to_dict.tree_to_dict())
227
228 def test_get_root_name(self):
229 """
230 Tests retrieving of the root element.
231 """
232 self.assertEqual(self.xml_to_dict.get_root(), LOOP_ELEMENT)
233
234 def test_get_root_attributes(self):
235 """
236 Tests retrieving of the root attributes.
237 """
238 self.assertEqual({}, self.xml_to_dict.get_root_attributes())
239
240
241class DictToXmlTest(TestCase):
242 """
243 Test class for the conversion from Python dictionary into XML.
244 The XML specification is taken from the HACKING file.
245 """
246
247 FIELDS_LIST = [
248 {FIELD_NAME: MAKE_JOBS, FIELD_VALUE: MAKE_JOBS_VAL,
249 FIELD_TYPE: INT_TYPE},
250 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL,
251 FIELD_TYPE: STR_TYPE},
252 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL,
253 FIELD_TYPE: STR_TYPE},
254 ]
255
256 VALID_DICT = {
257 NAME_ATTR: LOOP_NAME_VAL, TYPE_ATTR: LOOP_TYPE_VAL,
258 MAKE_JOBS: MAKE_JOBS_VAL
259 }
260
261 def setUp(self):
262 super(DictToXmlTest, self).setUp()
263 self.valid_xml = VALID_START
264 self.valid_xml += create_xml_fields(self.FIELDS_LIST)
265 self.valid_xml += VALID_CLOSE
266 self.dict_to_xml = DictToXml(self.VALID_DICT)
267
268 def test_dict_to_tree(self):
269 """
270 Tests dictionary to XML conversion.
271 """
272 self.assertEqual(self.valid_xml, self.dict_to_xml.dict_to_tree())
273
274 def test_dict_to_tree_with_bool(self):
275 """
276 Tests dictionary to XML conversion, with a bool inside, ElementTree
277 does not cope very well with bool type.
278 """
279 fields = [
280 {FIELD_NAME: REPO_QUIET, FIELD_VALUE: REPO_QUIET_VAL,
281 FIELD_TYPE: BOOL_TYPE},
282 {FIELD_NAME: TYPE_ATTR, FIELD_VALUE: LOOP_TYPE_VAL,
283 FIELD_TYPE: STR_TYPE},
284 {FIELD_NAME: NAME_ATTR, FIELD_VALUE: LOOP_NAME_VAL,
285 FIELD_TYPE: STR_TYPE},
286 ]
287 dictionary = {TYPE_ATTR: LOOP_TYPE_VAL, NAME_ATTR: LOOP_NAME_VAL,
288 REPO_QUIET: REPO_QUIET_VAL}
289 dict_2_xml = DictToXml(dictionary)
290 xml_string = VALID_START
291 xml_string += create_xml_fields(fields)
292 xml_string += VALID_CLOSE
293 self.assertEqual(xml_string, dict_2_xml.dict_to_tree())
0294
=== modified file 'dashboard/lib/template.py'
--- dashboard/lib/template.py 2012-08-22 14:44:49 +0000
+++ dashboard/lib/template.py 2012-09-07 12:52:19 +0000
@@ -16,6 +16,7 @@
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17# Django settings for dashboard project.17# Django settings for dashboard project.
1818
19
19class AbstractTemplate(object):20class AbstractTemplate(object):
20 "Template class interface."21 "Template class interface."
2122
2223
=== added file 'dashboard/lib/xml_fields.py'
--- dashboard/lib/xml_fields.py 1970-01-01 00:00:00 +0000
+++ dashboard/lib/xml_fields.py 2012-09-07 12:52:19 +0000
@@ -0,0 +1,47 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17# Django settings for dashboard project.
18
19# The file contains all the necessary elements, tags and everything else
20# related to the XML representation of a chain-build.
21
22# All the XML valid elements.
23FIELD_ELEMENT = 'field'
24FIELDS_ELEMENT = 'fields'
25LOOP_ELEMENT = 'loop'
26DESCRIPTION_ELEMENT = 'description'
27
28# All the XML attributes.
29NAME_ATTR = 'name'
30TYPE_ATTR = 'type'
31
32# The valid types for element values.
33BOOL_TYPE = 'bool'
34INT_TYPE = 'int'
35STR_TYPE = 'str'
36VALID_TYPES = [BOOL_TYPE, INT_TYPE, STR_TYPE]
37
38# List of valid values considered as boolean True.
39VALID_TRUES = ['1', 'y', 'yes', 'Yes', 'True', 'true']
40
41# This is the start of the XML file. The DTD, if updated, needs to be updated
42# also in the HACKING file. This one is taken from the HACKING file.
43XML_START = ('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE loop '
44 '[<!ELEMENT loop (description?,fields)><!ELEMENT description '
45 '(#PCDATA)><!ELEMENT fields (field+)><!ELEMENT field '
46 '(#PCDATA)><!ATTLIST field name CDATA #REQUIRED>'
47 '<!ATTLIST field type (str|int|bool) "str">]>')
048
=== added file 'dashboard/lib/xml_to_dict.py'
--- dashboard/lib/xml_to_dict.py 1970-01-01 00:00:00 +0000
+++ dashboard/lib/xml_to_dict.py 2012-09-07 12:52:19 +0000
@@ -0,0 +1,253 @@
1# Copyright (C) 2012 Linaro
2#
3# This file is part of linaro-ci-dashboard.
4#
5# linaro-ci-dashboard is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# linaro-ci-dashboard is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
17# Django settings for dashboard project.
18
19import xml.etree.ElementTree as ET
20from dashboard.lib.xml_fields import *
21
22
23class XmlToDictException(Exception):
24 """
25 Exception for the XmlToDict class.
26 """
27
28
29class DictToXmlException(Exception):
30 """
31 Exception fro the DictToXml class.
32 """
33
34
35class XmlToDict(object):
36 """
37 Class to convert an XML string into a Python dictionary.
38 This applies to the XML as defined for the loop chaining.
39
40 Simple usage:
41 xml_2_dict = XmlToDict(xml_string).tree_to_dict()
42 """
43
44 def __init__(self, xml):
45 """
46 Initialize the XmlToDict class.
47
48 :param xml: The XML tree as a string.
49 :type xml str
50 """
51 if not isinstance(xml, str):
52 raise XmlToDictException("Type of parameter is not 'str', "
53 "got '%s' instead." % type(xml).__name__)
54 super(XmlToDict, self).__init__()
55 self.tree = ET.fromstring(xml)
56
57 def get_root(self):
58 """
59 Returns the name of the root of the XML tree.
60 :return The name of the root element.
61 """
62 return self.tree.tag
63
64 def get_root_attributes(self):
65 """
66 Returns a dictionary containing the attributes of the root element.
67
68 :return A dictionary with the attributes of the root element.
69 """
70 root_attr = {}
71 for item in self.tree.items():
72 root_attr[item[0]] = item[1]
73 return root_attr
74
75 def get_elements_from_root(self):
76 """
77 Returns the list of elements of the XML tree starting from the root.
78 :return A list with Element instances.
79 """
80 return list(self.tree)
81
82 def get_element_by_name(self, name):
83 """
84 Returns an element from the XML tree by its name.
85 :param name: the name of the element to get.
86 :type name str
87 :return An Element instance, None if not found.
88 """
89 return self.tree.find(name)
90
91 def element_to_dict(self, element):
92 """
93 Returns a dictionary representation of the provided XML element.
94
95 :param element: The Element to convert.
96 :type element Element
97 :return The dictionary representation of element.
98 """
99 if not isinstance(element, ET.Element):
100 raise XmlToDictException("Type of parameter is not 'Element', "
101 "got '%s' instead" %
102 type(element).__name__)
103 element_dict = {}
104 element_tag = element.tag
105 element_text = element.text
106
107 if element_text:
108 element_text = self._check_element_text(element_text)
109
110 attributes = element.items()
111 children = list(element)
112
113 # Special case for XML 'field' tag.
114 # 'field' tag must have a value and at least 'name' attribute set.
115 if element_tag == FIELD_ELEMENT:
116 if attributes:
117 element_name = element.get(NAME_ATTR)
118 element_type = element.get(TYPE_ATTR)
119 # Convert the value into the specified type.
120 value = self.convert_to_type(element_type, element.text)
121 element_dict[element_name] = value
122 else:
123 if attributes:
124 # Other case, we store all attributes as key<>value, plus also
125 # the tag name and its value if OK.
126 # There might be name collisions if XML is not well done.
127 for attribute in attributes:
128 element_dict[attribute[0]] = attribute[1]
129 if element_text:
130 element_dict[element_tag] = element_text
131 elif element_text:
132 element_dict[element_tag] = element_text
133 if children:
134 for child in children:
135 element_dict.update(self.element_to_dict(child))
136 return element_dict
137
138 def _check_element_text(self, text):
139 """
140 Check that the value contained in an XML element is valid, meaning it
141 is not an empty space, a newline. In these cases, we return None since
142 we do not want to have that element in the final dictionary.
143
144 :param text: The value contained between opening and closing XML tag.
145 :type text str
146 :return None if text is empty spaces or newlines, text otherwise.
147 """
148 # We do this since when we get nested elements, there might be 'valid'
149 # characters, like newlines or empty spaces, between the opening and
150 # closing tags, but we do not want to have such fields in the resulting
151 # dictionary.
152 import os
153 text = str(text).strip()
154 if text == os.linesep or len(text) == 0:
155 text = None
156 return text
157
158 def convert_to_type(self, value_type, value):
159 """
160 Converts a value of an XML element into the provided type. If type is
161 None, tha value is converted into a string.
162
163 :param value_type: The type of the resulting value.
164 :type value_type str
165 :param value: The value associated with the XML element.
166 :return The value converted into the specified type.
167 """
168 converted = str(value).strip()
169 if value_type == BOOL_TYPE:
170 if converted in VALID_TRUES:
171 converted = True
172 else:
173 converted = False
174 elif value_type == INT_TYPE:
175 converted = int(converted)
176
177 return converted
178
179 def tree_to_dict(self):
180 """
181 Create a dictionary out of the XML tree.
182
183 :return A python dictionary of the XML tree.
184 """
185 tree_dict = {}
186
187 root_dict = self.get_root_attributes()
188 elements_dict = {}
189 for element in self.get_elements_from_root():
190 elements_dict.update(self.element_to_dict(element))
191
192 if root_dict:
193 tree_dict.update(root_dict)
194
195 if elements_dict:
196 if tree_dict:
197 tree_dict.update(elements_dict)
198 else:
199 tree_dict = elements_dict
200
201 return tree_dict
202
203
204class DictToXml(object):
205 """
206 Class to convert a Python dictionary into an XML string .
207
208 Simple usage:
209 dict_2_xml = DictToXml(dictionary).dict_to_tree()
210 """
211
212 def __init__(self, dictionary):
213 """
214 Initialize the DictToXml class.
215
216 :param dictionary: The dictionary to convert.
217 :type dictionary dict
218 """
219 if not isinstance(dictionary, dict):
220 raise DictToXmlException("Type of parameter is not 'dict, "
221 "got '%s' instead." %
222 type(dictionary).__name__)
223 super(DictToXml, self).__init__()
224 self.dictionary = dictionary
225
226 def dict_to_tree(self):
227 """
228 Create an XML tree out of the provided dictionary. The XML structure
229 is defined in the HACKING file.
230
231 :return A string with the XML representation of the dictionary.
232 """
233 dict_to_xml = XML_START
234
235 loop = ET.Element(LOOP_ELEMENT)
236 fields = ET.SubElement(loop, FIELDS_ELEMENT)
237 for key, value in self.dictionary.iteritems():
238 # Get the type of the value. If it comes from Django, everything
239 # will be unicode type, we default to 'str'.
240 # TODO maybe have a mappings of fields<>type?
241 value_type = type(value).__name__
242 if not value_type in VALID_TYPES:
243 value_type = STR_TYPE
244 attrib = {NAME_ATTR: key, TYPE_ATTR: value_type}
245 field = ET.SubElement(fields, FIELD_ELEMENT, attrib)
246 # ElementTree cannot serialize boolean types.
247 field.text = str(value)
248
249 # Dump the XML and add it to the valid start with its DTD.
250 xml_dump = ET.tostring(loop)
251 dict_to_xml += xml_dump
252
253 return dict_to_xml

Subscribers

People subscribed via source and target branches