Repository: ddionrails/ddionrails

Issue:
Update models in concepts app
- [x] #477
- [x] #475 
- [x] #474
- [x] #473 
- [x] #472



Discussion:

Resolving patch:
diff --git a/ddionrails/concepts/migrations/0002_auto_20190522_0901.py b/ddionrails/concepts/migrations/0002_auto_20190522_0901.py
new file mode 100644
--- /dev/null
+++ b/ddionrails/concepts/migrations/0002_auto_20190522_0901.py
@@ -0,0 +1,247 @@
+# Generated by Django 2.2.1 on 2019-05-22 09:01
+
+import config.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("concepts", "0001_initial")]
+
+    operations = [
+        migrations.AddField(
+            model_name="analysisunit",
+            name="label_de",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the analysis unit (German)",
+                max_length=255,
+                verbose_name="Label (German)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="concept",
+            name="label_de",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the concept (German)",
+                max_length=255,
+                verbose_name="Label (German)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="conceptualdataset",
+            name="label_de",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the conceptual dataset (German)",
+                max_length=255,
+                verbose_name="Label (German)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="topic",
+            name="label_de",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the topic (German)",
+                max_length=255,
+                verbose_name="Label (German)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="analysisunit",
+            name="description",
+            field=models.TextField(
+                blank=True,
+                help_text="Description of the analysis unit (Markdown)",
+                verbose_name="Description (Markdown)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="analysisunit",
+            name="label",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the analysis unit (English)",
+                max_length=255,
+                verbose_name="Label (English)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="analysisunit",
+            name="name",
+            field=models.CharField(
+                help_text="Name of the analysis unit (Lowercase)",
+                max_length=255,
+                unique=True,
+                validators=[config.validators.validate_lowercase],
+            ),
+        ),
+        migrations.AlterField(
+            model_name="concept",
+            name="description",
+            field=models.TextField(
+                blank=True,
+                help_text="Description of the concept (Markdown)",
+                verbose_name="Description (Markdown)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="concept",
+            name="label",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the concept (English)",
+                max_length=255,
+                verbose_name="Label (English)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="concept",
+            name="name",
+            field=models.CharField(
+                db_index=True,
+                help_text="Name of the concept (Lowercase)",
+                max_length=255,
+                unique=True,
+                validators=[config.validators.validate_lowercase],
+                verbose_name="concept name",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="concept",
+            name="topics",
+            field=models.ManyToManyField(
+                help_text="ManyToMany relation to concepts.Topic",
+                related_name="concepts",
+                to="concepts.Topic",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="conceptualdataset",
+            name="description",
+            field=models.TextField(
+                blank=True,
+                help_text="Description of the conceptual dataset (Markdown)",
+                verbose_name="Description (Markdown)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="conceptualdataset",
+            name="label",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the conceptual dataset (English)",
+                max_length=255,
+                verbose_name="Label (English)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="conceptualdataset",
+            name="name",
+            field=models.CharField(
+                help_text="Name of the conceptual dataset (Lowercase)",
+                max_length=255,
+                unique=True,
+                validators=[config.validators.validate_lowercase],
+            ),
+        ),
+        migrations.AlterField(
+            model_name="period",
+            name="definition",
+            field=models.CharField(
+                blank=True, help_text="Definition of the period", max_length=255
+            ),
+        ),
+        migrations.AlterField(
+            model_name="period",
+            name="description",
+            field=models.TextField(
+                blank=True,
+                help_text="Description of the period (Markdown)",
+                verbose_name="Description (Markdown)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="period",
+            name="label",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the period (English)",
+                max_length=255,
+                verbose_name="Label (English)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="period",
+            name="name",
+            field=models.CharField(
+                help_text="Name of the period (Lowercase)",
+                max_length=255,
+                validators=[config.validators.validate_lowercase],
+            ),
+        ),
+        migrations.AlterField(
+            model_name="period",
+            name="study",
+            field=models.ForeignKey(
+                help_text="Foreign key to studies.Study",
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="periods",
+                to="studies.Study",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="topic",
+            name="description",
+            field=models.TextField(
+                blank=True,
+                help_text="Description of the topic (Markdown)",
+                verbose_name="Description (Markdown)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="topic",
+            name="label",
+            field=models.CharField(
+                blank=True,
+                help_text="Label of the topic (English)",
+                max_length=255,
+                verbose_name="Label (English)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="topic",
+            name="name",
+            field=models.CharField(
+                help_text="Name of the topic (Lowercase)",
+                max_length=255,
+                unique=True,
+                validators=[config.validators.validate_lowercase],
+            ),
+        ),
+        migrations.AlterField(
+            model_name="topic",
+            name="parent",
+            field=models.ForeignKey(
+                blank=True,
+                help_text="Foreign key to concepts.Topic",
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="children",
+                to="concepts.Topic",
+                verbose_name="Parent (concepts.Topic)",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="topic",
+            name="study",
+            field=models.ForeignKey(
+                help_text="Foreign key to studies.Study",
+                on_delete=django.db.models.deletion.CASCADE,
+                to="studies.Study",
+            ),
+        ),
+    ]
diff --git a/ddionrails/concepts/models.py b/ddionrails/concepts/models.py
--- a/ddionrails/concepts/models.py
+++ b/ddionrails/concepts/models.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+""" Model definitions for ddionrails.concepts app """
+
 from django.db import models
 from django.urls import reverse
 
@@ -8,19 +11,58 @@
 
 
 class Topic(models.Model, ModelMixin, ElasticMixin):
+    """
+    Stores a single topic,
+    related to :model:`studies.Study` and :model:`concepts.Topic`.
+    """
 
-    name = models.CharField(max_length=255, validators=[validate_lowercase], unique=True)
-    label = models.CharField(max_length=255, blank=True)
-    description = models.TextField(blank=True)
-    study = models.ForeignKey(Study, on_delete=models.CASCADE)
+    # attributes
+    name = models.CharField(
+        max_length=255,
+        validators=[validate_lowercase],
+        unique=True,
+        help_text="Name of the topic (Lowercase)",
+    )
+    label = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (English)",
+        help_text="Label of the topic (English)",
+    )
+    label_de = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (German)",
+        help_text="Label of the topic (German)",
+    )
+    description = models.TextField(
+        blank=True,
+        verbose_name="Description (Markdown)",
+        help_text="Description of the topic (Markdown)",
+    )
+
+    # relations
+    study = models.ForeignKey(
+        Study, on_delete=models.CASCADE, help_text="Foreign key to studies.Study"
+    )
     parent = models.ForeignKey(
-        "self", related_name="children", null=True, blank=True, on_delete=models.CASCADE
+        "self",
+        related_name="children",
+        null=True,
+        blank=True,
+        on_delete=models.CASCADE,
+        verbose_name="Parent (concepts.Topic)",
+        help_text="Foreign key to concepts.Topic",
     )
 
     class Meta:
+        """ Django's metadata options """
+
         unique_together = ("study", "name")
 
     class DOR:
+        """ ddionrails' metadata options """
+
         id_fields = ["study", "name"]
         io_fields = ["study", "name", "label", "description", "parent"]
 
@@ -35,39 +77,58 @@ def get_children(cls, topic_id):
 class Concept(models.Model, ModelMixin, ElasticMixin):
     """
     Stores a single concept,
-    related to :model:`data.Variable` and :model:`instruments.ConceptQuestion`.
+    related to :model:`data.Variable`, :model:`concepts.Topic` and :model:`instruments.ConceptQuestion`.
     """
 
+    # attributes
     name = models.CharField(
         max_length=255,
         validators=[validate_lowercase],
         unique=True,
         db_index=True,
         verbose_name="concept name",
-        help_text="Name of the concept.",
+        help_text="Name of the concept (Lowercase)",
     )
     label = models.CharField(
-        max_length=255, blank=True, help_text="Label (English) of the concept."
+        max_length=255,
+        blank=True,
+        verbose_name="Label (English)",
+        help_text="Label of the concept (English)",
+    )
+    label_de = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (German)",
+        help_text="Label of the concept (German)",
     )
     description = models.TextField(
-        blank=True, help_text="Description of the concept using Markdown."
+        blank=True,
+        verbose_name="Description (Markdown)",
+        help_text="Description of the concept (Markdown)",
+    )
+
+    # relations
+    topics = models.ManyToManyField(
+        Topic, related_name="concepts", help_text="ManyToMany relation to concepts.Topic"
     )
-    topics = models.ManyToManyField(Topic, related_name="concepts")
 
     DOC_TYPE = "concept"
 
     class DOR(ModelMixin.DOR):
+        """ ddionrails' metadata options """
+
         id_fields = ["name"]
         io_fields = ["name", "label", "description"]
 
-    def __str__(self):
-        """ Returns a string representation of the concept using its name """
-        return "/concept/%s" % self.name
+    def __str__(self) -> str:
+        """ Returns a string representation using the "name" field """
+        return f"/concept/{self.name}"
 
-    def get_absolute_url(self):
+    def get_absolute_url(self) -> str:
+        """ Returns a canonical URL for the model using the "name" field """
         return reverse("concepts:concept_detail_name", kwargs={"concept_name": self.name})
 
-    def index(self):
+    def index(self) -> None:
         """ Indexes the concept in Elasticsearch (name, label and related study) """
         if self.label and self.label != "":
             label = self.label
@@ -89,7 +150,7 @@ def index(self):
         self.set_elastic(dict(name=self.name, label=label, study=study))
 
     @classmethod
-    def index_all(cls):
+    def index_all(cls) -> None:
         """ Indexes all concepts in Elasticsearch using the index method """
         for concept in cls.objects.all():
             concept.index()
@@ -97,6 +158,9 @@ def index_all(cls):
 
 class Period(models.Model, ModelMixin):
     """
+    Stores a single period,
+    related to :model:`studies.Study`.
+
     Periods define time references.
 
     For the definition, use one of the following formats:
@@ -107,59 +171,137 @@ class Period(models.Model, ModelMixin):
     -   Range: ``2010-01:2011-12``
     """
 
-    study = models.ForeignKey(Study, related_name="periods", on_delete=models.CASCADE)
-    name = models.CharField(max_length=255, validators=[validate_lowercase])
-    label = models.CharField(max_length=255, blank=True)
-    description = models.TextField(blank=True)
-    definition = models.CharField(max_length=255, blank=True)
+    # attributes
+    name = models.CharField(
+        max_length=255,
+        validators=[validate_lowercase],
+        help_text="Name of the period (Lowercase)",
+    )
+    label = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (English)",
+        help_text="Label of the period (English)",
+    )
+    description = models.TextField(
+        blank=True,
+        verbose_name="Description (Markdown)",
+        help_text="Description of the period (Markdown)",
+    )
+    definition = models.CharField(
+        max_length=255, blank=True, help_text="Definition of the period"
+    )
+
+    # relations
+    study = models.ForeignKey(
+        Study,
+        related_name="periods",
+        on_delete=models.CASCADE,
+        help_text="Foreign key to studies.Study",
+    )
 
     class Meta:
+        """ Django's metadata options """
+
         unique_together = ("study", "name")
 
     class DOR:
+        """ ddionrails' metadata options """
+
         id_fields = ["study", "name"]
         io_fields = ["study", "name", "label", "description", "definition"]
 
-    def title(self):
-        return self.label if self.label != "" else self.name
+    def __str__(self) -> str:
+        """ Returns a string representation using the "name" field """
+        return f"/period/{self.name}"
 
-    def is_range(self):
-        return ":" in self.definition
+    def title(self) -> str:
+        """ Returns a title representation using the "label" field, with "name" field as fallback """
+        return str(self.label) if self.label != "" else str(self.name)
 
-    def __str__(self):
-        return "/period/%s" % self.name
+    def is_range(self) -> bool:
+        """ Returns if "definition" defines a range """
+        return ":" in self.definition
 
 
 class AnalysisUnit(models.Model, ModelMixin):
     """
+    Stores a single analysis unit.
+
     Analysis units refer to real world objects.
 
     * p = individual (person)
     * h = household
     """
 
-    name = models.CharField(max_length=255, unique=True, validators=[validate_lowercase])
-    label = models.CharField(max_length=255, blank=True)
-    description = models.TextField(blank=True)
+    name = models.CharField(
+        max_length=255,
+        unique=True,
+        validators=[validate_lowercase],
+        help_text="Name of the analysis unit (Lowercase)",
+    )
+    label = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (English)",
+        help_text="Label of the analysis unit (English)",
+    )
+    label_de = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (German)",
+        help_text="Label of the analysis unit (German)",
+    )
+    description = models.TextField(
+        blank=True,
+        verbose_name="Description (Markdown)",
+        help_text="Description of the analysis unit (Markdown)",
+    )
 
-    def title(self):
-        return self.label if self.label != "" else self.name
+    def __str__(self) -> str:
+        """ Returns a string representation using the "name" field """
+        return f"/analysis_unit/{self.name}"
 
-    def __str__(self):
-        return "/analysis_unit/%s" % self.name
+    def title(self) -> str:
+        """ Returns a title representation using the "label" field, with "name" field as fallback """
+        return str(self.label) if self.label != "" else str(self.name)
 
 
 class ConceptualDataset(models.Model, ModelMixin):
     """
+    Stores a single conceptual dataset.
+
     Conceptual datasets group datasets.
     """
 
-    name = models.CharField(max_length=255, unique=True, validators=[validate_lowercase])
-    label = models.CharField(max_length=255, blank=True)
-    description = models.TextField(blank=True)
+    name = models.CharField(
+        max_length=255,
+        unique=True,
+        validators=[validate_lowercase],
+        help_text="Name of the conceptual dataset (Lowercase)",
+    )
+    label = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (English)",
+        help_text="Label of the conceptual dataset (English)",
+    )
+    label_de = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Label (German)",
+        help_text="Label of the conceptual dataset (German)",
+    )
+    description = models.TextField(
+        blank=True,
+        verbose_name="Description (Markdown)",
+        help_text="Description of the conceptual dataset (Markdown)",
+    )
 
-    def title(self):
-        return self.label if self.label != "" else self.name
+    def __str__(self) -> str:
+        """ Returns a string representation using the "name" field """
+        return f"/conceptual_dataset/{self.name}"
 
-    def __str__(self):
-        return "/conceptual_dataset/%s" % self.name
+    def title(self) -> str:
+        """ Returns a title representation using the "label" field, with "name" field as fallback """
+        return str(self.label) if self.label != "" else str(self.name)


Test patch: