Merge lp:~arthur-she/lava-dashboard/fix-for-bug-1175597 into lp:lava-dashboard

Proposed by Arthur She
Status: Approved
Approved by: Stevan Radaković
Approved revision: 408
Proposed branch: lp:~arthur-she/lava-dashboard/fix-for-bug-1175597
Merge into: lp:lava-dashboard
Diff against target: 458 lines (+318/-13)
7 files modified
dashboard_app/filters.py (+7/-5)
dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py (+270/-0)
dashboard_app/models.py (+13/-2)
dashboard_app/templates/dashboard_app/filter_form.html (+9/-0)
dashboard_app/templates/dashboard_app/filter_preview.html (+1/-1)
dashboard_app/templates/dashboard_app/filter_summary.html (+3/-3)
dashboard_app/views/filters/forms.py (+15/-2)
To merge this branch: bzr merge lp:~arthur-she/lava-dashboard/fix-for-bug-1175597
Reviewer Review Type Date Requested Status
Stevan Radaković Approve
Review via email: mp+167494@code.launchpad.net

Description of the change

Add "Equation" field (<=, ==, >=) to make the filter more flexible.

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

Arthur, great work!

Couple of comments:

=== modified file 'dashboard_app/filters.py'
--- dashboard_app/filters.py 2013-01-09 00:15:10 +0000
+++ dashboard_app/filters.py 2013-06-05 10:06:54 +0000
@@ -365,13 +365,24 @@

     content_type_id = ContentType.objects.get_for_model(TestRun).id

- for (name, value) in filter_data['attributes']:
+ for (name, equation, value) in filter_data['attributes']:
         # We punch through the generic relation abstraction here for 100x
         # better performance.
- conditions.append(
- models.Q(id__in=NamedAttribute.objects.filter(
- name=name, value=value, content_type_id=content_type_id
- ).values('object_id')))
+ if equation == '>=':
+ conditions.append(
+ models.Q(id__in=NamedAttribute.objects.filter(
+ name=name, value__gte=value, content_type_id=content_type_id
+ ).values('object_id')))
+ elif equation == '<=':
+ conditions.append(
+ models.Q(id__in=NamedAttribute.objects.filter(
+ name=name, value__lte=value, content_type_id=content_type_id
+ ).values('object_id')))
+ else:
+ conditions.append(
+ models.Q(id__in=NamedAttribute.objects.filter(
+ name=name, value=value, content_type_id=content_type_id
+ ).values('object_id')))

I'd really like to see a more generic solution here... If we add more filter equations later, this code will have to be subjected to refactoring again and again. If we would have some kind of mapping like
equation_map = {"<=": "lte" ....}
we could use pyhon's argument packing, something like
kwargs = {
    '{0}__{1}'.format('value', 'lte'): value,
    '{0}__{1}'.format('value', 'gte'): value
}

NamedAttribute.objects.filter(**kwargs)

Second thing are the tests. I know the test coverage in dashboard is scarce, but do we want to keep it that way?

review: Needs Fixing (code)
Revision history for this message
Stevan Radaković (stevanr) wrote :

Sorry for the code paste, I hope it's clear what lines I'm referring to.

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

One additional comment, we can circumvent the equation map altogether, if the db values for equation field would be defined as 'lte', 'gte'.. to start with. In the frontend we keep the '<=', '>=' of course...

Revision history for this message
Arthur She (arthur-she) wrote :

Thanks a lot, Stevan. It really helpful.
I'll modify it.

2013/6/5 Stevan Radaković <email address hidden>

> One additional comment, we can circumvent the equation map altogether, if
> the db values for equation field would be defined as 'lte', 'gte'.. to
> start with. In the frontend we keep the '<=', '>=' of course...
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

Revision history for this message
Arthur She (arthur-she) wrote :

Hi Stevan,
I know the idea, if we can make it more generic it should be better.
As you mentioned, we can use a dictionary to map the equation "equation_map
= {"<=": "lte", ">=": "gte", ""==": ""}"
and use argument packing, but I still don't have idea how to associate them
together. Could you talk more..

And regarding the tests, Tyler suggested me to do such modification. Do you
have any other idea to approach it?

Thanks a lot.
Arthur

2013/6/5 Stevan Radaković <email address hidden>

> Review: Needs Fixing code
>
> Arthur, great work!
>
> Couple of comments:
>
> === modified file 'dashboard_app/filters.py'
> --- dashboard_app/filters.py 2013-01-09 00:15:10 +0000
> +++ dashboard_app/filters.py 2013-06-05 10:06:54 +0000
> @@ -365,13 +365,24 @@
>
> content_type_id = ContentType.objects.get_for_model(TestRun).id
>
> - for (name, value) in filter_data['attributes']:
> + for (name, equation, value) in filter_data['attributes']:
> # We punch through the generic relation abstraction here for 100x
> # better performance.
> - conditions.append(
> - models.Q(id__in=NamedAttribute.objects.filter(
> - name=name, value=value, content_type_id=content_type_id
> - ).values('object_id')))
> + if equation == '>=':
> + conditions.append(
> + models.Q(id__in=NamedAttribute.objects.filter(
> + name=name, value__gte=value,
> content_type_id=content_type_id
> + ).values('object_id')))
> + elif equation == '<=':
> + conditions.append(
> + models.Q(id__in=NamedAttribute.objects.filter(
> + name=name, value__lte=value,
> content_type_id=content_type_id
> + ).values('object_id')))
> + else:
> + conditions.append(
> + models.Q(id__in=NamedAttribute.objects.filter(
> + name=name, value=value,
> content_type_id=content_type_id
> + ).values('object_id')))
>
> I'd really like to see a more generic solution here... If we add more
> filter equations later, this code will have to be subjected to refactoring
> again and again. If we would have some kind of mapping like
> equation_map = {"<=": "lte" ....}
> we could use pyhon's argument packing, something like
> kwargs = {
> '{0}__{1}'.format('value', 'lte'): value,
> '{0}__{1}'.format('value', 'gte'): value
> }
>
> NamedAttribute.objects.filter(**kwargs)
>
>
> Second thing are the tests. I know the test coverage in dashboard is
> scarce, but do we want to keep it that way?
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

Revision history for this message
Arthur She (arthur-she) wrote :

Stevan, do you mean, use
    equation = models.CharField(
            choices=(
                ('lte', '<='),
                ('eq', '=='),
                ('gte', '>=')),
            max_length=3, default='==')
instead of
    equation = models.CharField(
            choices=(
                ('<=', '<='),
                ('==', '=='),
                ('>=', '>=')),
            max_length=3, default='==')
in the class TestRunFilterAttribute

2013/6/5 Stevan Radaković <email address hidden>

> One additional comment, we can circumvent the equation map altogether, if
> the db values for equation field would be defined as 'lte', 'gte'.. to
> start with. In the frontend we keep the '<=', '>=' of course...
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

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

Exactly like that!

On 06/06/2013 11:07 AM, Arthur She wrote:
> Stevan, do you mean, use
> equation = models.CharField(
> choices=(
> ('lte', '<='),
> ('eq', '=='),
> ('gte', '>=')),
> max_length=3, default='==')
> instead of
> equation = models.CharField(
> choices=(
> ('<=', '<='),
> ('==', '=='),
> ('>=', '>=')),
> max_length=3, default='==')
> in the class TestRunFilterAttribute
>
>
>
>
>
> 2013/6/5 Stevan Radaković <email address hidden>
>
>> One additional comment, we can circumvent the equation map altogether, if
>> the db values for equation field would be defined as 'lte', 'gte'.. to
>> start with. In the frontend we keep the '<=', '>=' of course...
>> --
>>
>> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
>> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>>

--
Stevan Radaković | LAVA Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs

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

> Hi Stevan,
> I know the idea, if we can make it more generic it should be better.
> As you mentioned, we can use a dictionary to map the equation "equation_map
> = {"<=": "lte", ">=": "gte", ""==": ""}"
> and use argument packing, but I still don't have idea how to associate them
> together. Could you talk more..
>
> And regarding the tests, Tyler suggested me to do such modification. Do you
> have any other idea to approach it?
>
> Thanks a lot.
> Arthur
>

You can take a look at the lp:linaro-ci-dashboard it's also a django application and has a lot of good example for model tests. If you need more info about it please feel free to ping me on irc
>
> 2013/6/5 Stevan Radaković <email address hidden>
>
> > Review: Needs Fixing code
> >
> > Arthur, great work!
> >
> > Couple of comments:
> >
> > === modified file 'dashboard_app/filters.py'
> > --- dashboard_app/filters.py 2013-01-09 00:15:10 +0000
> > +++ dashboard_app/filters.py 2013-06-05 10:06:54 +0000
> > @@ -365,13 +365,24 @@
> >
> > content_type_id = ContentType.objects.get_for_model(TestRun).id
> >
> > - for (name, value) in filter_data['attributes']:
> > + for (name, equation, value) in filter_data['attributes']:
> > # We punch through the generic relation abstraction here for 100x
> > # better performance.
> > - conditions.append(
> > - models.Q(id__in=NamedAttribute.objects.filter(
> > - name=name, value=value, content_type_id=content_type_id
> > - ).values('object_id')))
> > + if equation == '>=':
> > + conditions.append(
> > + models.Q(id__in=NamedAttribute.objects.filter(
> > + name=name, value__gte=value,
> > content_type_id=content_type_id
> > + ).values('object_id')))
> > + elif equation == '<=':
> > + conditions.append(
> > + models.Q(id__in=NamedAttribute.objects.filter(
> > + name=name, value__lte=value,
> > content_type_id=content_type_id
> > + ).values('object_id')))
> > + else:
> > + conditions.append(
> > + models.Q(id__in=NamedAttribute.objects.filter(
> > + name=name, value=value,
> > content_type_id=content_type_id
> > + ).values('object_id')))
> >
> > I'd really like to see a more generic solution here... If we add more
> > filter equations later, this code will have to be subjected to refactoring
> > again and again. If we would have some kind of mapping like
> > equation_map = {"<=": "lte" ....}
> > we could use pyhon's argument packing, something like
> > kwargs = {
> > '{0}__{1}'.format('value', 'lte'): value,
> > '{0}__{1}'.format('value', 'gte'): value
> > }
> >
> > NamedAttribute.objects.filter(**kwargs)
> >
> >
> > Second thing are the tests. I know the test coverage in dashboard is
> > scarce, but do we want to keep it that way?
> > --
> >
> > https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-
> bug-1175597/+merge/167494
> > You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
> >

Revision history for this message
Arthur She (arthur-she) wrote :
Download full text (3.5 KiB)

Thanks a lot, Stevan. :)

2013/6/6 Stevan Radaković <email address hidden>

> > Hi Stevan,
> > I know the idea, if we can make it more generic it should be better.
> > As you mentioned, we can use a dictionary to map the equation
> "equation_map
> > = {"<=": "lte", ">=": "gte", ""==": ""}"
> > and use argument packing, but I still don't have idea how to associate
> them
> > together. Could you talk more..
> >
> > And regarding the tests, Tyler suggested me to do such modification. Do
> you
> > have any other idea to approach it?
> >
> > Thanks a lot.
> > Arthur
> >
>
> You can take a look at the lp:linaro-ci-dashboard it's also a django
> application and has a lot of good example for model tests. If you need more
> info about it please feel free to ping me on irc
> >
> > 2013/6/5 Stevan Radaković <email address hidden>
> >
> > > Review: Needs Fixing code
> > >
> > > Arthur, great work!
> > >
> > > Couple of comments:
> > >
> > > === modified file 'dashboard_app/filters.py'
> > > --- dashboard_app/filters.py 2013-01-09 00:15:10 +0000
> > > +++ dashboard_app/filters.py 2013-06-05 10:06:54 +0000
> > > @@ -365,13 +365,24 @@
> > >
> > > content_type_id = ContentType.objects.get_for_model(TestRun).id
> > >
> > > - for (name, value) in filter_data['attributes']:
> > > + for (name, equation, value) in filter_data['attributes']:
> > > # We punch through the generic relation abstraction here for
> 100x
> > > # better performance.
> > > - conditions.append(
> > > - models.Q(id__in=NamedAttribute.objects.filter(
> > > - name=name, value=value,
> content_type_id=content_type_id
> > > - ).values('object_id')))
> > > + if equation == '>=':
> > > + conditions.append(
> > > + models.Q(id__in=NamedAttribute.objects.filter(
> > > + name=name, value__gte=value,
> > > content_type_id=content_type_id
> > > + ).values('object_id')))
> > > + elif equation == '<=':
> > > + conditions.append(
> > > + models.Q(id__in=NamedAttribute.objects.filter(
> > > + name=name, value__lte=value,
> > > content_type_id=content_type_id
> > > + ).values('object_id')))
> > > + else:
> > > + conditions.append(
> > > + models.Q(id__in=NamedAttribute.objects.filter(
> > > + name=name, value=value,
> > > content_type_id=content_type_id
> > > + ).values('object_id')))
> > >
> > > I'd really like to see a more generic solution here... If we add more
> > > filter equations later, this code will have to be subjected to
> refactoring
> > > again and again. If we would have some kind of mapping like
> > > equation_map = {"<=": "lte" ....}
> > > we could use pyhon's argument packing, something like
> > > kwargs = {
> > > '{0}__{1}'.format('value', 'lte'): value,
> > > '{0}__{1}'.format('value', 'gte'): value
> > > }
> > >
> > > NamedAttribute.objects.filter(**kwargs)
> > >
> > >
> > > Second thing are the tests. I know the test coverage in dashboard is
> > > scarce, but do we want t...

Read more...

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

Hey Arthur,

so instead of all the if/else business, code would look something like this (maybe not 100% accurate didn't try it in the codebase:)

kwargs = {
    name: name,
    '{0}__{1}'.format('value', equation): value,
    content_type_id=content_type_id
}

conditions.append(models.Q(id__in=NamedAttribute.objects.filter(**kwargs).values('object_id')))

I may be missing some quotes but you'll figure it out :)

Revision history for this message
Arthur She (arthur-she) wrote :

Thanks a lot for your code, Stevan.
I think I might misunderstand your meaning for the "tests".
I'll check the code again...

2013/6/7 Stevan Radaković <email address hidden>

> Hey Arthur,
>
> so instead of all the if/else business, code would look something like
> this (maybe not 100% accurate didn't try it in the codebase:)
>
> kwargs = {
> name: name,
> '{0}__{1}'.format('value', equation): value,
> content_type_id=content_type_id
> }
>
>
> conditions.append(models.Q(id__in=NamedAttribute.objects.filter(**kwargs).values('object_id')))
>
> I may be missing some quotes but you'll figure it out :)
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

406. By Arthur She

Modify it to make it more generic.

modified:
  dashboard_app/filters.py
  dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py
  dashboard_app/models.py
  dashboard_app/views/filters/forms.py

407. By Arthur She

use display value instead of db value

modified:
  dashboard_app/models.py
  dashboard_app/templates/dashboard_app/filter_summary.html

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

I've also noticed we're missing the "Attributes 'filter_name' <= 'value' " sting on the filter preview page, please add this as well.

408. By Arthur She

Show attributes on filter preview page.

modified:
  dashboard_app/templates/dashboard_app/filter_preview.html
  dashboard_app/views/filters/forms.py

Revision history for this message
Arthur She (arthur-she) wrote :

Thanks a lot for your help, I've modified it and pushed to the server.

2013/6/18 Stevan Radaković <email address hidden>

> I've also noticed we're missing the "Attributes 'filter_name' <= 'value' "
> sting on the filter preview page, please add this as well.
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

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

Ok this looks like good to go now.
Thanks Arthur.
Approve +1.

review: Approve
Revision history for this message
Arthur She (arthur-she) wrote :

Thanks, Stevan :)

2013/6/24 Stevan Radaković <email address hidden>

> The proposal to merge lp:~arthur-she/lava-dashboard/fix-for-bug-1175597
> into lp:lava-dashboard has been updated.
>
> Status: Needs review => Approved
>
> For more details, see:
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> --
>
> https://code.launchpad.net/~arthur-she/lava-dashboard/fix-for-bug-1175597/+merge/167494
> You are the owner of lp:~arthur-she/lava-dashboard/fix-for-bug-1175597.
>

Unmerged revisions

408. By Arthur She

Show attributes on filter preview page.

modified:
  dashboard_app/templates/dashboard_app/filter_preview.html
  dashboard_app/views/filters/forms.py

407. By Arthur She

use display value instead of db value

modified:
  dashboard_app/models.py
  dashboard_app/templates/dashboard_app/filter_summary.html

406. By Arthur She

Modify it to make it more generic.

modified:
  dashboard_app/filters.py
  dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py
  dashboard_app/models.py
  dashboard_app/views/filters/forms.py

405. By Arthur She

fix for bug #1175597

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dashboard_app/filters.py'
2--- dashboard_app/filters.py 2013-01-09 00:15:10 +0000
3+++ dashboard_app/filters.py 2013-06-19 08:00:38 +0000
4@@ -365,13 +365,15 @@
5
6 content_type_id = ContentType.objects.get_for_model(TestRun).id
7
8- for (name, value) in filter_data['attributes']:
9+ for (name, equation, value) in filter_data['attributes']:
10 # We punch through the generic relation abstraction here for 100x
11 # better performance.
12- conditions.append(
13- models.Q(id__in=NamedAttribute.objects.filter(
14- name=name, value=value, content_type_id=content_type_id
15- ).values('object_id')))
16+ kwargs = {
17+ 'name': name,
18+ '{0}__{1}'.format('value', equation): value,
19+ 'content_type_id': content_type_id
20+ }
21+ conditions.append(models.Q(id__in=NamedAttribute.objects.filter(**kwargs).values('object_id')))
22
23 test_condition = None
24 for test in filter_data.get('tests', []):
25
26=== added file 'dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py'
27--- dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py 1970-01-01 00:00:00 +0000
28+++ dashboard_app/migrations/0030_auto__add_equation_to_testrunfilterattribute.py 2013-06-19 08:00:38 +0000
29@@ -0,0 +1,270 @@
30+# -*- coding: utf-8 -*-
31+import datetime
32+from south.db import db
33+from south.v2 import SchemaMigration
34+from django.db import models
35+
36+
37+class Migration(SchemaMigration):
38+
39+ def forwards(self, orm):
40+ # Adding field 'TestRunFilterAttribute.equation'
41+ db.add_column('dashboard_app_testrunfilterattribute', 'equation',
42+ self.gf('django.db.models.fields.CharField')(default='exact', max_length=6),
43+ keep_default=False)
44+
45+
46+ def backwards(self, orm):
47+ # Deleting field 'TestRunFilterAttribute.equation'
48+ db.delete_column('dashboard_app_testrunfilterattribute', 'equation')
49+
50+
51+ models = {
52+ 'auth.group': {
53+ 'Meta': {'object_name': 'Group'},
54+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
56+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
57+ },
58+ 'auth.permission': {
59+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
60+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
62+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
63+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
64+ },
65+ 'auth.user': {
66+ 'Meta': {'object_name': 'User'},
67+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
68+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
69+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
70+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
71+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
72+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
73+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
74+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
75+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
76+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
77+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
78+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
79+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
80+ },
81+ 'contenttypes.contenttype': {
82+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
83+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
84+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
85+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
86+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
87+ },
88+ 'dashboard_app.attachment': {
89+ 'Meta': {'object_name': 'Attachment'},
90+ 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
91+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
92+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
93+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
94+ 'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
95+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
96+ 'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
97+ },
98+ 'dashboard_app.bundle': {
99+ 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
100+ '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
101+ '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
102+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
103+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
104+ 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
105+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
106+ 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
107+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
108+ 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
109+ },
110+ 'dashboard_app.bundledeserializationerror': {
111+ 'Meta': {'object_name': 'BundleDeserializationError'},
112+ 'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
113+ 'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
114+ 'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
115+ },
116+ 'dashboard_app.bundlestream': {
117+ 'Meta': {'object_name': 'BundleStream'},
118+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
119+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
120+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
121+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
122+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
123+ 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
124+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
125+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
126+ },
127+ 'dashboard_app.hardwaredevice': {
128+ 'Meta': {'object_name': 'HardwareDevice'},
129+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
130+ 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
131+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
132+ },
133+ 'dashboard_app.image': {
134+ 'Meta': {'object_name': 'Image'},
135+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['dashboard_app.TestRunFilter']"}),
136+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
137+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
138+ },
139+ 'dashboard_app.imageset': {
140+ 'Meta': {'object_name': 'ImageSet'},
141+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142+ 'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
143+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
144+ },
145+ 'dashboard_app.launchpadbug': {
146+ 'Meta': {'object_name': 'LaunchpadBug'},
147+ 'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
148+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
149+ 'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'launchpad_bugs'", 'symmetrical': 'False', 'to': "orm['dashboard_app.TestRun']"})
150+ },
151+ 'dashboard_app.namedattribute': {
152+ 'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
153+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
154+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
155+ 'name': ('django.db.models.fields.TextField', [], {}),
156+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
157+ 'value': ('django.db.models.fields.TextField', [], {})
158+ },
159+ 'dashboard_app.pmqabundlestream': {
160+ 'Meta': {'object_name': 'PMQABundleStream'},
161+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.BundleStream']"}),
162+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
163+ },
164+ 'dashboard_app.softwarepackage': {
165+ 'Meta': {'unique_together': "(('name', 'version'),)", 'object_name': 'SoftwarePackage'},
166+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
167+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
168+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
169+ },
170+ 'dashboard_app.softwarepackagescratch': {
171+ 'Meta': {'object_name': 'SoftwarePackageScratch'},
172+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
173+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
174+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
175+ },
176+ 'dashboard_app.softwaresource': {
177+ 'Meta': {'object_name': 'SoftwareSource'},
178+ 'branch_revision': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
179+ 'branch_url': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
180+ 'branch_vcs': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
181+ 'commit_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
182+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
183+ 'project_name': ('django.db.models.fields.CharField', [], {'max_length': '32'})
184+ },
185+ 'dashboard_app.tag': {
186+ 'Meta': {'object_name': 'Tag'},
187+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
188+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '256'})
189+ },
190+ 'dashboard_app.test': {
191+ 'Meta': {'object_name': 'Test'},
192+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
193+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
194+ 'test_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
195+ },
196+ 'dashboard_app.testcase': {
197+ 'Meta': {'unique_together': "(('test', 'test_case_id'),)", 'object_name': 'TestCase'},
198+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
199+ 'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
200+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_cases'", 'to': "orm['dashboard_app.Test']"}),
201+ 'test_case_id': ('django.db.models.fields.TextField', [], {}),
202+ 'units': ('django.db.models.fields.TextField', [], {'blank': 'True'})
203+ },
204+ 'dashboard_app.testdefinition': {
205+ 'Meta': {'object_name': 'TestDefinition'},
206+ 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
207+ 'description': ('django.db.models.fields.TextField', [], {}),
208+ 'environment': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
209+ 'format': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
210+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
211+ 'location': ('django.db.models.fields.CharField', [], {'default': "'LOCAL'", 'max_length': '64'}),
212+ 'mime_type': ('django.db.models.fields.CharField', [], {'default': "'text/plain'", 'max_length': '64'}),
213+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
214+ 'target_dev_types': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
215+ 'target_os': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
216+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
217+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '256'})
218+ },
219+ 'dashboard_app.testresult': {
220+ 'Meta': {'ordering': "('_order',)", 'object_name': 'TestResult'},
221+ '_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
222+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
223+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
224+ 'lineno': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
225+ 'measurement': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '20', 'decimal_places': '10', 'blank': 'True'}),
226+ 'message': ('django.db.models.fields.TextField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
227+ 'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
228+ 'relative_index': ('django.db.models.fields.PositiveIntegerField', [], {}),
229+ 'result': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
230+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'test_results'", 'null': 'True', 'to': "orm['dashboard_app.TestCase']"}),
231+ 'test_run': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_results'", 'to': "orm['dashboard_app.TestRun']"}),
232+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
233+ },
234+ 'dashboard_app.testrun': {
235+ 'Meta': {'ordering': "['-import_assigned_date']", 'object_name': 'TestRun'},
236+ 'analyzer_assigned_date': ('django.db.models.fields.DateTimeField', [], {}),
237+ 'analyzer_assigned_uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}),
238+ 'bundle': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Bundle']"}),
239+ 'devices': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.HardwareDevice']"}),
240+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241+ 'import_assigned_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
242+ 'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
243+ 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwarePackage']"}),
244+ 'sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwareSource']"}),
245+ 'sw_image_desc': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
246+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.Tag']"}),
247+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Test']"}),
248+ 'time_check_performed': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
249+ },
250+ 'dashboard_app.testrundenormalization': {
251+ 'Meta': {'object_name': 'TestRunDenormalization'},
252+ 'count_fail': ('django.db.models.fields.PositiveIntegerField', [], {}),
253+ 'count_pass': ('django.db.models.fields.PositiveIntegerField', [], {}),
254+ 'count_skip': ('django.db.models.fields.PositiveIntegerField', [], {}),
255+ 'count_unknown': ('django.db.models.fields.PositiveIntegerField', [], {}),
256+ 'test_run': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'denormalization'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.TestRun']"})
257+ },
258+ 'dashboard_app.testrunfilter': {
259+ 'Meta': {'unique_together': "(('owner', 'name'),)", 'object_name': 'TestRunFilter'},
260+ 'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
261+ 'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
262+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
263+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '1024'}),
264+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
265+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
266+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"})
267+ },
268+ 'dashboard_app.testrunfilterattribute': {
269+ 'Meta': {'object_name': 'TestRunFilterAttribute'},
270+ 'equation': ('django.db.models.fields.CharField', [], {'default': "'exact'", 'max_length': '6'}),
271+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['dashboard_app.TestRunFilter']"}),
272+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
273+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
274+ 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
275+ },
276+ 'dashboard_app.testrunfiltersubscription': {
277+ 'Meta': {'unique_together': "(('user', 'filter'),)", 'object_name': 'TestRunFilterSubscription'},
278+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']"}),
279+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
280+ 'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
281+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
282+ },
283+ 'dashboard_app.testrunfiltertest': {
284+ 'Meta': {'object_name': 'TestRunFilterTest'},
285+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tests'", 'to': "orm['dashboard_app.TestRunFilter']"}),
286+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
288+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.Test']"})
289+ },
290+ 'dashboard_app.testrunfiltertestcase': {
291+ 'Meta': {'object_name': 'TestRunFilterTestCase'},
292+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
293+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
294+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cases'", 'to': "orm['dashboard_app.TestRunFilterTest']"}),
295+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.TestCase']"})
296+ }
297+ }
298+
299+ complete_apps = ['dashboard_app']
300\ No newline at end of file
301
302=== modified file 'dashboard_app/models.py'
303--- dashboard_app/models.py 2013-04-01 08:50:05 +0000
304+++ dashboard_app/models.py 2013-06-19 08:00:38 +0000
305@@ -1566,11 +1566,17 @@
306
307 name = models.CharField(max_length=1024)
308 value = models.CharField(max_length=1024)
309+ equation = models.CharField(
310+ choices=(
311+ ('lte', '<='),
312+ ('exact', '=='),
313+ ('gte', '>=')),
314+ max_length=6, default='exact')
315
316 filter = models.ForeignKey("TestRunFilter", related_name="attributes")
317
318 def __unicode__(self):
319- return '%s = %s' % (self.name, self.value)
320+ return '%s %s %s' % (self.name, self.get_equation_display(), self.value)
321
322
323 class TestRunFilterTest(models.Model):
324@@ -1633,9 +1639,14 @@
325 'test': trftest.test,
326 'test_cases': [trftestcase.test_case for trftestcase in trftest.cases.all().select_related('test_case')],
327 })
328+ att = []
329+ for a in self.attributes.all():
330+ b = (a.name, a.get_equation_display(), a.value)
331+ att.append(b)
332 return {
333 'bundle_streams': self.bundle_streams.all(),
334- 'attributes': self.attributes.all().values_list('name', 'value'),
335+ 'attributes': self.attributes.all().values_list('name', 'equation', 'value'),
336+ 'attr_display': att,
337 'tests': tests,
338 'build_number_attribute': self.build_number_attribute,
339 'uploaded_by': self.uploaded_by,
340
341=== modified file 'dashboard_app/templates/dashboard_app/filter_form.html'
342--- dashboard_app/templates/dashboard_app/filter_form.html 2012-09-19 22:56:03 +0000
343+++ dashboard_app/templates/dashboard_app/filter_form.html 2013-06-19 08:00:38 +0000
344@@ -42,6 +42,9 @@
345 Name
346 </th>
347 <th>
348+ Equation
349+ </th>
350+ <th>
351 Value
352 </th>
353 </tr>
354@@ -54,6 +57,9 @@
355 {{ form.name }}
356 </td>
357 <td class="value">
358+ {{ form.equation }}
359+ </td>
360+ <td class="value">
361 {{ form.value }}
362 </td>
363 <td>
364@@ -70,6 +76,9 @@
365 <td class="name">
366 {{ form.name }}
367 </td>
368+ <td class="equation">
369+ {{ form.equation }}
370+ </td>
371 <td class="value">
372 {{ form.value }}
373 </td>
374
375=== modified file 'dashboard_app/templates/dashboard_app/filter_preview.html'
376--- dashboard_app/templates/dashboard_app/filter_preview.html 2013-01-09 00:05:14 +0000
377+++ dashboard_app/templates/dashboard_app/filter_preview.html 2013-06-19 08:00:38 +0000
378@@ -14,7 +14,7 @@
379 <h1>Previewing new filter &ldquo;{{ form.name.value }}&rdquo;</h1>
380 {% endif %}
381
382-{% include "dashboard_app/filter_summary.html" with summary_data=filter.as_data %}
383+{% include "dashboard_app/filter_summary.html" with filter_data=form.as_data %}
384
385 <p>
386 These are the results matched by your filter.
387
388=== modified file 'dashboard_app/templates/dashboard_app/filter_summary.html'
389--- dashboard_app/templates/dashboard_app/filter_summary.html 2012-12-12 23:49:01 +0000
390+++ dashboard_app/templates/dashboard_app/filter_summary.html 2013-06-19 08:00:38 +0000
391@@ -9,14 +9,14 @@
392 {% endfor %}
393 </td>
394 </tr>
395-{% if filter_data.attributes %}
396+{% if filter_data.attr_display %}
397 <tr>
398 <th>
399 Attributes
400 </th>
401 <td>
402- {% for a in filter_data.attributes %}
403- {{ a.0 }} == {{ a.1 }} <br />
404+ {% for a in filter_data.attr_display %}
405+ {{ a.0 }} {{ a.1 }} {{ a.2 }} <br />
406 {% endfor %}
407 </td>
408 </tr>
409
410=== modified file 'dashboard_app/views/filters/forms.py'
411--- dashboard_app/views/filters/forms.py 2013-01-09 00:14:58 +0000
412+++ dashboard_app/views/filters/forms.py 2013-06-19 08:00:38 +0000
413@@ -63,6 +63,11 @@
414 class AttributesForm(forms.Form):
415 name = forms.CharField(max_length=1024)
416 value = forms.CharField(max_length=1024)
417+ equation = forms.ChoiceField(
418+ choices=[
419+ ('lte', '<='),
420+ ('exact', '=='),
421+ ('gte', '>=')])
422
423 AttributesFormSet = formset_factory(AttributesForm, extra=0)
424
425@@ -170,7 +175,7 @@
426 if commit:
427 instance.attributes.all().delete()
428 for a in self.attributes_formset.cleaned_data:
429- instance.attributes.create(name=a['name'], value=a['value'])
430+ instance.attributes.create(name=a['name'], equation=a['equation'], value=a['value'])
431 instance.tests.all().delete()
432 for i, test_form in enumerate(self.tests_formset.forms):
433 trf_test = instance.tests.create(
434@@ -201,8 +206,15 @@
435 tc_form.cleaned_data['test_case']
436 for tc_form in form.test_case_formset]
437 })
438+ att = []
439+ for form in self.attributes_formset.forms:
440+ att.append((
441+ form.cleaned_data['name'],
442+ dict(form.fields['equation'].choices)[form.cleaned_data['equation']],
443+ form.cleaned_data['value']))
444+ data['attr_display'] = att
445 data['attributes'] = [
446- (d['name'], d['value']) for d in self.attributes_formset.cleaned_data]
447+ (d['name'], d['equation'], d['value']) for d in self.attributes_formset.cleaned_data]
448 data['tests'] = tests
449 data['uploaded_by'] = None
450 return data
451@@ -218,6 +230,7 @@
452 for attr in self.instance.attributes.all():
453 initial.append({
454 'name': attr.name,
455+ 'equation': attr.equation,
456 'value': attr.value,
457 })
458 attr_set_args['initial'] = initial

Subscribers

People subscribed via source and target branches