Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce item levels #86

Merged
merged 4 commits into from Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 91 additions & 0 deletions eap_backend/eap_api/migrations/0011_auto_20220405_1609.py
@@ -0,0 +1,91 @@
# Generated by Django 3.2.8 on 2022-04-05 16:09

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
("eap_api", "0010_merge_0009_auto_20211110_1654_0009_auto_20211115_1615"),
]

operations = [
migrations.AddField(
model_name="argument",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="evidence",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="evidentialclaim",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="propertyclaim",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="propertyclaim",
name="level",
field=models.PositiveIntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name="propertyclaim",
name="property_claim",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="property_claims",
to="eap_api.propertyclaim",
),
),
migrations.AddField(
model_name="systemdescription",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="toplevelnormativegoal",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AlterField(
model_name="propertyclaim",
name="goal",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="property_claims",
to="eap_api.toplevelnormativegoal",
),
),
]
88 changes: 56 additions & 32 deletions eap_backend/eap_api/models.py
Expand Up @@ -18,6 +18,23 @@ class Shape(Enum):
CYLINDER = 3


class CaseItem(models.Model):
"""A class that all the assurance case items inherit from.

CaseItem is an abstract base class, meaning that it's not meant to have its own
table.
"""

name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
shape = Shape
created_date = models.DateTimeField(auto_now_add=True)

class Meta:
abstract = True


class AssuranceCase(models.Model):
name = models.CharField(max_length=200)
description = models.CharField(max_length=1000)
Expand All @@ -31,10 +48,7 @@ def was_published_recently(self):
return self.created_date >= timezone.now() - datetime.timedelta(days=1)


class TopLevelNormativeGoal(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class TopLevelNormativeGoal(CaseItem):
keywords = models.CharField(max_length=3000)
assurance_case = models.ForeignKey(
AssuranceCase, related_name="goals", on_delete=models.CASCADE
Expand All @@ -45,21 +59,14 @@ def __str__(self):
return self.name


class Context(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class Context(CaseItem):
shape = Shape.DIAMOND
created_date = models.DateTimeField(auto_now_add=True)
goal = models.ForeignKey(
TopLevelNormativeGoal, related_name="context", on_delete=models.CASCADE
)


class SystemDescription(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class SystemDescription(CaseItem):
shape = Shape.DIAMOND
goal = models.ForeignKey(
TopLevelNormativeGoal,
Expand All @@ -68,38 +75,55 @@ class SystemDescription(models.Model):
)


class PropertyClaim(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class PropertyClaim(CaseItem):
shape = Shape.ROUNDED_RECTANGLE
goal = models.ForeignKey(
TopLevelNormativeGoal, related_name="property_claims", on_delete=models.CASCADE
TopLevelNormativeGoal,
null=True,
blank=True,
related_name="property_claims",
on_delete=models.CASCADE,
)


class Argument(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
property_claim = models.ForeignKey(
"self",
null=True,
blank=True,
related_name="property_claims",
on_delete=models.CASCADE,
)
level = models.PositiveIntegerField()

def save(self, *args, **kwargs):
try:
parent_level = self.parent.level
except AttributeError:
# If the parent is a TopLevelNormativeGoal rather than a PropertyClaim, it
# doesn't have a level.
parent_level = 0
self.level = parent_level + 1
# TODO Is this the right place to assert these things?
has_goal_parent = bool(self.goal)
has_claim_parent = bool(self.property_claim)
if has_claim_parent and has_goal_parent:
raise ValueError("A PropertyClaim shouldn't have two parents.")
if not (has_claim_parent or has_goal_parent):
raise ValueError("A PropertyClaim should have a parent.")
super().save(*args, **kwargs)


class Argument(CaseItem):
shape = Shape.ROUNDED_RECTANGLE
property_claim = models.ManyToManyField(PropertyClaim, related_name="arguments")


class EvidentialClaim(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class EvidentialClaim(CaseItem):
shape = Shape.ROUNDED_RECTANGLE
argument = models.ForeignKey(
Argument, related_name="evidential_claims", on_delete=models.CASCADE
)


class Evidence(models.Model):
name = models.CharField(max_length=200)
short_description = models.CharField(max_length=1000)
long_description = models.CharField(max_length=3000)
class Evidence(CaseItem):
URL = models.CharField(max_length=3000)
shape = Shape.CYLINDER
evidential_claim = models.ManyToManyField(EvidentialClaim, related_name="evidence")
16 changes: 15 additions & 1 deletion eap_backend/eap_api/serializers.py
Expand Up @@ -71,9 +71,20 @@ class Meta:

class PropertyClaimSerializer(serializers.ModelSerializer):
goal_id = serializers.PrimaryKeyRelatedField(
source="goal", queryset=TopLevelNormativeGoal.objects.all(), write_only=True
source="goal",
queryset=TopLevelNormativeGoal.objects.all(),
write_only=True,
required=False,
)
property_claim_id = serializers.PrimaryKeyRelatedField(
source="property_claim",
queryset=PropertyClaim.objects.all(),
write_only=True,
required=False,
)
level = serializers.IntegerField(read_only=True)
arguments = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
property_claims = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

class Meta:
model = PropertyClaim
Expand All @@ -83,7 +94,10 @@ class Meta:
"short_description",
"long_description",
"goal_id",
"property_claim_id",
"level",
"arguments",
"property_claims",
)


Expand Down
36 changes: 21 additions & 15 deletions eap_backend/eap_api/views.py
Expand Up @@ -35,49 +35,49 @@
"model": TopLevelNormativeGoal,
"children": ["context", "system_description", "property_claims"],
"fields": ("name", "short_description", "long_description", "keywords"),
"parent_type": ("assurance_case", False),
"parent_types": [("assurance_case", False)],
},
"context": {
"serializer": ContextSerializer,
"model": Context,
"children": [],
"fields": ("name", "short_description", "long_description"),
"parent_type": ("goal", False),
"parent_types": [("goal", False)],
},
"system_description": {
"serializer": SystemDescriptionSerializer,
"model": SystemDescription,
"children": [],
"fields": ("name", "short_description", "long_description"),
"parent_type": ("goal", False),
"parent_types": [("goal", False)],
},
"property_claims": {
"serializer": PropertyClaimSerializer,
"model": PropertyClaim,
"children": ["arguments"],
"children": ["arguments", "property_claims"],
"fields": ("name", "short_description", "long_description"),
"parent_type": ("goal", False),
"parent_types": [("goal", False), ("property_claim", False)],
},
"arguments": {
"serializer": ArgumentSerializer,
"model": Argument,
"children": ["evidential_claims"],
"fields": ("name", "short_description", "long_description"),
"parent_type": ("property_claim", True),
"parent_types": [("property_claim", True)],
},
"evidential_claims": {
"serializer": EvidentialClaimSerializer,
"model": EvidentialClaim,
"children": ["evidence"],
"fields": ("name", "short_description", "long_description"),
"parent_type": ("argument", False),
"parent_types": [("argument", False)],
},
"evidence": {
"serializer": EvidenceSerializer,
"model": Evidence,
"children": [],
"fields": ("name", "short_description", "long_description", "URL"),
"parent_type": ("evidential_claim", True),
"parent_types": [("evidential_claim", True)],
},
}

Expand Down Expand Up @@ -145,7 +145,7 @@ def get_json_tree(id_list, obj_type):
return objs


def save_json_tree(data, obj_type, parent_id=None):
def save_json_tree(data, obj_type, parent_id=None, parent_type=None):
"""Recursively write items in an assurance case tree.

Create a new assurance case like the one described by data, including all
Expand All @@ -167,11 +167,15 @@ def save_json_tree(data, obj_type, parent_id=None):
# so that e.g. the new object gets a unique ID even if `data` specifies an
# ID.
this_data = {k: data[k] for k in TYPE_DICT[obj_type]["fields"]}
if parent_id is not None:
parent_type, plural = TYPE_DICT[obj_type]["parent_type"]
if plural:
parent_id = [parent_id]
this_data[parent_type + "_id"] = parent_id
if parent_id is not None and parent_type is not None:
for parent_type_tmp, plural in TYPE_DICT[obj_type]["parent_types"]:
# TODO This is silly. It's all because some parent_type names are written
# with a plural s in the end while others are not.
if parent_type not in parent_type_tmp:
continue
if plural:
parent_id = [parent_id]
this_data[parent_type_tmp + "_id"] = parent_id
serializer_class = TYPE_DICT[obj_type]["serializer"]
serializer = serializer_class(data=this_data)
if serializer.is_valid():
Expand All @@ -188,7 +192,9 @@ def save_json_tree(data, obj_type, parent_id=None):
if child_type not in data:
continue
for child_data in data[child_type]:
retval = save_json_tree(child_data, child_type, parent_id=id)
retval = save_json_tree(
child_data, child_type, parent_id=id, parent_type=obj_type
)
# If one of the subcalls returns an error, return.
if retval.status_code != success_http_code:
return retval
Expand Down