diff --git a/.coveragerc b/.coveragerc
index aeca2a7b9b..7433c3ad15 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -7,6 +7,7 @@ omit =
rdmo/settings/*
apps/core/management/commands/*
env/*
+ */migrations/*
exclude_lines =
raise Exception
except ImportError:
diff --git a/apps/accounts/testing/__init__.py b/apps/accounts/testing/__init__.py
deleted file mode 100644
index 8baa6e5aff..0000000000
--- a/apps/accounts/testing/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .tests import *
diff --git a/apps/accounts/testing/factories.py b/apps/accounts/testing/factories.py
deleted file mode 100644
index 5720db8e61..0000000000
--- a/apps/accounts/testing/factories.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from django.contrib.auth.models import User
-
-from factory.django import DjangoModelFactory
-
-from ..models import *
-
-
-class UserFactory(DjangoModelFactory):
-
- class Meta:
- model = User
-
- username = 'user'
- password = 'user'
-
- first_name = 'Ulf'
- last_name = 'User'
-
- email = 'user@example.com'
-
- @classmethod
- def _create(cls, model_class, *args, **kwargs):
- manager = cls._get_manager(model_class)
- return manager.create_user(*args, **kwargs)
-
-
-class ManagerFactory(UserFactory):
-
- username = 'manager'
- password = 'manager'
-
- first_name = 'Manni'
- last_name = 'Manager'
-
- email = 'manager@example.com'
-
-
-class AdminFactory(UserFactory):
-
- username = 'admin'
- password = 'admin'
-
- first_name = 'Albert'
- last_name = 'Admin'
-
- email = 'admin@example.com'
-
- is_staff = True
- is_superuser = True
-
-
-class AdditionalFieldFactory(DjangoModelFactory):
-
- class Meta:
- model = AdditionalField
-
-
-class AdditionalTextFieldFactory(AdditionalFieldFactory):
-
- type = 'text'
- help_en = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr'
- help_de = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr'
- text_en = 'text'
- text_de = 'text'
- key = 'text'
- required = True
-
-
-class AdditionalTextareaFieldFactory(AdditionalFieldFactory):
-
- type = 'textarea'
- help_en = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr'
- help_de = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr'
- text_en = 'textarea'
- text_de = 'textarea'
- key = 'textarea'
- required = True
diff --git a/apps/accounts/testing/tests.py b/apps/accounts/tests.py
similarity index 93%
rename from apps/accounts/testing/tests.py
rename to apps/accounts/tests.py
index 9477353e1d..070556af84 100644
--- a/apps/accounts/testing/tests.py
+++ b/apps/accounts/tests.py
@@ -1,22 +1,27 @@
import re
+from django.contrib.auth.models import User
from django.test import TestCase
-from django.core import mail
-from django.core.urlresolvers import reverse
from django.utils import translation
+from django.core.urlresolvers import reverse
+from django.core import mail
from apps.core.testing.mixins import TestModelStringMixin
-from .factories import *
+class AccountsTestCase(TestCase):
-class ProfileTests(TestModelStringMixin, TestCase):
+ fixtures = (
+ 'auth.json',
+ 'accounts.json'
+ )
+
+
+class ProfileTests(TestModelStringMixin, AccountsTestCase):
def setUp(self):
translation.activate('en')
- AdditionalTextFieldFactory()
- AdditionalTextareaFieldFactory()
- self.instance = UserFactory()
+ self.instances = User.objects.all()
def test_get_profile_update(self):
"""
@@ -124,19 +129,18 @@ def test_post_profile_update_next2(self):
self.assertRedirects(response, reverse('home'), target_status_code=302)
-class AdditionalFieldTests(TestModelStringMixin, TestCase):
+class AdditionalFieldTests(TestModelStringMixin, AccountsTestCase):
def setUp(self):
translation.activate('en')
- self.instance = AdditionalTextFieldFactory()
+ self.instances = User.objects.all()
-class PasswordTests(TestCase):
+class PasswordTests(AccountsTestCase):
def setUp(self):
- UserFactory()
-
translation.activate('en')
+ self.instances = User.objects.all()
def test_password_change_get(self):
"""
diff --git a/apps/conditions/admin.py b/apps/conditions/admin.py
index 2432294851..dae51b9b81 100644
--- a/apps/conditions/admin.py
+++ b/apps/conditions/admin.py
@@ -1,5 +1,10 @@
from django.contrib import admin
-from .models import *
+from .models import Condition
-admin.site.register(Condition)
+
+class ConditionAdmin(admin.ModelAdmin):
+ readonly_fields = ('uri', )
+
+
+admin.site.register(Condition, ConditionAdmin)
diff --git a/apps/conditions/migrations/0010_refactoring.py b/apps/conditions/migrations/0010_refactoring.py
new file mode 100644
index 0000000000..e2778fb443
--- /dev/null
+++ b/apps/conditions/migrations/0010_refactoring.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-01-25 13:48
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('conditions', '0009_options'),
+ ('options', '0006_refactoring'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='condition',
+ options={'ordering': ('key',), 'verbose_name': 'Condition', 'verbose_name_plural': 'Conditions'},
+ ),
+ migrations.RenameField(
+ model_name='condition',
+ old_name='description',
+ new_name='comment',
+ ),
+ migrations.RenameField(
+ model_name='condition',
+ old_name='title',
+ new_name='key',
+ ),
+ ]
diff --git a/apps/conditions/migrations/0011_refactoring.py b/apps/conditions/migrations/0011_refactoring.py
new file mode 100644
index 0000000000..4f133e9e52
--- /dev/null
+++ b/apps/conditions/migrations/0011_refactoring.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-01-25 13:56
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('conditions', '0010_refactoring'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='condition',
+ options={'ordering': ('uri',), 'verbose_name': 'Condition', 'verbose_name_plural': 'Conditions'},
+ ),
+ migrations.AddField(
+ model_name='condition',
+ name='uri',
+ field=models.URLField(blank=True, help_text='The Uniform Resource Identifier of this option set (auto-generated).', max_length=640, null=True, verbose_name='URI'),
+ ),
+ migrations.AddField(
+ model_name='condition',
+ name='uri_prefix',
+ field=models.URLField(blank=True, help_text='The prefix for the URI of this condition.', max_length=256, null=True, verbose_name='URI Prefix'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='comment',
+ field=models.TextField(blank=True, help_text='Additional information about this condition.', null=True, verbose_name='Comment'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='key',
+ field=models.SlugField(blank=True, help_text='The internal identifier of this condition. The URI will be generated from this key.', max_length=128, null=True, verbose_name='Key'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='relation',
+ field=models.CharField(choices=[('eq', 'is equal to (==)'), ('neq', 'is not equal to (!=)'), ('contains', 'contains'), ('gt', 'is greater than (>)'), ('gte', 'is greater than or equal (>=)'), ('lt', 'is lesser than (<)'), ('lte', 'is lesser than or equal (<=)'), ('empty', 'is empty'), ('notempty', 'is not empty')], help_text='Relation this condition is using.', max_length=8, verbose_name='Relation'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='source',
+ field=models.ForeignKey(blank=True, db_constraint=False, help_text='Attribute this condition is evaluating.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='domain.Attribute', verbose_name='Source'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='target_option',
+ field=models.ForeignKey(blank=True, db_constraint=False, help_text='Option this condition is checking against.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='options.Option', verbose_name='Target (Option)'),
+ ),
+ migrations.AlterField(
+ model_name='condition',
+ name='target_text',
+ field=models.CharField(blank=True, help_text='Raw text value this condition is checking against.', max_length=256, null=True, verbose_name='Target (Text)'),
+ ),
+ ]
diff --git a/apps/conditions/models.py b/apps/conditions/models.py
index b9e83d86bb..49354b4401 100644
--- a/apps/conditions/models.py
+++ b/apps/conditions/models.py
@@ -1,10 +1,13 @@
from __future__ import unicode_literals
from django.db import models
-from django.core.validators import RegexValidator
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
+from apps.core.utils import get_uri_prefix
+
+from .validators import ConditionUniqueKeyValidator
+
@python_2_unicode_compatible
class Condition(models.Model):
@@ -30,28 +33,61 @@ class Condition(models.Model):
(RELATION_NOT_EMPTY, 'is not empty'),
)
- title = models.CharField(max_length=256, validators=[
- RegexValidator('^[a-zA-z0-9_]*$', _('Only letters, numbers, or underscores are allowed.'))
- ])
- description = models.TextField(blank=True, null=True)
-
- source = models.ForeignKey('domain.Attribute', db_constraint=False, blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
- relation = models.CharField(max_length=8, choices=RELATION_CHOICES)
-
- target_text = models.CharField(max_length=256, blank=True, null=True)
- target_option = models.ForeignKey('options.Option', db_constraint=False, blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
+ uri = models.URLField(
+ max_length=640, blank=True, null=True,
+ verbose_name=_('URI'),
+ help_text=_('The Uniform Resource Identifier of this option set (auto-generated).')
+ )
+ uri_prefix = models.URLField(
+ max_length=256, blank=True, null=True,
+ verbose_name=_('URI Prefix'),
+ help_text=_('The prefix for the URI of this condition.')
+ )
+ key = models.SlugField(
+ max_length=128, blank=True, null=True,
+ verbose_name=_('Key'),
+ help_text=_('The internal identifier of this condition. The URI will be generated from this key.')
+ )
+ comment = models.TextField(
+ blank=True, null=True,
+ verbose_name=_('Comment'),
+ help_text=_('Additional information about this condition.')
+ )
+ source = models.ForeignKey(
+ 'domain.Attribute', db_constraint=False, blank=True, null=True, on_delete=models.SET_NULL, related_name='+',
+ verbose_name=_('Source'),
+ help_text=_('Attribute this condition is evaluating.')
+ )
+ relation = models.CharField(
+ max_length=8, choices=RELATION_CHOICES,
+ verbose_name=_('Relation'),
+ help_text=_('Relation this condition is using.')
+ )
+ target_text = models.CharField(
+ max_length=256, blank=True, null=True,
+ verbose_name=_('Target (Text)'),
+ help_text=_('Raw text value this condition is checking against.')
+ )
+ target_option = models.ForeignKey(
+ 'options.Option', db_constraint=False, blank=True, null=True, on_delete=models.SET_NULL, related_name='+',
+ verbose_name=_('Target (Option)'),
+ help_text=_('Option this condition is checking against.')
+ )
class Meta:
- ordering = ('title', )
+ ordering = ('uri', )
verbose_name = _('Condition')
verbose_name_plural = _('Conditions')
def __str__(self):
- return self.title
+ return self.uri or self.key
+
+ def clean(self):
+ ConditionUniqueKeyValidator(self).validate()
@property
- def source_label(self):
- return self.source.label
+ def source_path(self):
+ return self.source.path
@property
def relation_label(self):
@@ -64,6 +100,13 @@ def target_label(self):
else:
return self.target_text
+ def save(self, *args, **kwargs):
+ self.uri = self.build_uri()
+ super(Condition, self).save(*args, **kwargs)
+
+ def build_uri(self):
+ return get_uri_prefix(self) + '/conditions/' + self.key
+
def resolve(self, project, snapshot=None):
# get the values for the given project, the given snapshot and the condition's attribute
values = project.values.filter(snapshot=snapshot).filter(attribute=self.source)
diff --git a/apps/conditions/renderers.py b/apps/conditions/renderers.py
new file mode 100644
index 0000000000..3374af8fe3
--- /dev/null
+++ b/apps/conditions/renderers.py
@@ -0,0 +1,24 @@
+from apps.core.renderers import BaseXMLRenderer
+
+
+class XMLRenderer(BaseXMLRenderer):
+
+ def render_document(self, xml, conditions):
+ xml.startElement('conditions', {
+ 'xmlns:dc': "http://purl.org/dc/elements/1.1/"
+ })
+
+ for condition in conditions:
+ self.render_condition(xml, condition)
+
+ xml.endElement('conditions')
+
+ def render_condition(self, xml, condition):
+ xml.startElement('condition', {})
+ self.render_text_element(xml, 'dc:uri', {}, condition["uri"])
+ self.render_text_element(xml, 'dc:comment', {}, condition["comment"])
+ self.render_text_element(xml, 'source', {'dc:uri': condition["source"]}, None)
+ self.render_text_element(xml, 'relation', {}, condition["relation"])
+ self.render_text_element(xml, 'target_text', {}, condition["target_text"])
+ self.render_text_element(xml, 'target_option', {'dc:uri': condition["target_option"]}, None)
+ xml.endElement('condition')
diff --git a/apps/conditions/serializers.py b/apps/conditions/serializers.py
index 40277c43eb..790b3a3cca 100644
--- a/apps/conditions/serializers.py
+++ b/apps/conditions/serializers.py
@@ -3,7 +3,8 @@
from apps.domain.models import Attribute
from apps.options.models import OptionSet, Option
-from .models import *
+from .models import Condition
+from .validators import ConditionUniqueKeyValidator
class ConditionIndexSerializer(serializers.ModelSerializer):
@@ -12,9 +13,9 @@ class Meta:
model = Condition
fields = (
'id',
- 'title',
- 'description',
- 'source_label',
+ 'key',
+ 'comment',
+ 'source_path',
'relation_label',
'target_label'
)
@@ -26,13 +27,15 @@ class Meta:
model = Condition
fields = (
'id',
- 'title',
- 'description',
+ 'uri_prefix',
+ 'key',
+ 'comment',
'source',
'relation',
'target_text',
'target_option'
)
+ validators = (ConditionUniqueKeyValidator(), )
class AttributeOptionSerializer(serializers.ModelSerializer):
@@ -54,7 +57,7 @@ class Meta:
model = Attribute
fields = (
'id',
- 'label',
+ 'path',
'options'
)
@@ -81,3 +84,20 @@ class Meta:
'order',
'options'
)
+
+
+class ExportSerializer(serializers.ModelSerializer):
+
+ source = serializers.CharField(source='source.uri')
+ target_option = serializers.CharField(source='target_option.uri')
+
+ class Meta:
+ model = Condition
+ fields = (
+ 'uri',
+ 'comment',
+ 'source',
+ 'relation',
+ 'target_text',
+ 'target_option'
+ )
diff --git a/apps/conditions/templates/conditions/conditions.html b/apps/conditions/templates/conditions/conditions.html
index 228d2a05fe..e420739052 100644
--- a/apps/conditions/templates/conditions/conditions.html
+++ b/apps/conditions/templates/conditions/conditions.html
@@ -51,6 +51,14 @@
{% trans 'Export' %}
{% endfor %}
+
+
{% endblock %}
{% block page %}
@@ -72,12 +80,12 @@ {% trans 'Conditions' %}
{% trans 'Condition' %}
- {$ condition.title $}
+ {$ condition.key $}
-
-
{$ condition.source_label $}
+ {$ condition.source_path $}
{$ condition.relation_label $}
"{$ condition.target_label $}"
diff --git a/apps/conditions/templates/conditions/conditions_export.html b/apps/conditions/templates/conditions/conditions_export.html
index 4db2ed5a77..4ac76c3a7a 100644
--- a/apps/conditions/templates/conditions/conditions_export.html
+++ b/apps/conditions/templates/conditions/conditions_export.html
@@ -18,7 +18,7 @@ {{ condition.title }}
- {% trans 'Source' %} {{ condition.source_label }}
+ {% trans 'Source' %} {{ condition.source_path }}
diff --git a/apps/conditions/templates/conditions/conditions_modal_form_conditions.html b/apps/conditions/templates/conditions/conditions_modal_form_conditions.html
index af88b29c81..1ea124ee39 100644
--- a/apps/conditions/templates/conditions/conditions_modal_form_conditions.html
+++ b/apps/conditions/templates/conditions/conditions_modal_form_conditions.html
@@ -13,19 +13,30 @@
+
+
+
+
+
+
-
-
+ data-label="{% trans 'Comment' %}"
+ data-model="service.values.comment"
+ data-errors="service.errors.comment">
data-model="service.values.source"
data-errors="service.errors.source"
data-options="service.attributes"
- data-options-label="label"
+ data-options-label="path"
data-options-null="1">
[a-z]+)/$', conditions_export, name='conditions_export'),
]
diff --git a/apps/conditions/urls_api.py b/apps/conditions/urls_api.py
index 035525ed63..7d67f52f4d 100644
--- a/apps/conditions/urls_api.py
+++ b/apps/conditions/urls_api.py
@@ -2,7 +2,12 @@
from rest_framework import routers
-from .views import *
+from .views import (
+ ConditionViewSet,
+ AttributeViewSet,
+ OptionSetViewSet,
+ RelationViewSet
+)
router = routers.DefaultRouter()
router.register(r'conditions', ConditionViewSet, base_name='condition')
diff --git a/apps/conditions/utils.py b/apps/conditions/utils.py
new file mode 100644
index 0000000000..13c3527bb4
--- /dev/null
+++ b/apps/conditions/utils.py
@@ -0,0 +1,42 @@
+from apps.core.utils import get_ns_tag
+from apps.domain.models import Attribute
+from apps.options.models import Option
+
+from .models import Condition
+
+
+def import_conditions(conditions_node):
+
+ nsmap = conditions_node.nsmap
+
+ for condition_node in conditions_node.iterchildren():
+ condition_uri = condition_node[get_ns_tag('dc:uri', nsmap)].text
+
+ try:
+ condition = Condition.objects.get(uri=condition_uri)
+ except Condition.DoesNotExist:
+ condition = Condition()
+
+ condition.uri_prefix = condition_uri.split('/conditions/')[0]
+ condition.key = condition_uri.split('/')[-1]
+ condition.comment = condition_node[get_ns_tag('dc:comment', nsmap)]
+ condition.relation = condition_node['relation']
+
+ try:
+ source_uri = condition_node['source'].get(get_ns_tag('dc:uri', nsmap))
+ condition.source = Attribute.objects.get(uri=source_uri)
+ except (AttributeError, Attribute.DoesNotExist):
+ condition.source = None
+
+ try:
+ condition.target_text = condition_node['target_text']
+ except AttributeError:
+ condition.target_text = None
+
+ try:
+ option_uid = condition_node['target_option'].get(get_ns_tag('dc:uri', nsmap))
+ condition.target_option = Option.objects.get(uri=option_uid)
+ except (AttributeError, Option.DoesNotExist):
+ condition.target_option = None
+
+ condition.save()
diff --git a/apps/conditions/validators.py b/apps/conditions/validators.py
new file mode 100644
index 0000000000..cd0483e600
--- /dev/null
+++ b/apps/conditions/validators.py
@@ -0,0 +1,7 @@
+from apps.core.validators import UniqueKeyValidator
+
+
+class ConditionUniqueKeyValidator(UniqueKeyValidator):
+
+ app_label = 'conditions'
+ model_name = 'condition'
diff --git a/apps/conditions/views.py b/apps/conditions/views.py
index a0437f15ae..bf24699f07 100644
--- a/apps/conditions/views.py
+++ b/apps/conditions/views.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.http import HttpResponse
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
@@ -16,8 +17,15 @@
from apps.options.models import OptionSet
from apps.projects.models import Snapshot
-from .models import *
-from .serializers import *
+from .models import Condition
+from .serializers import (
+ ConditionSerializer,
+ ConditionIndexSerializer,
+ AttributeSerializer,
+ OptionSetSerializer,
+ ExportSerializer
+)
+from .renderers import XMLRenderer
@staff_member_required
@@ -34,6 +42,16 @@ def conditions_export(request, format):
})
+@staff_member_required
+def conditions_export_xml(request):
+ queryset = Condition.objects.all()
+ serializer = ExportSerializer(queryset, many=True)
+
+ response = HttpResponse(XMLRenderer().render(serializer.data), content_type="application/xml")
+ response['Content-Disposition'] = 'filename="conditions.xml"'
+ return response
+
+
class ConditionViewSet(viewsets.ModelViewSet):
permission_classes = (DjangoModelPermissions, )
diff --git a/apps/core/management/commands/create-test-data.py b/apps/core/management/commands/create-test-data.py
deleted file mode 100644
index 28cd289d45..0000000000
--- a/apps/core/management/commands/create-test-data.py
+++ /dev/null
@@ -1,290 +0,0 @@
-from django.core.management.base import BaseCommand
-from django.utils import translation
-
-from apps.domain.testing.factories import *
-from apps.conditions.testing.factories import *
-from apps.questions.testing.factories import *
-from apps.tasks.testing.factories import *
-from apps.projects.testing.factories import *
-
-
-class Command(BaseCommand):
-
- def handle(self, *args, **options):
- translation.activate('en')
-
- # domain
-
- AttributeEntityFactory(id=1, title='test')
-
- catalog = CatalogFactory(id=1, title='RDMO')
-
- ##################
- # single widgets #
- ##################
-
- AttributeEntityFactory(id=11, parent_id=1, title='single')
-
- AttributeEntityFactory(id=111, parent_id=11, title='1')
- AttributeEntityFactory(id=112, parent_id=11, title='2')
- AttributeEntityFactory(id=113, parent_id=11, title='3')
- AttributeEntityFactory(id=114, parent_id=11, title='4')
-
- section = SectionFactory(id=1, catalog=catalog, order=1, title='Single')
-
- subsection = SubsectionFactory(id=11, section=section, order=1, title='Text')
-
- AttributeFactory(id=1111, parent_id=111, title='text', value_type='text')
- AttributeFactory(id=1112, parent_id=111, title='integer', value_type='integer')
- AttributeFactory(id=1113, parent_id=111, title='float', value_type='float')
-
- QuestionFactory(id=1111, subsection=subsection, order=1, attribute_entity_id=1111, widget_type='text', text='text')
- QuestionFactory(id=1112, subsection=subsection, order=2, attribute_entity_id=1112, widget_type='text', text='integer')
- QuestionFactory(id=1113, subsection=subsection, order=3, attribute_entity_id=1113, widget_type='text', text='float')
-
- subsection = SubsectionFactory(id=12, section=section, order=2, title='Widgets')
-
- AttributeFactory(id=1121, parent_id=112, title='textarea', value_type='text')
- AttributeFactory(id=1122, parent_id=112, title='yesno', value_type='boolean')
-
- QuestionFactory(id=1121, subsection=subsection, order=1, attribute_entity_id=1121, widget_type='textarea', text='textarea')
- QuestionFactory(id=1122, subsection=subsection, order=2, attribute_entity_id=1122, widget_type='yesno', text='yesno')
-
- AttributeFactory(id=1131, parent_id=113, title='date', value_type='datetime')
- AttributeFactory(id=1132, parent_id=113, title='range', value_type='float')
-
- QuestionFactory(id=1131, subsection=subsection, order=3, attribute_entity_id=1131, widget_type='date', text='date')
- QuestionFactory(id=1132, subsection=subsection, order=4, attribute_entity_id=1132, widget_type='range', text='range')
-
- subsection = SubsectionFactory(id=13, section=section, order=3, title='Options')
-
- AttributeFactory(id=1141, parent_id=114, title='radio', value_type='options')
- AttributeFactory(id=1142, parent_id=114, title='select', value_type='options')
- AttributeFactory(id=1143, parent_id=114, title='checkbox', value_type='options')
-
- QuestionFactory(id=1141, subsection=subsection, order=1, attribute_entity_id=1141, widget_type='radio', text='radio')
- QuestionFactory(id=1142, subsection=subsection, order=2, attribute_entity_id=1142, widget_type='select', text='select')
- QuestionFactory(id=1143, subsection=subsection, order=3, attribute_entity_id=1143, widget_type='checkbox', text='checkbox')
-
- ######################
- # collection widgets #
- ######################
-
- AttributeEntityFactory(id=12, parent_id=1, title='collection')
-
- AttributeEntityFactory(id=121, parent_id=12, title='1')
- AttributeEntityFactory(id=122, parent_id=12, title='2')
- AttributeEntityFactory(id=123, parent_id=12, title='3')
- AttributeEntityFactory(id=124, parent_id=12, title='4')
-
- section = SectionFactory(id=2, catalog=catalog, order=2, title='Collection')
-
- subsection = SubsectionFactory(id=21, section=section, order=1, title='Text')
-
- AttributeFactory(id=1211, parent_id=121, is_collection=True, title='text', value_type='text')
- AttributeFactory(id=1212, parent_id=121, is_collection=True, title='integer', value_type='integer')
- AttributeFactory(id=1213, parent_id=121, is_collection=True, title='float', value_type='float')
-
- QuestionFactory(id=1211, subsection=subsection, order=1, attribute_entity_id=1211, widget_type='text', text='text')
- QuestionFactory(id=1212, subsection=subsection, order=2, attribute_entity_id=1212, widget_type='text', text='integer')
- QuestionFactory(id=1213, subsection=subsection, order=3, attribute_entity_id=1213, widget_type='text', text='float')
-
- subsection = SubsectionFactory(id=22, section=section, order=2, title='Widgets')
-
- AttributeFactory(id=1221, parent_id=122, is_collection=True, title='textarea', value_type='text')
- AttributeFactory(id=1222, parent_id=122, is_collection=True, title='yesno', value_type='boolean')
-
- QuestionFactory(id=1221, subsection=subsection, order=1, attribute_entity_id=1221, widget_type='textarea', text='textarea')
- QuestionFactory(id=1222, subsection=subsection, order=2, attribute_entity_id=1222, widget_type='yesno', text='yesno')
-
- AttributeFactory(id=1231, parent_id=123, is_collection=True, title='date', value_type='datetime')
- AttributeFactory(id=1232, parent_id=123, is_collection=True, title='range', value_type='float')
-
- QuestionFactory(id=1231, subsection=subsection, order=3, attribute_entity_id=1231, widget_type='date', text='date')
- QuestionFactory(id=1232, subsection=subsection, order=4, attribute_entity_id=1232, widget_type='range', text='range')
-
- subsection = SubsectionFactory(id=23, section=section, order=3, title='Options')
-
- AttributeFactory(id=1241, parent_id=124, is_collection=True, title='radio', value_type='options')
- AttributeFactory(id=1242, parent_id=124, is_collection=True, title='select', value_type='options')
-
- QuestionFactory(id=1241, subsection=subsection, order=1, attribute_entity_id=1241, widget_type='radio', text='radio')
- QuestionFactory(id=1242, subsection=subsection, order=2, attribute_entity_id=1242, widget_type='select', text='select')
-
- ###############
- # set widgets #
- ###############
-
- AttributeEntityFactory(id=13, parent_id=1, title='set_single')
-
- AttributeEntityFactory(id=131, parent_id=13, title='1')
- AttributeEntityFactory(id=132, parent_id=13, title='2')
- AttributeEntityFactory(id=133, parent_id=13, title='3')
- AttributeEntityFactory(id=134, parent_id=13, title='4')
-
- section = SectionFactory(id=3, catalog=catalog, order=3, title='Set')
-
- subsection = SubsectionFactory(id=31, section=section, order=1, title='Text')
-
- entity = QuestionEntityFactory(id=131, subsection=subsection, order=1, attribute_entity_id=131)
-
- AttributeFactory(id=1311, parent_id=131, title='text', value_type='text')
- AttributeFactory(id=1312, parent_id=131, title='integer', value_type='integer')
- AttributeFactory(id=1313, parent_id=131, title='float', value_type='float')
-
- QuestionFactory(id=1311, subsection=subsection, parent=entity, order=1, attribute_entity_id=1311, widget_type='text', text='text')
- QuestionFactory(id=1312, subsection=subsection, parent=entity, order=2, attribute_entity_id=1312, widget_type='text', text='integer')
- QuestionFactory(id=1313, subsection=subsection, parent=entity, order=3, attribute_entity_id=1313, widget_type='text', text='float')
-
- entity = QuestionEntityFactory(id=132, subsection=subsection, order=2, attribute_entity_id=132)
-
- AttributeFactory(id=1321, parent_id=132, title='textarea', value_type='text')
- AttributeFactory(id=1322, parent_id=132, title='yesno', value_type='boolean')
-
- QuestionFactory(id=1321, subsection=subsection, parent=entity, order=1, attribute_entity_id=1321, widget_type='textarea', text='textarea')
- QuestionFactory(id=1322, subsection=subsection, parent=entity, order=2, attribute_entity_id=1322, widget_type='yesno', text='yesno')
-
- entity = QuestionEntityFactory(id=133, subsection=subsection, order=3, attribute_entity_id=133)
-
- AttributeFactory(id=1331, parent_id=133, title='date', value_type='datetime')
- AttributeFactory(id=1332, parent_id=133, title='range', value_type='float')
-
- QuestionFactory(id=1331, subsection=subsection, parent=entity, order=1, attribute_entity_id=1331, widget_type='date', text='date')
- QuestionFactory(id=1332, subsection=subsection, parent=entity, order=2, attribute_entity_id=1332, widget_type='range', text='range')
-
- entity = QuestionEntityFactory(id=134, subsection=subsection, order=4, attribute_entity_id=134)
-
- AttributeFactory(id=1341, parent_id=134, title='radio', value_type='options')
- AttributeFactory(id=1342, parent_id=134, title='select', value_type='options')
- AttributeFactory(id=1343, parent_id=134, title='checkbox', value_type='options')
-
- QuestionFactory(id=1341, subsection=subsection, parent=entity, order=1, attribute_entity_id=1341, widget_type='radio', text='radio')
- QuestionFactory(id=1342, subsection=subsection, parent=entity, order=2, attribute_entity_id=1342, widget_type='select', text='select')
- QuestionFactory(id=1343, subsection=subsection, parent=entity, order=3, attribute_entity_id=1343, widget_type='checkbox', text='checkbox')
-
- ##########################
- # set colelction widgets #
- ##########################
-
- AttributeEntityFactory(id=14, parent_id=1, title='set_collection', is_collection=True)
-
- AttributeEntityFactory(id=141, parent_id=14, title='1')
- AttributeEntityFactory(id=142, parent_id=14, title='2')
- AttributeEntityFactory(id=143, parent_id=14, title='3')
- AttributeEntityFactory(id=144, parent_id=14, title='4')
-
- AttributeFactory(id=1410, parent_id=14, title='id', value_type='text')
-
- section = SectionFactory(id=4, catalog=catalog, order=4, title='Set collection')
-
- subsection = SubsectionFactory(id=41, section=section, order=4, title='Text')
-
- entity = QuestionEntityFactory(id=141, subsection=subsection, order=1, attribute_entity_id=141)
-
- AttributeFactory(id=1411, parent_id=141, is_collection=True, title='text', value_type='text')
- AttributeFactory(id=1412, parent_id=141, is_collection=True, title='integer', value_type='integer')
- AttributeFactory(id=1413, parent_id=141, is_collection=True, title='float', value_type='float')
-
- QuestionFactory(id=1411, subsection=subsection, parent=entity, order=1, attribute_entity_id=1411, widget_type='text', text='text')
- QuestionFactory(id=1412, subsection=subsection, parent=entity, order=2, attribute_entity_id=1412, widget_type='text', text='integer')
- QuestionFactory(id=1413, subsection=subsection, parent=entity, order=3, attribute_entity_id=1413, widget_type='text', text='float')
-
- entity = QuestionEntityFactory(id=142, subsection=subsection, order=2, attribute_entity_id=142)
-
- AttributeFactory(id=1421, parent_id=142, is_collection=True, title='textarea', value_type='text')
- AttributeFactory(id=1422, parent_id=142, is_collection=True, title='yesno', value_type='boolean')
-
- QuestionFactory(id=1421, subsection=subsection, parent=entity, order=1, attribute_entity_id=1421, widget_type='textarea', text='textarea')
- QuestionFactory(id=1422, subsection=subsection, parent=entity, order=2, attribute_entity_id=1422, widget_type='yesno', text='yesno')
-
- entity = QuestionEntityFactory(id=143, subsection=subsection, order=3, attribute_entity_id=143)
-
- AttributeFactory(id=1431, parent_id=143, is_collection=True, title='date', value_type='datetime')
- AttributeFactory(id=1432, parent_id=143, is_collection=True, title='range', value_type='float')
-
- QuestionFactory(id=1431, subsection=subsection, parent=entity, order=1, attribute_entity_id=1431, widget_type='date', text='date')
- QuestionFactory(id=1432, subsection=subsection, parent=entity, order=2, attribute_entity_id=1432, widget_type='range', text='range')
-
- entity = QuestionEntityFactory(id=144, subsection=subsection, order=4, attribute_entity_id=144)
-
- AttributeFactory(id=1441, parent_id=144, is_collection=True, title='radio', value_type='options')
- AttributeFactory(id=1442, parent_id=144, is_collection=True, title='select', value_type='options')
-
- QuestionFactory(id=1441, subsection=subsection, parent=entity, order=1, attribute_entity_id=1441, widget_type='radio', text='radio')
- QuestionFactory(id=1442, subsection=subsection, parent=entity, order=2, attribute_entity_id=1442, widget_type='select', text='select')
-
- ##############
- # conditions #
- ##############
-
- AttributeEntityFactory(id=15, parent_id=1, title='conditions')
-
- section = SectionFactory(id=5, catalog=catalog, order=5, title='Conditions')
-
- # single condition
-
- subsection = SubsectionFactory(id=51, section=section, order=1, title='Condition')
-
- condition_yesno = AttributeFactory(id=1510, parent_id=15, title='condition_yesno', value_type='bool')
- condition_attribute = AttributeFactory(id=1511, parent_id=15, title='condition_attribute', value_type='text')
-
- QuestionFactory(id=1510, subsection=subsection, order=1, attribute_entity_id=1510, widget_type='yesno', text='condition_bool')
- QuestionFactory(id=1511, subsection=subsection, order=2, attribute_entity_id=1511, widget_type='text', text='condition_attribute')
-
- condition_attribute.conditions.add(ConditionFactory(id=51, source=condition_yesno))
-
- # condition for set
-
- subsection = SubsectionFactory(id=52, section=section, order=2, title='Condition set')
-
- condition_yesno = AttributeFactory(id=1520, parent_id=15, title='condition_set_yesno', value_type='bool')
- condition_entity = AttributeEntityFactory(id=152, parent_id=15, title='condition_set_entity')
-
- AttributeFactory(id=1521, parent_id=152, title='condition_set_attribute', value_type='text')
-
- QuestionFactory(id=1520, subsection=subsection, order=1, attribute_entity_id=1520, widget_type='yesno', text='condition_set_bool')
-
- entity = QuestionEntityFactory(id=152, subsection=subsection, order=2, attribute_entity_id=152)
- QuestionFactory(id=1521, subsection=subsection, parent=entity, order=1, attribute_entity_id=1521, widget_type='text', text='condition_set_attribute')
-
- condition_entity.conditions.add(ConditionFactory(id=52, source=condition_yesno))
-
- # condition for set collection
-
- subsection = SubsectionFactory(id=53, section=section, order=3, title='Condition set collection')
-
- condition_yesno = AttributeFactory(id=1530, parent_id=15, title='condition_set_collection_yesno', value_type='bool')
- condition_entity = AttributeEntityFactory(id=153, is_collection=True, parent_id=15, title='condition_set_collection_entity')
-
- AttributeFactory(id=1531, parent_id=153, title='id', value_type='text')
- AttributeFactory(id=1532, parent_id=153, title='text', value_type='text')
-
- QuestionFactory(id=1530, subsection=subsection, order=1, attribute_entity_id=1530, widget_type='yesno', text='condition_set_collection_bool')
-
- entity = QuestionEntityFactory(id=153, subsection=subsection, order=2, attribute_entity_id=153)
- QuestionFactory(id=1532, subsection=subsection, parent=entity, order=1, attribute_entity_id=1532, widget_type='text', text='condition_set_collection_attribute')
-
- condition_entity.conditions.add(ConditionFactory(id=53, source=condition_yesno))
-
- #########
- # tasks #
- #########
-
- AttributeEntityFactory(id=16, parent_id=1, title='tasks')
-
- section = SectionFactory(id=6, catalog=catalog, order=6, title='Tasks')
-
- subsection = SubsectionFactory(id=61, section=section, order=1, title='Task')
-
- task_yesno = AttributeFactory(id=1610, parent_id=16, title='task_condition_bool', value_type='bool')
- task_attribute = AttributeFactory(id=1611, parent_id=16, title='task_date', value_type='datetime')
-
- QuestionFactory(id=1610, subsection=subsection, order=1, attribute_entity_id=1610, widget_type='yesno', text='task_condition_bool')
- QuestionFactory(id=1611, subsection=subsection, order=2, attribute_entity_id=1611, widget_type='date', text='task_date')
-
- task = TaskFactory(id=1610, attribute=task_attribute)
- task.conditions.add(ConditionFactory(id=61, source=task_yesno))
-
- ## project
-
- ProjectFactory(catalog=catalog, owner=[1])
diff --git a/apps/core/management/commands/import.py b/apps/core/management/commands/import.py
new file mode 100644
index 0000000000..de88b402a4
--- /dev/null
+++ b/apps/core/management/commands/import.py
@@ -0,0 +1,53 @@
+from lxml import objectify
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand, CommandError
+
+from apps.conditions.utils import import_conditions
+from apps.options.utils import import_options
+from apps.domain.utils import import_domain
+from apps.questions.utils import import_catalogs
+from apps.tasks.utils import import_tasks
+from apps.views.utils import import_views
+from apps.projects.utils import import_projects
+
+
+class Command(BaseCommand):
+
+ def add_arguments(self, parser):
+ parser.add_argument('xmlfile', action='store', default=False, help='RDMO XML export file')
+ parser.add_argument('--user', action='store', default=False, help='RDMO username for this import')
+
+ def handle(self, *args, **options):
+ with open(options['xmlfile']) as f:
+ xml_root = objectify.parse(f).getroot()
+
+ if xml_root.tag == 'conditions':
+ import_conditions(xml_root)
+
+ elif xml_root.tag == 'options':
+ import_options(xml_root)
+
+ elif xml_root.tag == 'domain':
+ import_domain(xml_root)
+
+ elif xml_root.tag == 'catalogs':
+ import_catalogs(xml_root)
+
+ elif xml_root.tag == 'tasks':
+ import_tasks(xml_root)
+
+ elif xml_root.tag == 'views':
+ import_views(xml_root)
+
+ elif xml_root.tag == 'projects':
+
+ try:
+ user = User.objects.get(username=options['user'])
+ except User.DoesNotExist:
+ raise CommandError('Give a valid username using --user.')
+
+ import_projects(xml_root, user)
+
+ else:
+ raise Exception('This is not a proper RDMO XML Export.')
diff --git a/apps/core/management/commands/set-uri-prefix.py b/apps/core/management/commands/set-uri-prefix.py
new file mode 100644
index 0000000000..3a4e1690e5
--- /dev/null
+++ b/apps/core/management/commands/set-uri-prefix.py
@@ -0,0 +1,53 @@
+from lxml import objectify
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+
+from apps.conditions.models import Condition
+from apps.options.models import OptionSet, Option
+from apps.domain.models import AttributeEntity
+from apps.questions.models import Catalog, Section, Subsection, QuestionEntity
+from apps.tasks.models import Task
+from apps.views.models import View
+
+
+class Command(BaseCommand):
+
+ def add_arguments(self, parser):
+ parser.add_argument('uri_prefix', action='store', help='URI prefix to be used for all elements.')
+
+ def handle(self, *args, **options):
+
+ for obj in Condition.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in OptionSet.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in Option.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in AttributeEntity.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in Catalog.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in Section.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in Subsection.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in QuestionEntity.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in Task.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ for obj in View.objects.all():
+ self._set_uri_prefix(obj, options['uri_prefix'])
+
+ def _set_uri_prefix(self, obj, uri_prefix):
+ obj.uri_prefix = uri_prefix
+ obj.save()
diff --git a/apps/core/renderers.py b/apps/core/renderers.py
new file mode 100644
index 0000000000..82d895da6d
--- /dev/null
+++ b/apps/core/renderers.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.utils.six.moves import StringIO
+from django.utils.encoding import smart_text
+from rest_framework.renderers import BaseRenderer
+
+
+class BaseXMLRenderer(BaseRenderer):
+
+ media_type = 'application/xml'
+ format = 'xml'
+
+ def render(self, data):
+
+ if data is None:
+ return ''
+
+ stream = StringIO()
+
+ xml = SimplerXMLGenerator(stream, "utf-8")
+ xml.startDocument()
+ self.render_document(xml, data)
+ xml.endDocument()
+ return stream.getvalue()
+
+ def render_text_element(self, xml, tag, attrs, text):
+ # remove None values from attrs
+ attrs = dict((key, value) for key, value in attrs.items() if value)
+
+ xml.startElement(tag, attrs)
+ if text is not None:
+ xml.characters(smart_text(text))
+ xml.endElement(tag)
+
+ def render_document(self, xml, data):
+ pass
diff --git a/apps/core/static/core/css/base.scss b/apps/core/static/core/css/base.scss
index ad8048abfb..0bf8c853f0 100644
--- a/apps/core/static/core/css/base.scss
+++ b/apps/core/static/core/css/base.scss
@@ -125,9 +125,11 @@ form .yesno label {
/* modals */
-.modal-body > p:last-child,
-.modal-body formgroup:last-child .form-group {
- margin-bottom: 0;
+.modal-body {
+ > p:last-child,
+ formgroup:last-child .form-group {
+ margin-bottom: 0;
+ }
}
/* options */
diff --git a/apps/core/testing/__init__.py b/apps/core/testing/__init__.py
index 8baa6e5aff..e69de29bb2 100644
--- a/apps/core/testing/__init__.py
+++ b/apps/core/testing/__init__.py
@@ -1 +0,0 @@
-from .tests import *
diff --git a/apps/core/testing/factories.py b/apps/core/testing/factories.py
deleted file mode 100644
index d68433dc9c..0000000000
--- a/apps/core/testing/factories.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from django.contrib.sites.models import Site
-
-from factory.django import DjangoModelFactory
-
-from ..models import *
-
-
-class SiteFactory(DjangoModelFactory):
-
- class Meta:
- model = Site
-
- name = "RDMO"
- domain = "localhost:8000"
diff --git a/apps/core/testing/mixins.py b/apps/core/testing/mixins.py
index d1bb3539ff..e78ac5e8b6 100644
--- a/apps/core/testing/mixins.py
+++ b/apps/core/testing/mixins.py
@@ -34,56 +34,173 @@ def get_instance_as_json(self, instance=None):
class TestListViewMixin(object):
def test_list_view(self):
+
url = reverse(self.list_url_name)
response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_list_view'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
class TestRetrieveViewMixin(object):
def test_retrieve_view(self):
- url = reverse(self.retrieve_url_name, args=[self.instance.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ for instance in self.instances:
+ url = reverse(self.retrieve_url_name, args=[self.instance.pk])
+ response = self.client.get(url)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_retrieve_view'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
class TestCreateViewMixin(TestSingleObjectMixin):
def test_create_view_get(self):
+
url = reverse(self.create_url_name)
response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_create_view_get'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
def test_create_view_post(self):
- url = reverse(self.create_url_name)
- response = self.client.post(url, self.get_instance_as_dict())
- self.assertEqual(response.status_code, 302)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.create_url_name)
+ data = self.get_instance_as_dict(instance)
+ response = self.client.post(url, data)
+
+ try:
+ self.assertEqual(response.status_code, 302)
+ except AssertionError:
+ print(
+ ('test', 'test_create_view_post'),
+ ('url', url),
+ ('data', data),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
+
+ def prepare_create_instance(self, instance):
+ return instance
class TestUpdateViewMixin(TestSingleObjectMixin):
def test_update_view_get(self):
- url = reverse(self.update_url_name, args=[self.instance.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.update_url_name, args=[instance.pk])
+ response = self.client.get(url)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_update_view_get'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
def test_update_view_post(self):
- url = reverse(self.update_url_name, args=[self.instance.pk])
- response = self.client.post(url, self.get_instance_as_dict())
- self.assertEqual(response.status_code, 302)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.update_url_name, args=[instance.pk])
+ data = self.get_instance_as_dict(instance)
+ response = self.client.post(url, data)
+
+ try:
+ self.assertEqual(response.status_code, 302)
+ except AssertionError:
+ print(
+ ('test', 'test_update_view_post'),
+ ('url', url),
+ ('data', data),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
+
+ def prepare_update_instance(self, instance):
+ return instance
class TestDeleteViewMixin(TestSingleObjectMixin):
def test_delete_view_get(self):
- url = reverse(self.delete_url_name, args=[self.instance.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.delete_url_name, args=[self.instance.pk])
+ response = self.client.get(url)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_delete_view_get'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
def test_delete_view_post(self):
- url = reverse(self.delete_url_name, args=[self.instance.pk])
- response = self.client.post(url)
- self.assertEqual(response.status_code, 302)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.delete_url_name, args=[self.instance.pk])
+ response = self.client.post(url)
+
+ try:
+ self.assertEqual(response.status_code, 302)
+ except AssertionError:
+ print(
+ ('test', 'test_update_view_post'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('content', response.content)
+ )
+ raise
+
+ def prepare_delete_instance(self, instance):
+ return instance
class TestModelViewMixin(TestListViewMixin,
@@ -97,7 +214,8 @@ class TestModelViewMixin(TestListViewMixin,
class TestModelStringMixin(TestSingleObjectMixin):
def test_model_str(self):
- self.assertIsNotNone(self.instance.__str__())
+ for instance in self.instances:
+ self.assertIsNotNone(instance.__str__())
class TestListAPIViewMixin(object):
@@ -105,39 +223,120 @@ class TestListAPIViewMixin(object):
def test_list_api_view(self):
url = reverse(self.api_url_name + '-list')
response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_list_api_view'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('json', response.json())
+ )
+ raise
class TestRetrieveAPIViewMixin(object):
def test_retrieve_api_view(self):
- url = reverse(self.api_url_name + '-detail', args=[self.instance.pk])
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
+
+ for instance in self.instances:
+ instance = self.prepare_retrieve_instance(instance)
+
+ url = reverse(self.api_url_name + '-detail', args=[instance.pk])
+ response = self.client.get(url)
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_retrieve_api_view'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('json', response.json())
+ )
+ raise
+
+ def prepare_retrieve_instance(self, instance):
+ return instance
class TestCreateAPIViewMixin(TestSingleObjectMixin):
def test_create_api_view(self):
- url = reverse(self.api_url_name + '-list')
- response = self.client.post(url, self.get_instance_as_dict())
- self.assertEqual(response.status_code, 201)
+
+ for instance in self.instances:
+ instance = self.prepare_create_instance(instance)
+
+ url = reverse(self.api_url_name + '-list')
+ data = self.get_instance_as_dict(instance)
+ response = self.client.post(url, self.get_instance_as_dict(instance))
+
+ try:
+ self.assertEqual(response.status_code, 201)
+ except AssertionError:
+ print(
+ ('test', 'test_create_api_view'),
+ ('url', url),
+ ('data', data),
+ ('status_code', response.status_code),
+ ('json', response.json())
+ )
+ raise
+
+ def prepare_create_instance(self, instance):
+ return instance
class TestUpdateAPIViewMixin(TestSingleObjectMixin):
def test_update_api_view(self):
- url = reverse(self.api_url_name + '-detail', args=[self.instance.pk])
- response = self.client.put(url, self.get_instance_as_json(), content_type="application/json")
- self.assertEqual(response.status_code, 200)
+
+ for instance in self.instances:
+ instance = self.prepare_update_instance(instance)
+
+ url = reverse(self.api_url_name + '-detail', args=[instance.pk])
+ data = self.get_instance_as_json(instance)
+ response = self.client.put(url, data, content_type="application/json")
+
+ try:
+ self.assertEqual(response.status_code, 200)
+ except AssertionError:
+ print(
+ ('test', 'test_update_api_view'),
+ ('url', url),
+ ('data', data),
+ ('status_code', response.status_code),
+ ('json', response.json())
+ )
+ raise
+
+ def prepare_update_instance(self, instance):
+ return instance
class TestDeleteAPIViewMixin(TestSingleObjectMixin):
def test_delete_api_view(self):
- url = reverse(self.api_url_name + '-detail', args=[self.instance.pk])
- response = self.client.delete(url)
- self.assertEqual(response.status_code, 204)
+
+ for instance in self.instances:
+ instance = self.prepare_delete_instance(instance)
+
+ url = reverse(self.api_url_name + '-detail', args=[instance.pk])
+ response = self.client.delete(url)
+ try:
+ self.assertEqual(response.status_code, 204)
+ except AssertionError:
+ print(
+ ('test', 'test_delete_api_view'),
+ ('url', url),
+ ('status_code', response.status_code),
+ ('json', response.json())
+ )
+ raise
+
+ def prepare_delete_instance(self, instance):
+ return instance
class TestModelAPIViewMixin(TestListAPIViewMixin,
diff --git a/apps/core/testing/tests.py b/apps/core/tests.py
similarity index 81%
rename from apps/core/testing/tests.py
rename to apps/core/tests.py
index 8f7d228e2f..26b6e9b782 100644
--- a/apps/core/testing/tests.py
+++ b/apps/core/tests.py
@@ -1,26 +1,28 @@
from django.test import TestCase
from django.conf import settings
-from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.template import RequestContext, Template
from django.test.client import RequestFactory
-from django.contrib.auth.models import AnonymousUser
from django.utils import translation
-from apps.accounts.testing.factories import UserFactory, ManagerFactory, AdminFactory
+# from apps.core.testing.mixins import (
-from .mixins import *
+# )
-class CoreTests(TestCase):
+class CoreTestCase(TestCase):
+
+ fixtures = (
+ 'auth.json',
+ 'accounts.json',
+ )
+
+
+class CoreTests(CoreTestCase):
def setUp(self):
translation.activate('en')
- UserFactory()
- ManagerFactory()
- AdminFactory()
-
def test_home_view(self):
""" The home page can be accessed. """
@@ -34,12 +36,12 @@ def test_home_view(self):
self.assertRedirects(response, reverse('projects'))
# test as manager
- self.client.login(username='user', password='user')
+ self.client.login(username='manager', password='manager')
response = self.client.get('/')
self.assertRedirects(response, reverse('projects'))
# test as admin
- self.client.login(username='user', password='user')
+ self.client.login(username='admin', password='admin')
response = self.client.get('/')
self.assertRedirects(response, reverse('projects'))
@@ -63,12 +65,9 @@ def test_i18n_switcher(self):
self.assertIn('en', response['Content-Language'])
-class CoreTagsTests(TestCase):
- def setUp(self):
- self.user = UserFactory()
- self.manager = ManagerFactory()
- self.admin = AdminFactory()
+class CoreTagsTests(CoreTestCase):
+ def setUp(self):
self.request = RequestFactory().get('/')
def test_i18n_switcher(self):
diff --git a/apps/core/utils.py b/apps/core/utils.py
index 372bc816de..4651cf4252 100644
--- a/apps/core/utils.py
+++ b/apps/core/utils.py
@@ -38,6 +38,15 @@ def get_next(request):
return get_script_alias(request) + next
+def get_ns_tag(tag, nsmap):
+ tag_split = tag.split(':')
+ return '{%s}%s' % (nsmap[tag_split[0]], tag_split[1])
+
+
+def get_uri_prefix(obj):
+ return obj.uri_prefix.rstrip('/') if obj.uri_prefix else settings.DEFAULT_URI_PREFIX
+
+
def render_to_format(request, format, title, template_src, context):
# for some weird reason we have to cast here explicitly
diff --git a/apps/core/validators.py b/apps/core/validators.py
new file mode 100644
index 0000000000..ecadb14485
--- /dev/null
+++ b/apps/core/validators.py
@@ -0,0 +1,88 @@
+from django.apps import apps
+from django.core.exceptions import ValidationError, ObjectDoesNotExist, MultipleObjectsReturned
+from django.utils.translation import ugettext_lazy as _
+
+from rest_framework import serializers
+
+
+class UniqueKeyValidator(object):
+
+ def __init__(self, instance=None):
+ self.instance = instance
+
+ def set_context(self, serializer):
+ self.instance = serializer.instance
+
+ def validate(self, data=None):
+ model = apps.get_model(app_label=self.app_label, model_name=self.model_name)
+
+ if data:
+ key = data['key']
+ else:
+ key = self.instance.key
+
+ try:
+ if self.instance:
+ model.objects.exclude(pk=self.instance.pk).get(key=key)
+ else:
+ model.objects.get(key=key)
+ except ObjectDoesNotExist:
+ return
+ except MultipleObjectsReturned:
+ pass
+
+ raise ValidationError({
+ 'key': _('%(model)s with this key already exists.') % {
+ 'model': model._meta.verbose_name.title()
+ }
+ })
+
+ def __call__(self, data=None):
+ try:
+ self.validate(data)
+ except ValidationError as e:
+ raise serializers.ValidationError({
+ 'key': e.error_dict['key'][0][0]
+ })
+
+
+class UniquePathValidator(object):
+
+ def __init__(self, instance=None):
+ self.instance = instance
+
+ def set_context(self, serializer):
+ self.instance = serializer.instance
+
+ def validate(self, data=None):
+ model = apps.get_model(app_label=self.app_label, model_name=self.model_name)
+
+ if data:
+ path = self.get_path(model, data)
+ else:
+ path = self.instance.path
+
+ try:
+ if self.instance:
+ model.objects.exclude(pk=self.instance.pk).get(path=path)
+ else:
+ model.objects.get(path=path)
+ except ObjectDoesNotExist:
+ return
+ except MultipleObjectsReturned:
+ pass
+
+ raise ValidationError({
+ 'key': _('%(model)s with the path "%(path)s" already exists. Please adjust the the Key.') % {
+ 'model': model._meta.verbose_name.title(),
+ 'path': path
+ }
+ })
+
+ def __call__(self, data=None):
+ try:
+ self.validate(data)
+ except ValidationError as e:
+ raise serializers.ValidationError({
+ 'key': e.error_dict['key'][0][0]
+ })
diff --git a/apps/domain/admin.py b/apps/domain/admin.py
index 37e3733758..c05bc4ef6b 100644
--- a/apps/domain/admin.py
+++ b/apps/domain/admin.py
@@ -1,11 +1,11 @@
from django.contrib import admin
-from .models import *
+from .models import AttributeEntity, Attribute, VerboseName, Range
class AttributeEntityAdmin(admin.ModelAdmin):
+ readonly_fields = ('uri', 'path', 'parent_collection', 'is_attribute')
- readonly_fields = ('label', 'parent_collection', 'is_attribute')
admin.site.register(AttributeEntity, AttributeEntityAdmin)
admin.site.register(Attribute, AttributeEntityAdmin)
diff --git a/apps/domain/migrations/0025_refactoring.py b/apps/domain/migrations/0025_refactoring.py
new file mode 100644
index 0000000000..bbfa27d656
--- /dev/null
+++ b/apps/domain/migrations/0025_refactoring.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-01-25 15:46
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('domain', '0024_meta'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='attributeentity',
+ options={'ordering': ('uri',), 'verbose_name': 'AttributeEntity', 'verbose_name_plural': 'AttributeEntities'},
+ ),
+ migrations.RenameField(
+ model_name='attributeentity',
+ old_name='description',
+ new_name='comment',
+ ),
+ migrations.RenameField(
+ model_name='attributeentity',
+ old_name='title',
+ new_name='key',
+ ),
+ ]
diff --git a/apps/domain/migrations/0026_refactoring.py b/apps/domain/migrations/0026_refactoring.py
new file mode 100644
index 0000000000..d83c8c94bf
--- /dev/null
+++ b/apps/domain/migrations/0026_refactoring.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-01-25 15:54
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import mptt.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('domain', '0025_refactoring'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='verbosename',
+ options={'verbose_name': 'Verbose name', 'verbose_name_plural': 'Verbose names'},
+ ),
+ migrations.AddField(
+ model_name='attributeentity',
+ name='uri_prefix',
+ field=models.URLField(blank=True, help_text='The prefix for the URI of this attribute/entity.', max_length=256, null=True, verbose_name='URI Prefix'),
+ ),
+ migrations.AlterField(
+ model_name='attribute',
+ name='optionsets',
+ field=models.ManyToManyField(blank=True, help_text='Option sets for this attribute.', to='options.OptionSet', verbose_name='Option sets'),
+ ),
+ migrations.AlterField(
+ model_name='attribute',
+ name='unit',
+ field=models.CharField(blank=True, help_text='Unit of values for this attribute.', max_length=64, null=True, verbose_name='Unit'),
+ ),
+ migrations.AlterField(
+ model_name='attribute',
+ name='value_type',
+ field=models.CharField(choices=[('text', 'Text'), ('url', 'URL'), ('integer', 'Integer'), ('float', 'Float'), ('boolean', 'Boolean'), ('datetime', 'Datetime'), ('options', 'Options')], help_text='Type of values for this attribute.', max_length=8, verbose_name='Value type'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='comment',
+ field=models.TextField(blank=True, help_text='Additional information about this attribute/entity.', null=True, verbose_name='Comment'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='conditions',
+ field=models.ManyToManyField(blank=True, help_text='List of conditions evaluated for this attribute/entity.', to='conditions.Condition', verbose_name='Conditions'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='is_attribute',
+ field=models.BooleanField(default=False, help_text='Designates whether this attribute/entity is an attribute (auto-generated).', verbose_name='is attribute'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='is_collection',
+ field=models.BooleanField(default=False, help_text='Designates whether this attribute/entity is a collection.', verbose_name='is collection'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='key',
+ field=models.SlugField(blank=True, help_text='The internal identifier of this attribute/entity. The URI will be generated from this key.', max_length=128, null=True, verbose_name='Key'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='label',
+ field=models.CharField(db_index=True, help_text='The label to be displayed this attribute/entity (auto-generated).', max_length=512, verbose_name='Label'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='parent',
+ field=mptt.fields.TreeForeignKey(blank=True, help_text='Parent entity in the domain model.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='domain.AttributeEntity', verbose_name='Parent entity'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='parent_collection',
+ field=models.ForeignKey(blank=True, default=None, help_text='Next collection entity upwards in the domain model (auto-generated).', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='domain.AttributeEntity', verbose_name='Parent collection'),
+ ),
+ migrations.AlterField(
+ model_name='attributeentity',
+ name='uri',
+ field=models.URLField(blank=True, help_text='The Uniform Resource Identifier of this attribute/entity set (auto-generated).', max_length=640, null=True, verbose_name='URI'),
+ ),
+ migrations.AlterField(
+ model_name='range',
+ name='attribute',
+ field=models.OneToOneField(help_text='Attribute this verbose name belongs to.', on_delete=django.db.models.deletion.CASCADE, to='domain.Attribute', verbose_name='Attribute'),
+ ),
+ migrations.AlterField(
+ model_name='range',
+ name='maximum',
+ field=models.FloatField(help_text='Maximum value for this attribute.', verbose_name='Maximum'),
+ ),
+ migrations.AlterField(
+ model_name='range',
+ name='minimum',
+ field=models.FloatField(help_text='Minimal value for this attribute.', verbose_name='Minimum'),
+ ),
+ migrations.AlterField(
+ model_name='range',
+ name='step',
+ field=models.FloatField(help_text='Step in which this attribute can be incremented/decremented.', verbose_name='Step'),
+ ),
+ migrations.AlterField(
+ model_name='verbosename',
+ name='attribute_entity',
+ field=models.OneToOneField(help_text='Attribute/entity this verbose name belongs to.', on_delete=django.db.models.deletion.CASCADE, to='domain.AttributeEntity', verbose_name='Attribute entity'),
+ ),
+ migrations.AlterField(
+ model_name='verbosename',
+ name='name_de',
+ field=models.CharField(help_text='German name displayed for this attribute/entity (e.g. Projekt).', max_length=256, verbose_name='Name (de)'),
+ ),
+ migrations.AlterField(
+ model_name='verbosename',
+ name='name_en',
+ field=models.CharField(help_text='English name displayed for this attribute/entity (e.g. project).', max_length=256, verbose_name='Name (en)'),
+ ),
+ migrations.AlterField(
+ model_name='verbosename',
+ name='name_plural_de',
+ field=models.CharField(help_text='German plural name displayed for this attribute/entity (e.g. Projekte).', max_length=256, verbose_name='Plural name (de)'),
+ ),
+ migrations.AlterField(
+ model_name='verbosename',
+ name='name_plural_en',
+ field=models.CharField(help_text='English plural name displayed for this attribute/entity (e.g. projects).', max_length=256, verbose_name='Plural name (en)'),
+ ),
+ ]
diff --git a/apps/domain/migrations/0027_meta.py b/apps/domain/migrations/0027_meta.py
new file mode 100644
index 0000000000..365b0b08af
--- /dev/null
+++ b/apps/domain/migrations/0027_meta.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-01-27 15:24
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('domain', '0026_refactoring'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='attributeentity',
+ options={'ordering': ('uri',), 'verbose_name': 'Attribute entity', 'verbose_name_plural': 'Attribute entities'},
+ ),
+ ]
diff --git a/apps/domain/migrations/0028_path.py b/apps/domain/migrations/0028_path.py
new file mode 100644
index 0000000000..b80ad76a43
--- /dev/null
+++ b/apps/domain/migrations/0028_path.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9 on 2017-02-03 13:27
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('domain', '0027_meta'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='attributeentity',
+ old_name='label',
+ new_name='path',
+ ),
+ ]
diff --git a/apps/domain/models.py b/apps/domain/models.py
index 34676ef50b..c95fa1221e 100644
--- a/apps/domain/models.py
+++ b/apps/domain/models.py
@@ -1,47 +1,105 @@
from __future__ import unicode_literals
from django.db import models
-from django.db.models.signals import post_save
-from django.core.validators import RegexValidator
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
+from apps.core.utils import get_uri_prefix
from apps.core.models import TranslationMixin
from apps.conditions.models import Condition
+from .validators import AttributeEntityUniquePathValidator
+
@python_2_unicode_compatible
class AttributeEntity(MPTTModel):
- parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True, help_text='optional')
+ uri = models.URLField(
+ max_length=640, blank=True, null=True,
+ verbose_name=_('URI'),
+ help_text=_('The Uniform Resource Identifier of this attribute/entity set (auto-generated).')
+ )
+ uri_prefix = models.URLField(
+ max_length=256, blank=True, null=True,
+ verbose_name=_('URI Prefix'),
+ help_text=_('The prefix for the URI of this attribute/entity.')
+ )
+ key = models.SlugField(
+ max_length=128, blank=True, null=True,
+ verbose_name=_('Key'),
+ help_text=_('The internal identifier of this attribute/entity. The URI will be generated from this key.')
+ )
+ comment = models.TextField(
+ blank=True, null=True,
+ verbose_name=_('Comment'),
+ help_text=_('Additional information about this attribute/entity.')
+ )
+ parent = TreeForeignKey(
+ 'self', null=True, blank=True, related_name='children', db_index=True,
+ verbose_name=_('Parent entity'),
+ help_text=_('Parent entity in the domain model.')
+ )
+ parent_collection = models.ForeignKey(
+ 'AttributeEntity', blank=True, null=True, default=None, related_name='+', db_index=True,
+ verbose_name=_('Parent collection'),
+ help_text=_('Next collection entity upwards in the domain model (auto-generated).')
+ )
+ is_collection = models.BooleanField(
+ default=False,
+ verbose_name=_('is collection'),
+ help_text=_('Designates whether this attribute/entity is a collection.')
+ )
+ is_attribute = models.BooleanField(
+ default=False,
+ verbose_name=_('is attribute'),
+ help_text=_('Designates whether this attribute/entity is an attribute (auto-generated).')
+ )
+ conditions = models.ManyToManyField(
+ Condition, blank=True,
+ verbose_name=_('Conditions'),
+ help_text=_('List of conditions evaluated for this attribute/entity.')
+ )
+ path = models.CharField(
+ max_length=512, db_index=True,
+ verbose_name=_('Path'),
+ help_text=_('The path part of the URI of this attribute/entity (auto-generated).')
+ )
- title = models.CharField(max_length=256, validators=[
- RegexValidator('^[a-zA-z0-9_]*$', _('Only letters, numbers, or underscores are allowed.'))
- ])
- label = models.CharField(max_length=512, db_index=True)
+ class Meta:
+ ordering = ('uri', )
+ verbose_name = _('Attribute entity')
+ verbose_name_plural = _('Attribute entities')
- description = models.TextField(blank=True, null=True)
- uri = models.URLField(blank=True, null=True)
+ def __str__(self):
+ return self.uri or self.key
- is_collection = models.BooleanField(default=False)
- is_attribute = models.BooleanField(default=False)
+ def save(self, *args, **kwargs):
+ self.path = AttributeEntity.build_path(self.key, self.parent)
+ self.uri = get_uri_prefix(self) + '/domain/' + self.path
+ self.is_attribute = self.is_attribute or False
- parent_collection = models.ForeignKey('AttributeEntity', blank=True, null=True, default=None, related_name='+', db_index=True)
+ # loop over parents to find parent collection
+ self.parent_collection = None
+ parent = self.parent
+ while parent:
+ # set parent_collection if it is not yet set and if parent is a collection
+ if not self.parent_collection and parent.is_collection:
+ self.parent_collection = parent
+ break
- conditions = models.ManyToManyField(Condition, blank=True)
+ parent = parent.parent
- class Meta:
- ordering = ('label', )
- verbose_name = _('AttributeEntity')
- verbose_name_plural = _('AttributeEntities')
+ super(AttributeEntity, self).save(*args, **kwargs)
- class MPTTMeta:
- order_insertion_by = ['title']
+ # recursively save children
+ for child in self.children.all():
+ child.save()
- def __str__(self):
- return self.label
+ def clean(self):
+ self.path = AttributeEntity.build_path(self.key, self.parent)
+ AttributeEntityUniquePathValidator(self)()
@property
def range(self):
@@ -61,6 +119,14 @@ def has_options(self):
def has_conditions(self):
return bool(self.conditions.all())
+ @classmethod
+ def build_path(self, key, parent):
+ path = key
+ while parent:
+ path = parent.key + '/' + path
+ parent = parent.parent
+ return path
+
@python_2_unicode_compatible
class Attribute(AttributeEntity):
@@ -82,17 +148,32 @@ class Attribute(AttributeEntity):
(VALUE_TYPE_OPTIONS, _('Options'))
)
- value_type = models.CharField(max_length=8, choices=VALUE_TYPE_CHOICES)
- unit = models.CharField(max_length=64, blank=True, null=True)
-
- optionsets = models.ManyToManyField('options.OptionSet', blank=True)
+ value_type = models.CharField(
+ max_length=8, choices=VALUE_TYPE_CHOICES,
+ verbose_name=_('Value type'),
+ help_text=_('Type of values for this attribute.')
+ )
+ unit = models.CharField(
+ max_length=64, blank=True, null=True,
+ verbose_name=_('Unit'),
+ help_text=_('Unit of values for this attribute.')
+ )
+ optionsets = models.ManyToManyField(
+ 'options.OptionSet', blank=True,
+ verbose_name=_('Option sets'),
+ help_text=_('Option sets for this attribute.')
+ )
class Meta:
verbose_name = _('Attribute')
verbose_name_plural = _('Attributes')
def __str__(self):
- return self.label
+ return self.uri or self.key
+
+ def save(self, *args, **kwargs):
+ self.is_attribute = True
+ super(Attribute, self).save(*args, **kwargs)
@property
def options(self):
@@ -103,63 +184,41 @@ def options(self):
return options_list
-def post_save_attribute_entity(sender, **kwargs):
-
- if not kwargs['raw']:
- instance = kwargs['instance']
-
- # init fields
- instance.label = instance.title
- instance.is_attribute = hasattr(instance, 'attribute')
- instance.parent_collection = None
-
- # set parent_collection if the entity is a collection itself
- if instance.is_collection and not instance.is_attribute:
- instance.parent_collection = instance
-
- # loop over parents
- parent = instance.parent
- while parent:
- # set parent_collection if it is not yet set and if parent is a collection
- if not instance.parent_collection and parent.is_collection:
- instance.parent_collection = parent
-
- # update own full name
- instance.label = parent.title + '.' + instance.label
-
- parent = parent.parent
-
- post_save.disconnect(post_save_attribute_entity, sender=sender)
- instance.save()
- post_save.connect(post_save_attribute_entity, sender=sender)
-
- # update the full name and parent_collection of children
- # this makes it recursive
- for child in instance.children.all():
- child.save()
-
-
-post_save.connect(post_save_attribute_entity, sender=AttributeEntity)
-post_save.connect(post_save_attribute_entity, sender=Attribute)
-
-
@python_2_unicode_compatible
class VerboseName(models.Model, TranslationMixin):
- attribute_entity = models.OneToOneField('AttributeEntity')
-
- name_en = models.CharField(max_length=256)
- name_de = models.CharField(max_length=256)
-
- name_plural_en = models.CharField(max_length=256)
- name_plural_de = models.CharField(max_length=256)
+ attribute_entity = models.OneToOneField(
+ 'AttributeEntity',
+ verbose_name=_('Attribute entity'),
+ help_text=_('Attribute/entity this verbose name belongs to.')
+ )
+ name_en = models.CharField(
+ max_length=256,
+ verbose_name=_('Name (en)'),
+ help_text=_('English name displayed for this attribute/entity (e.g. project).')
+ )
+ name_de = models.CharField(
+ max_length=256,
+ verbose_name=_('Name (de)'),
+ help_text=_('German name displayed for this attribute/entity (e.g. Projekt).')
+ )
+ name_plural_en = models.CharField(
+ max_length=256,
+ verbose_name=_('Plural name (en)'),
+ help_text=_('English plural name displayed for this attribute/entity (e.g. projects).')
+ )
+ name_plural_de = models.CharField(
+ max_length=256,
+ verbose_name=_('Plural name (de)'),
+ help_text=_('German plural name displayed for this attribute/entity (e.g. Projekte).')
+ )
class Meta:
- verbose_name = _('VerboseName')
- verbose_name_plural = _('VerboseNames')
+ verbose_name = _('Verbose name')
+ verbose_name_plural = _('Verbose names')
def __str__(self):
- return self.attribute_entity.label
+ return self.attribute_entity.uri
@property
def name(self):
@@ -173,11 +232,23 @@ def name_plural(self):
@python_2_unicode_compatible
class Range(models.Model, TranslationMixin):
- attribute = models.OneToOneField('Attribute')
-
- minimum = models.FloatField()
- maximum = models.FloatField()
- step = models.FloatField()
+ attribute = models.OneToOneField(
+ 'Attribute',
+ verbose_name=_('Attribute'),
+ help_text=_('Attribute this verbose name belongs to.')
+ )
+ minimum = models.FloatField(
+ verbose_name=_('Minimum'),
+ help_text=_('Minimal value for this attribute.')
+ )
+ maximum = models.FloatField(
+ verbose_name=_('Maximum'),
+ help_text=_('Maximum value for this attribute.')
+ )
+ step = models.FloatField(
+ verbose_name=_('Step'),
+ help_text=_('Step in which this attribute can be incremented/decremented.')
+ )
class Meta:
ordering = ('attribute', )
@@ -185,4 +256,4 @@ class Meta:
verbose_name_plural = _('Ranges')
def __str__(self):
- return self.attribute.label
+ return self.attribute.uri
diff --git a/apps/domain/renderers.py b/apps/domain/renderers.py
index 38cb6bac73..a7d27c432b 100644
--- a/apps/domain/renderers.py
+++ b/apps/domain/renderers.py
@@ -1,141 +1,82 @@
-from __future__ import unicode_literals
+from apps.core.renderers import BaseXMLRenderer
-from django.utils.xmlutils import SimplerXMLGenerator
-from django.utils.six.moves import StringIO
-from django.utils.encoding import smart_text
-from rest_framework.renderers import BaseRenderer
+class XMLRenderer(BaseXMLRenderer):
-class XMLRenderer(BaseRenderer):
- """
- Renderer which serializes to XML.
- """
-
- media_type = 'application/xml'
- format = 'xml'
-
- def render(self, data):
- """
- Renders 'data' into serialized XML.
- """
- if data is None:
- return ''
-
- stream = StringIO()
-
- xml = SimplerXMLGenerator(stream, "utf-8")
- xml.startDocument()
- xml.startElement('Domain', {
+ def render_document(self, xml, attribute_entities):
+ xml.startElement('domain', {
'xmlns:dc': "http://purl.org/dc/elements/1.1/"
})
- for attribute_entity in data:
+ for attribute_entity in attribute_entities:
if attribute_entity['is_attribute']:
- self._attribute(xml, attribute_entity)
+ self.render_attribute(xml, attribute_entity)
else:
- self._attribute_entity(xml, attribute_entity)
-
- xml.endElement('Domain')
- xml.endDocument()
- return stream.getvalue()
-
- def _attribute(self, xml, attribute):
- xml.startElement('Attribute', {})
- self._text_element(xml, 'dc:title', {}, attribute["title"])
- self._text_element(xml, 'dc:description', {}, attribute["description"])
- self._text_element(xml, 'dc:uri', {}, attribute["uri"])
- self._text_element(xml, 'is_collection', {}, attribute["is_collection"])
- self._text_element(xml, 'value_type', {}, attribute["value_type"])
- self._text_element(xml, 'unit', {}, attribute["unit"])
-
- if 'options' in attribute and attribute['options']:
- xml.startElement('Options', {})
-
- for option in attribute['options']:
- self._option(xml, option)
-
- xml.endElement('Options')
-
- if 'range' in attribute and attribute['range']:
- self._range(xml, attribute['range'])
-
- if 'conditions' in attribute and attribute['conditions']:
- xml.startElement('Conditions', {})
-
- for conditions in attribute['conditions']:
- self._conditions(xml, conditions)
+ self.render_attribute_entity(xml, attribute_entity)
- xml.endElement('Conditions')
+ xml.endElement('domain')
- if 'verbosename' in attribute and attribute['verbosename']:
- self._verbosename(xml, attribute['verbosename'])
+ def render_attribute_entity(self, xml, attribute_entity):
+ xml.startElement('entity', {})
+ self.render_text_element(xml, 'dc:uri', {}, attribute_entity["uri"])
+ self.render_text_element(xml, 'dc:comment', {}, attribute_entity["comment"])
+ self.render_text_element(xml, 'is_collection', {}, attribute_entity["is_collection"])
+ self.render_verbosename(xml, attribute_entity['verbosename'])
- xml.endElement('Attribute')
-
- def _attribute_entity(self, xml, attribute_entity):
- xml.startElement('AttributeEntity', {})
- self._text_element(xml, 'dc:title', {}, attribute_entity["title"])
- self._text_element(xml, 'dc:description', {}, attribute_entity["description"])
- self._text_element(xml, 'dc:uri', {}, attribute_entity["uri"])
- self._text_element(xml, 'is_collection', {}, attribute_entity["is_collection"])
+ if 'conditions' in attribute_entity and attribute_entity['conditions']:
+ xml.startElement('conditions', {})
+ for condition_uri in attribute_entity['conditions']:
+ self.render_text_element(xml, 'condition', {'dc:uri': condition_uri}, None)
+ xml.endElement('conditions')
if 'children' in attribute_entity:
- xml.startElement('Children', {})
-
+ xml.startElement('children', {})
for child in attribute_entity['children']:
if child['is_attribute']:
- self._attribute(xml, child)
+ self.render_attribute(xml, child)
else:
- self._attribute_entity(xml, child)
-
- xml.endElement('Children')
-
- if 'conditions' in attribute_entity and attribute_entity['conditions']:
- xml.startElement('Conditions', {})
+ self.render_attribute_entity(xml, child)
+ xml.endElement('children')
+
+ xml.endElement('entity')
+
+ def render_attribute(self, xml, attribute):
+ xml.startElement('attribute', {})
+ self.render_text_element(xml, 'dc:uri', {}, attribute["uri"])
+ self.render_text_element(xml, 'dc:comment', {}, attribute["comment"])
+ self.render_text_element(xml, 'is_collection', {}, attribute["is_collection"])
+ self.render_text_element(xml, 'value_type', {}, attribute["value_type"])
+ self.render_text_element(xml, 'unit', {}, attribute["unit"])
+ self.render_range(xml, attribute['range'])
+ self.render_verbosename(xml, attribute['verbosename'])
+
+ if 'optionsets' in attribute and attribute['optionsets']:
+ xml.startElement('optionsets', {})
+ for optionset_uri in attribute['optionsets']:
+ self.render_text_element(xml, 'optionset', {'dc:uri': optionset_uri}, None)
+ xml.endElement('optionsets')
- for conditions in attribute_entity['conditions']:
- self._conditions(xml, conditions)
-
- xml.endElement('Conditions')
-
- if 'verbosename' in attribute_entity and attribute_entity['verbosename']:
- self._verbosename(xml, attribute_entity['verbosename'])
-
- xml.endElement('AttributeEntity')
+ if 'conditions' in attribute and attribute['conditions']:
+ xml.startElement('conditions', {})
+ for condition_uri in attribute['conditions']:
+ self.render_text_element(xml, 'condition', {'dc:uri': condition_uri}, None)
+ xml.endElement('conditions')
- def _option(self, xml, option):
- xml.startElement('Option', {})
- self._text_element(xml, 'order', {}, option["order"])
- self._text_element(xml, 'text_de', {}, option["text_de"])
- self._text_element(xml, 'text_en', {}, option["text_en"])
- self._text_element(xml, 'additional_input', {}, option["additional_input"])
- xml.endElement('Option')
+ xml.endElement('attribute')
- def _range(self, xml, range):
+ def render_range(self, xml, range):
xml.startElement('range', {})
- self._text_element(xml, 'minimum', {}, range["minimum"])
- self._text_element(xml, 'maximum', {}, range["maximum"])
- self._text_element(xml, 'step', {}, range["step"])
+ if range:
+ self.render_text_element(xml, 'minimum', {}, range["minimum"])
+ self.render_text_element(xml, 'maximum', {}, range["maximum"])
+ self.render_text_element(xml, 'step', {}, range["step"])
xml.endElement('range')
- def _conditions(self, xml, conditions):
- xml.startElement('condition', {})
- self._text_element(xml, 'source', {}, conditions["source"])
- self._text_element(xml, 'relation', {}, conditions["relation"])
- self._text_element(xml, 'target_text', {}, conditions["target_text"])
- self._text_element(xml, 'target_option', {}, conditions["target_option"])
- xml.endElement('condition')
-
- def _verbosename(self, xml, verbosename):
+ def render_verbosename(self, xml, verbosename):
xml.startElement('verbosename', {})
- self._text_element(xml, 'name_en', {}, verbosename["name_en"])
- self._text_element(xml, 'name_de', {}, verbosename["name_de"])
- self._text_element(xml, 'name_plural_en', {}, verbosename["name_plural_en"])
- self._text_element(xml, 'name_plural_de', {}, verbosename["name_plural_de"])
+ if verbosename:
+ self.render_text_element(xml, 'name', {'lang': 'en'}, verbosename["name_en"])
+ self.render_text_element(xml, 'name', {'lang': 'de'}, verbosename["name_de"])
+ self.render_text_element(xml, 'name_plural', {'lang': 'en'}, verbosename["name_plural_en"])
+ self.render_text_element(xml, 'name_plural', {'lang': 'de'}, verbosename["name_plural_de"])
xml.endElement('verbosename')
-
- def _text_element(self, xml, tag, attributes, text):
- xml.startElement(tag, attributes)
- if text is not None:
- xml.characters(smart_text(text))
- xml.endElement(tag)
diff --git a/apps/domain/serializers.py b/apps/domain/serializers.py
index 487ca441f9..221bb22b43 100644
--- a/apps/domain/serializers.py
+++ b/apps/domain/serializers.py
@@ -3,8 +3,8 @@
from apps.options.models import OptionSet
from apps.conditions.models import Condition
-from .models import *
-
+from .models import AttributeEntity, Attribute, Range, VerboseName
+from .validators import AttributeEntityUniquePathValidator
class AttributeEntityNestedSerializer(serializers.ModelSerializer):
@@ -14,8 +14,7 @@ class Meta:
model = AttributeEntity
fields = (
'id',
- 'title',
- 'label',
+ 'path',
'is_collection',
'is_attribute',
'children'
@@ -32,51 +31,53 @@ class Meta:
model = AttributeEntity
fields = (
'id',
- 'label'
+ 'path'
)
-class AttributeEntitySerializer(serializers.ModelSerializer):
+class AttributeIndexSerializer(serializers.ModelSerializer):
class Meta:
- model = AttributeEntity
+ model = Attribute
fields = (
'id',
- 'parent',
- 'title',
- 'description',
- 'uri',
- 'is_collection',
- 'conditions'
+ 'path'
)
-class AttributeIndexSerializer(AttributeEntitySerializer):
+class AttributeEntitySerializer(serializers.ModelSerializer):
class Meta:
- model = Attribute
+ model = AttributeEntity
fields = (
'id',
- 'label'
+ 'parent',
+ 'uri_prefix',
+ 'key',
+ 'comment',
+ 'is_collection',
+ 'conditions'
)
+ validators = (AttributeEntityUniquePathValidator(), )
-class AttributeSerializer(AttributeEntitySerializer):
+class AttributeSerializer(serializers.ModelSerializer):
class Meta:
model = Attribute
fields = (
'id',
'parent',
- 'title',
- 'description',
- 'uri',
+ 'uri_prefix',
+ 'key',
+ 'comment',
'value_type',
'unit',
'is_collection',
'optionsets',
'conditions'
)
+ validators = (AttributeEntityUniquePathValidator(), )
class RangeSerializer(serializers.ModelSerializer):
@@ -112,7 +113,7 @@ class Meta:
model = OptionSet
fields = (
'id',
- 'title',
+ 'key',
)
@@ -122,7 +123,7 @@ class Meta:
model = Condition
fields = (
'id',
- 'title',
+ 'key'
)
@@ -166,33 +167,37 @@ class ExportSerializer(serializers.ModelSerializer):
value_type = serializers.CharField(source='attribute.value_type', read_only=True)
unit = serializers.CharField(source='attribute.unit', read_only=True)
- # options = ExportOptionSerializer(source='attribute.options', many=True, read_only=True)
range = ExportRangeSerializer(source='attribute.range', read_only=True)
verbosename = ExportVerboseNameSerializer(read_only=True)
- conditions = ExportConditionSerializer(many=True, read_only=True)
+ optionsets = serializers.SerializerMethodField()
+ conditions = serializers.SerializerMethodField()
children = serializers.SerializerMethodField()
class Meta:
model = AttributeEntity
fields = (
- 'id',
- 'title',
- 'description',
'uri',
+ 'comment',
'is_collection',
'is_attribute',
'value_type',
'unit',
'is_collection',
- 'conditions',
- # 'options',
'range',
'verbosename',
'conditions',
+ 'optionsets',
'children'
)
def get_children(self, obj):
# get the children from the cached mptt tree
return ExportSerializer(obj.get_children(), many=True, read_only=True).data
+
+ def get_optionsets(self, obj):
+ if hasattr(obj, 'attribute'):
+ return [option.uri for option in obj.attribute.optionsets.all()]
+
+ def get_conditions(self, obj):
+ return [condition.uri for condition in obj.conditions.all()]
diff --git a/apps/domain/templates/domain/domain.html b/apps/domain/templates/domain/domain.html
index 421ed638ec..f61b786447 100644
--- a/apps/domain/templates/domain/domain.html
+++ b/apps/domain/templates/domain/domain.html
@@ -78,7 +78,7 @@ {% trans 'Export' %}
{% trans 'Domain' %}
-
@@ -94,14 +94,14 @@ {% trans 'Domain' %}
{% trans 'Entity' %}
{% trans 'Attribute' %}
- {$ entity.label $}
+ {$ entity.path $}
collection
diff --git a/apps/domain/templates/domain/domain_export.html b/apps/domain/templates/domain/domain_export.html
index 5ea8975cb1..9c37484c33 100644
--- a/apps/domain/templates/domain/domain_export.html
+++ b/apps/domain/templates/domain/domain_export.html
@@ -8,9 +8,9 @@ {% trans 'Domain '%}
{% for entity in entities %}
{% if entity.is_attribute %}
- {% trans 'Attribute' %} – {{ entity.label }}
+ {% trans 'Attribute' %} – {{ entity.path }}
{% else %}
- {% trans 'Entity' %} – {{ entity.label }}
+ {% trans 'Entity' %} – {{ entity.path }}
{% endif%}
diff --git a/apps/domain/templates/domain/domain_modal_delete_entities.html b/apps/domain/templates/domain/domain_modal_delete_entities.html
index 5164ffcea6..6fe3675f38 100644
--- a/apps/domain/templates/domain/domain_modal_delete_entities.html
+++ b/apps/domain/templates/domain/domain_modal_delete_entities.html
@@ -9,7 +9,7 @@
- {% trans 'Delete attribute set' %}
+ {% trans 'Delete entity' %}
{% trans 'Delete attribute' %}
@@ -33,7 +33,7 @@