diff --git a/.coveragerc b/.coveragerc index aeca2a7b9..7433c3ad1 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 8baa6e5af..000000000 --- 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 5720db8e6..000000000 --- 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 9477353e1..070556af8 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 243229485..dae51b9b8 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 000000000..e2778fb44 --- /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 000000000..4f133e9e5 --- /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 b9e83d86b..49354b440 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 000000000..3374af8fe --- /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 40277c43e..790b3a3cc 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 228d2a05f..e42073905 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 $}