Repository: ddionrails/ddionrails

Issue:
ddionrails.base.mixins uses eval for questionable purposes
### Subject of the issue

eval executes arbitrary python code and is used by the modelMixin to get attributes of objects:

https://github.com/ddionrails/ddionrails/blob/0a9b1d2d490ad7fc9ce90d5e6f68bfaf06c06bf3/ddionrails/base/mixins.py#L94-L106

https://github.com/ddionrails/ddionrails/blob/0a9b1d2d490ad7fc9ce90d5e6f68bfaf06c06bf3/ddionrails/base/mixins.py#L134-L143

https://github.com/ddionrails/ddionrails/blob/0a9b1d2d490ad7fc9ce90d5e6f68bfaf06c06bf3/ddionrails/base/mixins.py#L134-L155

Dedicated functionality should be used to do this instead, i.e. getattr().
This might have to be done recursively since the functions seem to have been used to get attributes over an arbitrary number of relations, e.g. object.objectAttribute.attribute or object.attribute.


Discussion:
@hansendx Did you find any occurences where .get_attribute() is used?
I did a quick search for ".get_attribute" and found nothing.

Just remove it?

Resolving patch:
diff --git a/ddionrails/base/mixins.py b/ddionrails/base/mixins.py
--- a/ddionrails/base/mixins.py
+++ b/ddionrails/base/mixins.py
@@ -1,6 +1,9 @@
-"""
-Mixins for DDI on Rails.
-"""
+# -*- coding: utf-8 -*-
+
+""" Mixins for ddionrails.base app """
+
+from typing import Dict
+
 from django import forms
 
 from config.helpers import render_markdown
@@ -64,14 +67,14 @@ def get_or_create(cls, x, lower_strings=True):
         return cls.objects.get_or_create(**definition)[0]
 
     @classmethod
-    def get(cls, x):
+    def get(cls, parameters: Dict):
         """
         Default for the get_or_create based on a dict.
 
         The method uses only relevant identifiers based on ``DOR.id_fields``.
         """
         try:
-            definition = {key: x[key] for key in cls.DOR.id_fields}
+            definition = {key: parameters[key] for key in cls.DOR.id_fields}
             result = cls.objects.get(**definition)
         except cls.DoesNotExist:
             result = None
@@ -90,19 +93,19 @@ class Meta:
 
         return DefaultForm
 
-    def to_dict(self):
+    def to_dict(self) -> Dict:
         """
         Uses the ``DOR.io_fields`` attribute to generate a default
         dict object for the current instance.
         """
-        default_dict = {}
-        for field_name in self.DOR.io_fields:
-            field = eval("self.%s" % field_name)
+        dictionary = dict()
+        for field in self.DOR.io_fields:
+            value = getattr(self, field)
             try:
-                default_dict[field_name] = field.pk
+                dictionary[field] = value.pk
             except AttributeError:
-                default_dict[field_name] = field
-        return default_dict
+                dictionary[field] = value
+        return dictionary
 
     def title(self):
         """
@@ -130,31 +133,16 @@ def html_description(self):
             html = ""
         return html
 
-    def get_attribute(self, attribute, default=None):
-        """
-        Example::
-
-            study_id = variable.get_attribute("self.dataset.study.id")
-        """
-        try:
-            return eval(attribute)
-        except AttributeError:
-            return default
-
-    def string_id(self):
-        a = []
-        for field_name in self.DOR.id_fields:
-            field = eval("self.%s" % field_name)
+    def __str__(self):
+        """ Returns a string reprensentation of the instance, using DOR.id_fields """
+        result = []
+        for field in self.DOR.id_fields:
+            value = getattr(self, field)
             try:
-                s = field.string_id()
+                result.append(value.string_id())
             except AttributeError:
-                s = str(field)
-            a.append(s)
-        a = "/".join(a)
-        return a
-
-    def __str__(self):
-        return self.string_id()
+                result.append(str(value))
+        return "/".join(result)
 
 
 class AdminMixin:


Test patch:
diff --git a/tests/base/test_mixins.py b/tests/base/test_mixins.py
--- a/tests/base/test_mixins.py
+++ b/tests/base/test_mixins.py
@@ -99,34 +99,18 @@ def test_html_description_method_without_description(self):
         result = mixin.html_description()
         assert result == ""
 
-    def test_get_attribute_method(self):
+    def test_string_method_single_id_field(self):
         mixin = ModelMixin()
         mixin.name = "some-name"
-        result = mixin.get_attribute("self.name")
-        assert result == mixin.name
+        assert mixin.name == str(mixin)
 
-    def test_get_attribute_method_default(self):
-        mixin = ModelMixin()
-        result = mixin.get_attribute("self.name")
-        assert result is None
-
-    def test_string_id_method_single_id_field(self):
-        mixin = ModelMixin()
-        mixin.name = "some-name"
-        assert mixin.string_id() == mixin.name
-
-    def test_string_id_method_multiple_id_field(self):
+    def test_string_method_multiple_id_field(self):
         mixin = ModelMixin()
         mixin.DOR.id_fields = ["id1", "id2"]
         mixin.id1 = "name-1"
         mixin.id2 = "name-2"
-        assert mixin.string_id() == mixin.id1 + "/" + mixin.id2
-
-    def test_string_method(self, mocker):
-        mixin = ModelMixin()
-        with mocker.patch("ddionrails.base.mixins.ModelMixin.string_id", return_value=""):
-            assert str(mixin) == ""
-            mixin.string_id.assert_called_once()
+        expected = f"{mixin.id1}/{mixin.id2}"
+        assert expected == str(mixin)
 
 
 @pytest.fixture