From eb10887862359ae8539bbf1051eb8e8a19dab8df Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Fri, 7 Sep 2018 17:03:46 -0400 Subject: [PATCH 01/23] WIP: Version controlled database models - cleanerversion dependency installed - database migrations created (not complete) --- FAIRshake/settings.py | 1 + .../migrations/0005_auto_20180907_1618.py | 177 +++++++++++++++ .../migrations/0006_auto_20180907_1618.py | 134 +++++++++++ FAIRshakeAPI/models.py | 211 ++++++++++++++++++ requirements.txt | 1 + 5 files changed, 524 insertions(+) create mode 100644 FAIRshakeAPI/migrations/0005_auto_20180907_1618.py create mode 100644 FAIRshakeAPI/migrations/0006_auto_20180907_1618.py diff --git a/FAIRshake/settings.py b/FAIRshake/settings.py index d08a9d8..17c52f8 100644 --- a/FAIRshake/settings.py +++ b/FAIRshake/settings.py @@ -62,6 +62,7 @@ 'corsheaders', 'ajax_select', 'analytical', + 'versions_tests', 'allauth', 'allauth.account', 'allauth.socialaccount', diff --git a/FAIRshakeAPI/migrations/0005_auto_20180907_1618.py b/FAIRshakeAPI/migrations/0005_auto_20180907_1618.py new file mode 100644 index 0000000..468e6b7 --- /dev/null +++ b/FAIRshakeAPI/migrations/0005_auto_20180907_1618.py @@ -0,0 +1,177 @@ +# Generated by Django 2.0.7 on 2018-09-07 16:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import versions.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0004_auto_20180823_1910'), + ] + + operations = [ + migrations.CreateModel( + name='AnswerNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('answer', models.TextField(blank=True, default='')), + ('comment', models.TextField(blank=True, default='')), + ('url_comment', models.TextField(blank=True, default='')), + ], + options={ + 'verbose_name': 'answer', + 'verbose_name_plural': 'answers', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='AssessmentNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('methodology', models.TextField(blank=True, choices=[('self', 'Digital Object Creator Assessment'), ('user', 'Independent User Assessment'), ('auto', 'Automatic Assessment'), ('test', 'Test Assessment')], max_length=16)), + ('assessor', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'assessment', + 'verbose_name_plural': 'assessments', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='DigitalObjectNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('url', models.CharField(max_length=255)), + ('title', models.CharField(blank=True, default='', max_length=255)), + ('fairsharing', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'digital_object', + 'verbose_name_plural': 'digital_objects', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='MetricNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('yesnobut', 'Yes no or but question'), ('text', 'Simple textbox input'), ('url', 'A url input')], default='yesnobut', max_length=16)), + ('license', models.CharField(blank=True, default='', max_length=255)), + ('rationale', models.TextField(blank=True, default='')), + ('principle', models.CharField(blank=True, choices=[('F', 'Findability'), ('A', 'Accessibility'), ('I', 'Interoperability'), ('R', 'Reusability')], default='', max_length=16)), + ('fairmetrics', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'metric', + 'verbose_name_plural': 'metrics', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='ProjectNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('digital_objects', versions.fields.VersionedManyToManyField(blank=True, related_name='projects', to='FAIRshakeAPI.DigitalObjectNew')), + ], + options={ + 'verbose_name': 'project', + 'verbose_name_plural': 'projects', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='RubricNew', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('license', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('metrics', versions.fields.VersionedManyToManyField(blank=True, related_name='rubrics', to='FAIRshakeAPI.MetricNew')), + ], + options={ + 'verbose_name': 'rubric', + 'verbose_name_plural': 'rubrics', + 'ordering': ['id'], + }, + ), + migrations.AddField( + model_name='digitalobjectnew', + name='rubrics', + field=versions.fields.VersionedManyToManyField(blank=True, related_name='digital_objects', to='FAIRshakeAPI.RubricNew'), + ), + migrations.AddField( + model_name='assessmentnew', + name='project', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assessments', to='FAIRshakeAPI.ProjectNew'), + ), + migrations.AddField( + model_name='assessmentnew', + name='rubric', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='assessments', to='FAIRshakeAPI.RubricNew'), + ), + migrations.AddField( + model_name='assessmentnew', + name='target', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='assessments', to='FAIRshakeAPI.DigitalObjectNew'), + ), + migrations.AddField( + model_name='answernew', + name='assessment', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='FAIRshakeAPI.AssessmentNew'), + ), + migrations.AddField( + model_name='answernew', + name='metric', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='FAIRshakeAPI.MetricNew'), + ), + ] diff --git a/FAIRshakeAPI/migrations/0006_auto_20180907_1618.py b/FAIRshakeAPI/migrations/0006_auto_20180907_1618.py new file mode 100644 index 0000000..a1a5198 --- /dev/null +++ b/FAIRshakeAPI/migrations/0006_auto_20180907_1618.py @@ -0,0 +1,134 @@ +# Generated by Django 2.0.7 on 2018-09-07 16:18 + +from django.db import migrations, transaction +from versions.models import VersionManager + +def migrate_db(forwards=None): + def migrate(apps, schema_editor): + if forwards: + Metric = apps.get_model('FAIRshakeAPI', 'Metric') + MetricNew = apps.get_model('FAIRshakeAPI', 'MetricNew') + else: + Metric = apps.get_model('FAIRshakeAPI', 'MetricNew') + MetricNew = apps.get_model('FAIRshakeAPI', 'Metric') + + old_metric_2_new = {} + for metric in Metric.objects.all(): + new_metric = MetricNew.objects.create( + title=metric.title, + url=metric.url, + description=metric.description, + image=metric.image, + tags=metric.tags, + type=metric.type, + license=metric.license, + rationale=metric.rationale, + principle=metric.principle, + fairmetrics=metric.fairmetrics, + ) + new_metric.save() + for author in metric.authors.all(): + new_metric.authors.add(author) + old_metric_2_new[metric] = new_metric + + old_rubric_2_new = {} + Rubric = apps.get_model('FAIRshakeAPI', 'Rubric') + RubricNew = apps.get_model('FAIRshakeAPI', 'RubricNew') + for rubric in Rubric.objects.all(): + new_rubric = RubricNew.objects.create( + title=rubric.title, + url=rubric.url, + description=rubric.description, + image=rubric.image, + tags=rubric.tags, + type=rubric.type, + license=rubric.license, + ) + new_rubric.save() + for author in rubric.authors.all(): + new_rubric.authors.add(author) + for metric in rubric.metrics.all(): + new_rubric.metrics.add(old_metric_2_new[metric]) + old_rubric_2_new[rubric] = new_rubric + + old_obj_2_new = {} + DigitalObject = apps.get_model('FAIRshakeAPI', 'DigitalObject') + DigitalObjectNew = apps.get_model('FAIRshakeAPI', 'DigitalObjectNew') + for obj in DigitalObject.objects.all(): + new_obj = DigitalObjectNew.objects.create( + title=obj.title, + url=obj.url, + description=obj.description, + image=obj.image, + tags=obj.tags, + type=obj.type, + fairsharing=obj.fairsharing, + ) + new_obj.save() + for author in obj.authors.all(): + new_obj.authors.add(author) + for rubric in obj.rubrics.all(): + new_obj.rubrics.add(old_rubric_2_new[rubric]) + old_obj_2_new[obj] = new_obj + + old_project_2_new = {} + Project = apps.get_model('FAIRshakeAPI', 'Project') + ProjectNew = apps.get_model('FAIRshakeAPI', 'ProjectNew') + for project in Project.objects.all(): + new_project = ProjectNew.objects.create( + title=project.title, + url=project.url, + description=project.description, + image=project.image, + tags=project.tags, + type=project.type, + ) + new_project.save() + for author in project.authors.all(): + new_project.authors.add(author) + for obj in project.digital_objects.all(): + new_project.digital_objects.add(old_obj_2_new[obj]) + old_project_2_new[project] = new_project + + old_assessment_2_new = {} + Assessment = apps.get_model('FAIRshakeAPI', 'Assessment') + AssessmentNew = apps.get_model('FAIRshakeAPI', 'AssessmentNew') + for assessment in Assessment.objects.all(): + new_assessment = AssessmentNew.objects.create( + project=old_project_2_new[assessment.project], + target=old_obj_2_new[assessment.target], + rubric=old_rubric_2_new[assessment.rubric], + methodology=assessment.methodology, + assessor=assessment.assessor, + ) + new_assessment.save() + old_assessment_2_new[assessment] = new_assessment + + old_answer_2_new = {} + Answer = apps.get_model('FAIRshakeAPI', 'Answer') + AnswerNew = apps.get_model('FAIRshakeAPI', 'AnswerNew') + for answer in Answer.objects.all(): + new_answer = AnswerNew.objects.create( + assessment=old_assessment_2_new[answer.assessment], + metric=old_metric_2_new[answer.metric], + answer=answer.answer, + comment=answer.comment, + url_comment=answer.url_comment, + ) + new_answer.save() + old_answer_2_new[answer] = new_answer + + return migrate + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0005_auto_20180907_1618'), + ] + + operations = [ + migrations.RunPython( + migrate_db(forwards=True), + migrate_db(forwards=False), + ) + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index fcedf46..fad6593 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -2,6 +2,217 @@ from django.db import models from django.contrib.auth.models import AbstractUser from collections import OrderedDict +from versions.models import Versionable +from versions.fields import VersionedForeignKey, VersionedManyToManyField + +class IdentifiableModelMixinNew(Versionable): + title = models.CharField(max_length=255, blank=False) + url = models.CharField(max_length=255, blank=True) + # urls = ArrayField(models.CharField(max_length=255), blank=True) + description = models.TextField(blank=True, null=False, default='') + image = models.CharField(max_length=255, blank=True, null=False, default='') + tags = models.CharField(max_length=255, blank=True) + # tags = ArrayField(models.CharField(max_length=255), blank=True) + + type = models.CharField(max_length=16, blank=True, null=False, default='', choices=( + ('', 'Other'), + ('any', 'Any Digital Object'), + ('data', 'Dataset'), + ('repo', 'Repository'), + ('test', 'Test Object'), + ('tool', 'Tool'), + )) + + authors = VersionedManyToManyField('Author', blank=True) + + def tags_as_list(self): + return self.tags.split() + + def model_name(self): + return self._meta.verbose_name_raw + + def has_permission(self, user, perm): + if perm in ['list', 'retrieve', 'stats']: + return True + elif perm in ['create', 'add']: + return user.is_authenticated or user.is_staff + elif perm in ['modify', 'remove', 'delete']: + if self is None: + return user.is_authenticated + else: + return (self.authors and self.authors.filter(id=user.id).exists()) or user.is_staff + else: + logging.warning('perm %s not handled' % (perm)) + return user.is_staff + + def __str__(self): + return '{title} ({id})'.format(id=self.id, title=self.title) + + class Meta: + abstract = True + +class ProjectNew(IdentifiableModelMixinNew): + digital_objects = VersionedManyToManyField('DigitalObjectNew', blank=True, related_name='projects') + + class Meta: + verbose_name = 'project' + verbose_name_plural = 'projects' + ordering = ['id'] + + class MetaEx: + children = [ + 'digital_objects', + ] + +class DigitalObjectNew(IdentifiableModelMixinNew): + # A digital object's title is optional while its url is mandatory, unlike the rest of the identifiables + url = models.CharField(max_length=255, blank=False) + # urls = ArrayField(models.CharField(max_length=255), blank=False) + title = models.CharField(max_length=255, blank=True, null=False, default='') + fairsharing = models.CharField(max_length=255, blank=True, null=False, default='') + + rubrics = VersionedManyToManyField('RubricNew', blank=True, related_name='digital_objects') + + class Meta: + verbose_name = 'digital_object' + verbose_name_plural = 'digital_objects' + ordering = ['id'] + + class MetaEx: + children = [ + 'projects', + 'rubrics', + ] + +class RubricNew(IdentifiableModelMixinNew): + license = models.CharField(max_length=255, blank=True, null=False, default='') + + metrics = VersionedManyToManyField('MetricNew', blank=True, related_name='rubrics') + + class Meta: + verbose_name = 'rubric' + verbose_name_plural = 'rubrics' + ordering = ['id'] + + class MetaEx: + children = [ + 'metrics', + 'digital_objects', + ] + +class MetricNew(IdentifiableModelMixinNew): + type = models.CharField(max_length=16, blank=True, null=False, default='yesnobut', choices=( + ('yesnobut', 'Yes no or but question'), + ('text', 'Simple textbox input'), + ('url', 'A url input'), + )) + + license = models.CharField(max_length=255, blank=True, null=False, default='') + + rationale = models.TextField(blank=True, null=False, default='') + principle = models.CharField(max_length=16, blank=True, null=False, default='', choices=( + ('F', 'Findability',), + ('A', 'Accessibility',), + ('I', 'Interoperability',), + ('R', 'Reusability',), + )) + fairmetrics = models.CharField(max_length=255, blank=True, null=False, default='') + + class Meta: + verbose_name = 'metric' + verbose_name_plural = 'metrics' + ordering = ['id'] + + class MetaEx: + children = [ + 'rubrics', + ] + +class AssessmentNew(Versionable): + project = models.ForeignKey('ProjectNew', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='assessments') + target = models.ForeignKey('DigitalObjectNew', on_delete=models.CASCADE, editable=False, related_name='assessments') + rubric = models.ForeignKey('RubricNew', on_delete=models.CASCADE, editable=False, related_name='assessments') + methodology = models.TextField(max_length=16, blank=True, choices=( + ('self', 'Digital Object Creator Assessment'), + ('user', 'Independent User Assessment'), + ('auto', 'Automatic Assessment'), + ('test', 'Test Assessment'), + )) + assessor = models.ForeignKey('Author', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='+') + + def has_permission(self, user, perm): + if perm in ['list', 'retrieve']: + return True + elif perm in ['create', 'add']: + return user.is_authenticated or user.is_staff + elif perm in ['modify', 'remove', 'delete']: + if self is None: + return user.is_authenticated + else: + return (self and self.assessor == user) or user.is_staff + else: + logging.warning('perm %s not handled' % (perm)) + return user.is_staff + + def __str__(self): + return '{methodology} assessment on Target[{target}] for Project[{project}] with Rubric[{rubric}] ({id})'.format( + id=self.id, + project=self.project, + target=self.target, + rubric=self.rubric, + methodology=self.methodology + ) + + class Meta: + verbose_name = 'assessment' + verbose_name_plural = 'assessments' + ordering = ['id'] + + class MetaEx: + children = [ + 'answers', + ] + +class AnswerNew(Versionable): + assessment = models.ForeignKey('AssessmentNew', on_delete=models.CASCADE, editable=False, related_name='answers') + metric = models.ForeignKey('MetricNew', on_delete=models.CASCADE, editable=False, related_name='answers') + answer = models.TextField(blank=True, null=False, default='') + comment = models.TextField(blank=True, null=False, default='') + url_comment = models.TextField(blank=True, null=False, default='') + +# yesnomaybe (depends on metric__type) + def value(self): + return { + 'yes': 1, + 'yesbut': 0.75, + 'nobut': 0.25, + 'no': 0, + '': 0, + }.get(self.answer, 1) + + def inverse(self): + return { + 1: 'yes', + 0.75: 'yesbut', + 0.25: 'nobut', + 0: 'no', + }.get(self.answer, 'yes') + + def has_permission(self, user, perm): + return (self and self.assessment.has_permission(user, perm)) or user.is_staff + + def __str__(self): + return 'Answer to Metric[{metric}] for assessment[{assessment}]: {answer} ({id})'.format( + id=self.id, + assessment=self.assessment, + metric=self.metric, + answer=self.answer, + ) + + class Meta: + verbose_name = 'answer' + verbose_name_plural = 'answers' + ordering = ['id'] class IdentifiableModelMixin(models.Model): id = models.AutoField(primary_key=True) diff --git a/requirements.txt b/requirements.txt index e6392a0..fc37c21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ djangorestframework drf-yasg dynamic-rest https://github.com/pennersr/django-allauth/archive/master.zip +https://github.com/swisscom/cleanerversion/archive/master.zip numpy plotly pymysql From 6cc6fcdbf733be9be2d30b6095e9519cb2557850 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 10 Sep 2018 11:03:13 -0400 Subject: [PATCH 02/23] WIP: Version controlled database models - cleanerversion extended: - VersionManager--`use_in_migrations` --- FAIRshake/settings.py | 1 + ...907_1618.py => 0005_auto_20180910_1449.py} | 21 ++++++++++++++++++- ...907_1618.py => 0006_auto_20180910_1449.py} | 11 +--------- FAIRshakeAPI/models.py | 3 +-- extensions/versions_ex/__init__.py | 0 extensions/versions_ex/models.py | 11 ++++++++++ 6 files changed, 34 insertions(+), 13 deletions(-) rename FAIRshakeAPI/migrations/{0005_auto_20180907_1618.py => 0005_auto_20180910_1449.py} (92%) rename FAIRshakeAPI/migrations/{0006_auto_20180907_1618.py => 0006_auto_20180910_1449.py} (93%) create mode 100644 extensions/versions_ex/__init__.py create mode 100644 extensions/versions_ex/models.py diff --git a/FAIRshake/settings.py b/FAIRshake/settings.py index 17c52f8..1da5566 100644 --- a/FAIRshake/settings.py +++ b/FAIRshake/settings.py @@ -73,6 +73,7 @@ 'extensions.allauth_ex', 'extensions.drf_yasg_ex', 'extensions.rest_auth_ex', + 'extensions.versions_ex', 'FAIRshakeHub', 'FAIRshakeAPI', ] diff --git a/FAIRshakeAPI/migrations/0005_auto_20180907_1618.py b/FAIRshakeAPI/migrations/0005_auto_20180910_1449.py similarity index 92% rename from FAIRshakeAPI/migrations/0005_auto_20180907_1618.py rename to FAIRshakeAPI/migrations/0005_auto_20180910_1449.py index 468e6b7..dbdd2f7 100644 --- a/FAIRshakeAPI/migrations/0005_auto_20180907_1618.py +++ b/FAIRshakeAPI/migrations/0005_auto_20180910_1449.py @@ -1,8 +1,9 @@ -# Generated by Django 2.0.7 on 2018-09-07 16:18 +# Generated by Django 2.0.7 on 2018-09-10 14:49 from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import extensions.versions_ex.models import versions.fields @@ -30,6 +31,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'answers', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.CreateModel( name='AssessmentNew', @@ -47,6 +51,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'assessments', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.CreateModel( name='DigitalObjectNew', @@ -70,6 +77,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'digital_objects', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.CreateModel( name='MetricNew', @@ -96,6 +106,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'metrics', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.CreateModel( name='ProjectNew', @@ -119,6 +132,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'projects', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.CreateModel( name='RubricNew', @@ -143,6 +159,9 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'rubrics', 'ordering': ['id'], }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], ), migrations.AddField( model_name='digitalobjectnew', diff --git a/FAIRshakeAPI/migrations/0006_auto_20180907_1618.py b/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py similarity index 93% rename from FAIRshakeAPI/migrations/0006_auto_20180907_1618.py rename to FAIRshakeAPI/migrations/0006_auto_20180910_1449.py index a1a5198..813fa42 100644 --- a/FAIRshakeAPI/migrations/0006_auto_20180907_1618.py +++ b/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py @@ -1,7 +1,4 @@ -# Generated by Django 2.0.7 on 2018-09-07 16:18 - from django.db import migrations, transaction -from versions.models import VersionManager def migrate_db(forwards=None): def migrate(apps, schema_editor): @@ -26,7 +23,6 @@ def migrate(apps, schema_editor): principle=metric.principle, fairmetrics=metric.fairmetrics, ) - new_metric.save() for author in metric.authors.all(): new_metric.authors.add(author) old_metric_2_new[metric] = new_metric @@ -44,7 +40,6 @@ def migrate(apps, schema_editor): type=rubric.type, license=rubric.license, ) - new_rubric.save() for author in rubric.authors.all(): new_rubric.authors.add(author) for metric in rubric.metrics.all(): @@ -64,7 +59,6 @@ def migrate(apps, schema_editor): type=obj.type, fairsharing=obj.fairsharing, ) - new_obj.save() for author in obj.authors.all(): new_obj.authors.add(author) for rubric in obj.rubrics.all(): @@ -83,7 +77,6 @@ def migrate(apps, schema_editor): tags=project.tags, type=project.type, ) - new_project.save() for author in project.authors.all(): new_project.authors.add(author) for obj in project.digital_objects.all(): @@ -101,7 +94,6 @@ def migrate(apps, schema_editor): methodology=assessment.methodology, assessor=assessment.assessor, ) - new_assessment.save() old_assessment_2_new[assessment] = new_assessment old_answer_2_new = {} @@ -115,7 +107,6 @@ def migrate(apps, schema_editor): comment=answer.comment, url_comment=answer.url_comment, ) - new_answer.save() old_answer_2_new[answer] = new_answer return migrate @@ -123,7 +114,7 @@ def migrate(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('FAIRshakeAPI', '0005_auto_20180907_1618'), + ('FAIRshakeAPI', '0005_auto_20180910_1449'), ] operations = [ diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index fad6593..ff941ae 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -2,8 +2,7 @@ from django.db import models from django.contrib.auth.models import AbstractUser from collections import OrderedDict -from versions.models import Versionable -from versions.fields import VersionedForeignKey, VersionedManyToManyField +from extensions.versions_ex.models import VersionableEx as Versionable, VersionedForeignKey, VersionedManyToManyField class IdentifiableModelMixinNew(Versionable): title = models.CharField(max_length=255, blank=False) diff --git a/extensions/versions_ex/__init__.py b/extensions/versions_ex/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extensions/versions_ex/models.py b/extensions/versions_ex/models.py new file mode 100644 index 0000000..f41f16b --- /dev/null +++ b/extensions/versions_ex/models.py @@ -0,0 +1,11 @@ +from versions.models import Versionable, VersionManager +from versions.fields import VersionedForeignKey, VersionedManyToManyField + +class VersionManagerEx(VersionManager): + use_in_migrations = True + +class VersionableEx(Versionable): + objects = VersionManagerEx() + + class Meta: + abstract = True From fd7b5b3fde58de76d1e3a513668e0720db8a147a Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 10 Sep 2018 12:58:27 -0400 Subject: [PATCH 03/23] Versioned migrations - VersionedQuerySet extension - Override create to force certain methods on resulting objects Fixes #43 --- .../migrations/0006_auto_20180910_1449.py | 30 ++++++++----------- extensions/versions_ex/models.py | 29 +++++++++++++++++- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py b/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py index 813fa42..b7eefa0 100644 --- a/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py +++ b/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py @@ -2,12 +2,8 @@ def migrate_db(forwards=None): def migrate(apps, schema_editor): - if forwards: - Metric = apps.get_model('FAIRshakeAPI', 'Metric') - MetricNew = apps.get_model('FAIRshakeAPI', 'MetricNew') - else: - Metric = apps.get_model('FAIRshakeAPI', 'MetricNew') - MetricNew = apps.get_model('FAIRshakeAPI', 'Metric') + Metric = apps.get_model('FAIRshakeAPI', 'Metric' if forwards else 'MetricNew') + MetricNew = apps.get_model('FAIRshakeAPI', 'MetricNew' if forwards else 'Metric') old_metric_2_new = {} for metric in Metric.objects.all(): @@ -27,9 +23,9 @@ def migrate(apps, schema_editor): new_metric.authors.add(author) old_metric_2_new[metric] = new_metric + Rubric = apps.get_model('FAIRshakeAPI', 'Rubric' if forwards else 'RubricNew') + RubricNew = apps.get_model('FAIRshakeAPI', 'RubricNew' if forwards else 'Rubric') old_rubric_2_new = {} - Rubric = apps.get_model('FAIRshakeAPI', 'Rubric') - RubricNew = apps.get_model('FAIRshakeAPI', 'RubricNew') for rubric in Rubric.objects.all(): new_rubric = RubricNew.objects.create( title=rubric.title, @@ -46,9 +42,9 @@ def migrate(apps, schema_editor): new_rubric.metrics.add(old_metric_2_new[metric]) old_rubric_2_new[rubric] = new_rubric + DigitalObject = apps.get_model('FAIRshakeAPI', 'DigitalObject' if forwards else 'DigitalObjectNew') + DigitalObjectNew = apps.get_model('FAIRshakeAPI', 'DigitalObjectNew' if forwards else 'DigitalObject') old_obj_2_new = {} - DigitalObject = apps.get_model('FAIRshakeAPI', 'DigitalObject') - DigitalObjectNew = apps.get_model('FAIRshakeAPI', 'DigitalObjectNew') for obj in DigitalObject.objects.all(): new_obj = DigitalObjectNew.objects.create( title=obj.title, @@ -65,9 +61,9 @@ def migrate(apps, schema_editor): new_obj.rubrics.add(old_rubric_2_new[rubric]) old_obj_2_new[obj] = new_obj + Project = apps.get_model('FAIRshakeAPI', 'Project' if forwards else 'ProjectNew') + ProjectNew = apps.get_model('FAIRshakeAPI', 'ProjectNew' if forwards else 'Project') old_project_2_new = {} - Project = apps.get_model('FAIRshakeAPI', 'Project') - ProjectNew = apps.get_model('FAIRshakeAPI', 'ProjectNew') for project in Project.objects.all(): new_project = ProjectNew.objects.create( title=project.title, @@ -83,12 +79,12 @@ def migrate(apps, schema_editor): new_project.digital_objects.add(old_obj_2_new[obj]) old_project_2_new[project] = new_project + Assessment = apps.get_model('FAIRshakeAPI', 'Assessment' if forwards else 'AssessmentNew') + AssessmentNew = apps.get_model('FAIRshakeAPI', 'AssessmentNew' if forwards else 'Assessment') old_assessment_2_new = {} - Assessment = apps.get_model('FAIRshakeAPI', 'Assessment') - AssessmentNew = apps.get_model('FAIRshakeAPI', 'AssessmentNew') for assessment in Assessment.objects.all(): new_assessment = AssessmentNew.objects.create( - project=old_project_2_new[assessment.project], + project=old_project_2_new[assessment.project] if assessment.project is not None else None, target=old_obj_2_new[assessment.target], rubric=old_rubric_2_new[assessment.rubric], methodology=assessment.methodology, @@ -96,9 +92,9 @@ def migrate(apps, schema_editor): ) old_assessment_2_new[assessment] = new_assessment + Answer = apps.get_model('FAIRshakeAPI', 'Answer' if forwards else 'AnswerNew') + AnswerNew = apps.get_model('FAIRshakeAPI', 'AnswerNew' if forwards else 'Answer') old_answer_2_new = {} - Answer = apps.get_model('FAIRshakeAPI', 'Answer') - AnswerNew = apps.get_model('FAIRshakeAPI', 'AnswerNew') for answer in Answer.objects.all(): new_answer = AnswerNew.objects.create( assessment=old_assessment_2_new[answer.assessment], diff --git a/extensions/versions_ex/models.py b/extensions/versions_ex/models.py index f41f16b..382842d 100644 --- a/extensions/versions_ex/models.py +++ b/extensions/versions_ex/models.py @@ -1,9 +1,36 @@ -from versions.models import Versionable, VersionManager +from versions.models import Versionable, VersionManager, VersionedQuerySet from versions.fields import VersionedForeignKey, VersionedManyToManyField +class VersionedQuerySetEx(VersionedQuerySet): + def _set_item_querytime(self, item, type_check=False): + """ + Sets the time for which the query was made on the resulting item + :param item: an item of type Versionable + :param type_check: Check the item to be a Versionable + :return: Returns the item itself with the time set + """ + if isinstance(item, VersionedQuerySet) or isinstance(item, VersionedQuerySetEx): + item.querytime = self.querytime + else: #if isinstance(item, Versionable) or isinstance(item, VersionableEx): + # Note: This will fall back to a `fake` model during django migrations, hence the else + item._querytime = self.querytime + return item + class VersionManagerEx(VersionManager): use_in_migrations = True + def get_queryset(self): + qs = VersionedQuerySetEx(self.model, using=self._db) + if hasattr(self, 'instance') and hasattr(self.instance, '_querytime'): + qs.querytime = self.instance._querytime + return qs + + def create(*args, **kwargs): + res = VersionManager.create(*args, **kwargs) + res.is_current = VersionableEx.is_current + res.querytime = VersionableEx.is_current + return res + class VersionableEx(Versionable): objects = VersionManagerEx() From c84a65268e9ca791155aeb3a87fbe289b24d8f9f Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 10 Sep 2018 15:44:19 -0400 Subject: [PATCH 04/23] Completed migrations to new Database Fixes #43 --- ...910_1449.py => 0005_auto_20180910_1752.py} | 18 +- ...910_1449.py => 0006_auto_20180910_1752.py} | 13 +- .../migrations/0007_auto_20180910_1756.py | 34 +++ .../migrations/0008_auto_20180910_1933.py | 211 +++++++++++++++ .../migrations/0009_auto_20180910_1933.py | 132 ++++++++++ .../migrations/0010_auto_20180910_1935.py | 94 +++++++ FAIRshakeAPI/models.py | 242 ++---------------- 7 files changed, 515 insertions(+), 229 deletions(-) rename FAIRshakeAPI/migrations/{0005_auto_20180910_1449.py => 0005_auto_20180910_1752.py} (92%) rename FAIRshakeAPI/migrations/{0006_auto_20180910_1449.py => 0006_auto_20180910_1752.py} (86%) create mode 100644 FAIRshakeAPI/migrations/0007_auto_20180910_1756.py create mode 100644 FAIRshakeAPI/migrations/0008_auto_20180910_1933.py create mode 100644 FAIRshakeAPI/migrations/0009_auto_20180910_1933.py create mode 100644 FAIRshakeAPI/migrations/0010_auto_20180910_1935.py diff --git a/FAIRshakeAPI/migrations/0005_auto_20180910_1449.py b/FAIRshakeAPI/migrations/0005_auto_20180910_1752.py similarity index 92% rename from FAIRshakeAPI/migrations/0005_auto_20180910_1449.py rename to FAIRshakeAPI/migrations/0005_auto_20180910_1752.py index dbdd2f7..8087463 100644 --- a/FAIRshakeAPI/migrations/0005_auto_20180910_1449.py +++ b/FAIRshakeAPI/migrations/0005_auto_20180910_1752.py @@ -1,11 +1,11 @@ -# Generated by Django 2.0.7 on 2018-09-10 14:49 +# Generated by Django 2.0.7 on 2018-09-10 17:52 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import extensions.versions_ex.models import versions.fields - +import uuid class Migration(migrations.Migration): @@ -55,6 +55,20 @@ class Migration(migrations.Migration): ('objects', extensions.versions_ex.models.VersionManagerEx()), ], ), + migrations.CreateModel( + name='AssessmentRequestNew', + fields=[ + ('id', models.UUIDField(primary_key=True, default=uuid.uuid4, serialize=False)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('assessment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='request', to='FAIRshakeAPI.AssessmentNew')), + ('requestor', models.ForeignKey(blank=True, default='', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'assessment_request', + 'verbose_name_plural': 'assessment_requests', + 'ordering': ['id'], + }, + ), migrations.CreateModel( name='DigitalObjectNew', fields=[ diff --git a/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py b/FAIRshakeAPI/migrations/0006_auto_20180910_1752.py similarity index 86% rename from FAIRshakeAPI/migrations/0006_auto_20180910_1449.py rename to FAIRshakeAPI/migrations/0006_auto_20180910_1752.py index b7eefa0..0cb78aa 100644 --- a/FAIRshakeAPI/migrations/0006_auto_20180910_1449.py +++ b/FAIRshakeAPI/migrations/0006_auto_20180910_1752.py @@ -105,12 +105,23 @@ def migrate(apps, schema_editor): ) old_answer_2_new[answer] = new_answer + AssessmentRequest = apps.get_model('FAIRshakeAPI', 'AssessmentRequest' if forwards else 'AssessmentRequestNew') + AssessmentRequestNew = apps.get_model('FAIRshakeAPI', 'AssessmentRequestNew' if forwards else 'AssessmentRequest') + old_assessment_request_2_new = {} + for assessment_request in AssessmentRequest.objects.all(): + new_assessment_request = AssessmentRequestNew.objects.create( + assessment=old_assessment_2_new[assessment_request.assessment], + requestor=assessment_request.requestor, + timestamp=assessment_request.timestamp, + ) + old_assessment_request_2_new[assessment_request] = new_assessment_request + return migrate class Migration(migrations.Migration): dependencies = [ - ('FAIRshakeAPI', '0005_auto_20180910_1449'), + ('FAIRshakeAPI', '0005_auto_20180910_1752'), ] operations = [ diff --git a/FAIRshakeAPI/migrations/0007_auto_20180910_1756.py b/FAIRshakeAPI/migrations/0007_auto_20180910_1756.py new file mode 100644 index 0000000..7c6ea99 --- /dev/null +++ b/FAIRshakeAPI/migrations/0007_auto_20180910_1756.py @@ -0,0 +1,34 @@ +# Generated by Django 2.0.7 on 2018-09-10 17:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0006_auto_20180910_1752'), + ] + + operations = [ + migrations.DeleteModel( + name='Answer', + ), + migrations.DeleteModel( + name='Assessment', + ), + migrations.DeleteModel( + name='AssessmentRequest', + ), + migrations.DeleteModel( + name='DigitalObject', + ), + migrations.DeleteModel( + name='Metric', + ), + migrations.DeleteModel( + name='Project', + ), + migrations.DeleteModel( + name='Rubric', + ), + ] diff --git a/FAIRshakeAPI/migrations/0008_auto_20180910_1933.py b/FAIRshakeAPI/migrations/0008_auto_20180910_1933.py new file mode 100644 index 0000000..8f6d2a7 --- /dev/null +++ b/FAIRshakeAPI/migrations/0008_auto_20180910_1933.py @@ -0,0 +1,211 @@ +# Generated by Django 2.0.7 on 2018-09-10 19:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import extensions.versions_ex.models +import uuid +import versions.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0007_auto_20180910_1756'), + ] + + operations = [ + migrations.CreateModel( + name='Answer', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('answer', models.TextField(blank=True, default='')), + ('comment', models.TextField(blank=True, default='')), + ('url_comment', models.TextField(blank=True, default='')), + ], + options={ + 'verbose_name': 'answer', + 'verbose_name_plural': 'answers', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.CreateModel( + name='Assessment', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('methodology', models.TextField(blank=True, choices=[('self', 'Digital Object Creator Assessment'), ('user', 'Independent User Assessment'), ('auto', 'Automatic Assessment'), ('test', 'Test Assessment')], max_length=16)), + ('assessor', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'assessment', + 'verbose_name_plural': 'assessments', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.CreateModel( + name='AssessmentRequest', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('assessment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='request', to='FAIRshakeAPI.Assessment')), + ('requestor', models.ForeignKey(blank=True, default='', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'assessment_request', + 'verbose_name_plural': 'assessment_requests', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='DigitalObject', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('url', models.CharField(max_length=255)), + ('title', models.CharField(blank=True, default='', max_length=255)), + ('fairsharing', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'digital_object', + 'verbose_name_plural': 'digital_objects', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.CreateModel( + name='Metric', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('yesnobut', 'Yes no or but question'), ('text', 'Simple textbox input'), ('url', 'A url input')], default='yesnobut', max_length=16)), + ('license', models.CharField(blank=True, default='', max_length=255)), + ('rationale', models.TextField(blank=True, default='')), + ('principle', models.CharField(blank=True, choices=[('F', 'Findability'), ('A', 'Accessibility'), ('I', 'Interoperability'), ('R', 'Reusability')], default='', max_length=16)), + ('fairmetrics', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'metric', + 'verbose_name_plural': 'metrics', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('digital_objects', versions.fields.VersionedManyToManyField(blank=True, related_name='projects', to='FAIRshakeAPI.DigitalObject')), + ], + options={ + 'verbose_name': 'project', + 'verbose_name_plural': 'projects', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.CreateModel( + name='Rubric', + fields=[ + ('id', models.UUIDField(primary_key=True, serialize=False)), + ('identity', models.UUIDField()), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('title', models.CharField(max_length=255)), + ('url', models.CharField(blank=True, max_length=255)), + ('description', models.TextField(blank=True, default='')), + ('image', models.CharField(blank=True, default='', max_length=255)), + ('tags', models.CharField(blank=True, max_length=255)), + ('type', models.CharField(blank=True, choices=[('', 'Other'), ('any', 'Any Digital Object'), ('data', 'Dataset'), ('repo', 'Repository'), ('test', 'Test Object'), ('tool', 'Tool')], default='', max_length=16)), + ('license', models.CharField(blank=True, default='', max_length=255)), + ('authors', versions.fields.VersionedManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), + ('metrics', versions.fields.VersionedManyToManyField(blank=True, related_name='rubrics', to='FAIRshakeAPI.Metric')), + ], + options={ + 'verbose_name': 'rubric', + 'verbose_name_plural': 'rubrics', + 'ordering': ['id'], + }, + managers=[ + ('objects', extensions.versions_ex.models.VersionManagerEx()), + ], + ), + migrations.AddField( + model_name='digitalobject', + name='rubrics', + field=versions.fields.VersionedManyToManyField(blank=True, related_name='digital_objects', to='FAIRshakeAPI.Rubric'), + ), + migrations.AddField( + model_name='assessment', + name='project', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assessments', to='FAIRshakeAPI.Project'), + ), + migrations.AddField( + model_name='assessment', + name='rubric', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='assessments', to='FAIRshakeAPI.Rubric'), + ), + migrations.AddField( + model_name='assessment', + name='target', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='assessments', to='FAIRshakeAPI.DigitalObject'), + ), + migrations.AddField( + model_name='answer', + name='assessment', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='FAIRshakeAPI.Assessment'), + ), + migrations.AddField( + model_name='answer', + name='metric', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='FAIRshakeAPI.Metric'), + ), + ] diff --git a/FAIRshakeAPI/migrations/0009_auto_20180910_1933.py b/FAIRshakeAPI/migrations/0009_auto_20180910_1933.py new file mode 100644 index 0000000..5f6d860 --- /dev/null +++ b/FAIRshakeAPI/migrations/0009_auto_20180910_1933.py @@ -0,0 +1,132 @@ +from django.db import migrations, transaction + +def migrate_db(forwards=None): + def migrate(apps, schema_editor): + Metric = apps.get_model('FAIRshakeAPI', 'Metric' if forwards else 'MetricNew') + MetricNew = apps.get_model('FAIRshakeAPI', 'MetricNew' if forwards else 'Metric') + + old_metric_2_new = {} + for metric in Metric.objects.all(): + new_metric = MetricNew.objects.create( + title=metric.title, + url=metric.url, + description=metric.description, + image=metric.image, + tags=metric.tags, + type=metric.type, + license=metric.license, + rationale=metric.rationale, + principle=metric.principle, + fairmetrics=metric.fairmetrics, + ) + for author in metric.authors.all(): + new_metric.authors.add(author) + old_metric_2_new[metric] = new_metric + + Rubric = apps.get_model('FAIRshakeAPI', 'Rubric' if forwards else 'RubricNew') + RubricNew = apps.get_model('FAIRshakeAPI', 'RubricNew' if forwards else 'Rubric') + old_rubric_2_new = {} + for rubric in Rubric.objects.all(): + new_rubric = RubricNew.objects.create( + title=rubric.title, + url=rubric.url, + description=rubric.description, + image=rubric.image, + tags=rubric.tags, + type=rubric.type, + license=rubric.license, + ) + for author in rubric.authors.all(): + new_rubric.authors.add(author) + for metric in rubric.metrics.all(): + new_rubric.metrics.add(old_metric_2_new[metric]) + old_rubric_2_new[rubric] = new_rubric + + DigitalObject = apps.get_model('FAIRshakeAPI', 'DigitalObject' if forwards else 'DigitalObjectNew') + DigitalObjectNew = apps.get_model('FAIRshakeAPI', 'DigitalObjectNew' if forwards else 'DigitalObject') + old_obj_2_new = {} + for obj in DigitalObject.objects.all(): + new_obj = DigitalObjectNew.objects.create( + title=obj.title, + url=obj.url, + description=obj.description, + image=obj.image, + tags=obj.tags, + type=obj.type, + fairsharing=obj.fairsharing, + ) + for author in obj.authors.all(): + new_obj.authors.add(author) + for rubric in obj.rubrics.all(): + new_obj.rubrics.add(old_rubric_2_new[rubric]) + old_obj_2_new[obj] = new_obj + + Project = apps.get_model('FAIRshakeAPI', 'Project' if forwards else 'ProjectNew') + ProjectNew = apps.get_model('FAIRshakeAPI', 'ProjectNew' if forwards else 'Project') + old_project_2_new = {} + for project in Project.objects.all(): + new_project = ProjectNew.objects.create( + title=project.title, + url=project.url, + description=project.description, + image=project.image, + tags=project.tags, + type=project.type, + ) + for author in project.authors.all(): + new_project.authors.add(author) + for obj in project.digital_objects.all(): + new_project.digital_objects.add(old_obj_2_new[obj]) + old_project_2_new[project] = new_project + + Assessment = apps.get_model('FAIRshakeAPI', 'Assessment' if forwards else 'AssessmentNew') + AssessmentNew = apps.get_model('FAIRshakeAPI', 'AssessmentNew' if forwards else 'Assessment') + old_assessment_2_new = {} + for assessment in Assessment.objects.all(): + new_assessment = AssessmentNew.objects.create( + project=old_project_2_new[assessment.project] if assessment.project is not None else None, + target=old_obj_2_new[assessment.target], + rubric=old_rubric_2_new[assessment.rubric], + methodology=assessment.methodology, + assessor=assessment.assessor, + ) + old_assessment_2_new[assessment] = new_assessment + + Answer = apps.get_model('FAIRshakeAPI', 'Answer' if forwards else 'AnswerNew') + AnswerNew = apps.get_model('FAIRshakeAPI', 'AnswerNew' if forwards else 'Answer') + old_answer_2_new = {} + for answer in Answer.objects.all(): + new_answer = AnswerNew.objects.create( + assessment=old_assessment_2_new[answer.assessment], + metric=old_metric_2_new[answer.metric], + answer=answer.answer, + comment=answer.comment, + url_comment=answer.url_comment, + ) + old_answer_2_new[answer] = new_answer + + AssessmentRequest = apps.get_model('FAIRshakeAPI', 'AssessmentRequest' if forwards else 'AssessmentRequestNew') + AssessmentRequestNew = apps.get_model('FAIRshakeAPI', 'AssessmentRequestNew' if forwards else 'AssessmentRequest') + old_assessment_request_2_new = {} + for assessment_request in AssessmentRequest.objects.all(): + new_assessment_request = AssessmentRequestNew.objects.create( + assessment=old_assessment_2_new[assessment_request.assessment], + requestor=assessment_request.requestor, + timestamp=assessment_request.timestamp, + ) + old_assessment_request_2_new[assessment_request] = new_assessment_request + + return migrate + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0008_auto_20180910_1933'), + ] + + operations = [ + migrations.RunPython( + migrate_db(forwards=False), + migrate_db(forwards=True), + ) + ] diff --git a/FAIRshakeAPI/migrations/0010_auto_20180910_1935.py b/FAIRshakeAPI/migrations/0010_auto_20180910_1935.py new file mode 100644 index 0000000..c34f832 --- /dev/null +++ b/FAIRshakeAPI/migrations/0010_auto_20180910_1935.py @@ -0,0 +1,94 @@ +# Generated by Django 2.0.7 on 2018-09-10 19:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0009_auto_20180910_1933'), + ] + + operations = [ + migrations.RemoveField( + model_name='answernew', + name='assessment', + ), + migrations.RemoveField( + model_name='answernew', + name='metric', + ), + migrations.RemoveField( + model_name='assessmentnew', + name='assessor', + ), + migrations.RemoveField( + model_name='assessmentnew', + name='project', + ), + migrations.RemoveField( + model_name='assessmentnew', + name='rubric', + ), + migrations.RemoveField( + model_name='assessmentnew', + name='target', + ), + migrations.RemoveField( + model_name='assessmentrequestnew', + name='assessment', + ), + migrations.RemoveField( + model_name='assessmentrequestnew', + name='requestor', + ), + migrations.RemoveField( + model_name='digitalobjectnew', + name='authors', + ), + migrations.RemoveField( + model_name='digitalobjectnew', + name='rubrics', + ), + migrations.RemoveField( + model_name='metricnew', + name='authors', + ), + migrations.RemoveField( + model_name='projectnew', + name='authors', + ), + migrations.RemoveField( + model_name='projectnew', + name='digital_objects', + ), + migrations.RemoveField( + model_name='rubricnew', + name='authors', + ), + migrations.RemoveField( + model_name='rubricnew', + name='metrics', + ), + migrations.DeleteModel( + name='AnswerNew', + ), + migrations.DeleteModel( + name='AssessmentNew', + ), + migrations.DeleteModel( + name='AssessmentRequestNew', + ), + migrations.DeleteModel( + name='DigitalObjectNew', + ), + migrations.DeleteModel( + name='MetricNew', + ), + migrations.DeleteModel( + name='ProjectNew', + ), + migrations.DeleteModel( + name='RubricNew', + ), + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index ff941ae..e7618fb 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -1,10 +1,11 @@ +import uuid import logging from django.db import models from django.contrib.auth.models import AbstractUser from collections import OrderedDict from extensions.versions_ex.models import VersionableEx as Versionable, VersionedForeignKey, VersionedManyToManyField -class IdentifiableModelMixinNew(Versionable): +class IdentifiableModelMixin(Versionable): title = models.CharField(max_length=255, blank=False) url = models.CharField(max_length=255, blank=True) # urls = ArrayField(models.CharField(max_length=255), blank=True) @@ -50,8 +51,8 @@ def __str__(self): class Meta: abstract = True -class ProjectNew(IdentifiableModelMixinNew): - digital_objects = VersionedManyToManyField('DigitalObjectNew', blank=True, related_name='projects') +class Project(IdentifiableModelMixin): + digital_objects = VersionedManyToManyField('DigitalObject', blank=True, related_name='projects') class Meta: verbose_name = 'project' @@ -63,222 +64,14 @@ class MetaEx: 'digital_objects', ] -class DigitalObjectNew(IdentifiableModelMixinNew): +class DigitalObject(IdentifiableModelMixin): # A digital object's title is optional while its url is mandatory, unlike the rest of the identifiables url = models.CharField(max_length=255, blank=False) # urls = ArrayField(models.CharField(max_length=255), blank=False) title = models.CharField(max_length=255, blank=True, null=False, default='') fairsharing = models.CharField(max_length=255, blank=True, null=False, default='') - rubrics = VersionedManyToManyField('RubricNew', blank=True, related_name='digital_objects') - - class Meta: - verbose_name = 'digital_object' - verbose_name_plural = 'digital_objects' - ordering = ['id'] - - class MetaEx: - children = [ - 'projects', - 'rubrics', - ] - -class RubricNew(IdentifiableModelMixinNew): - license = models.CharField(max_length=255, blank=True, null=False, default='') - - metrics = VersionedManyToManyField('MetricNew', blank=True, related_name='rubrics') - - class Meta: - verbose_name = 'rubric' - verbose_name_plural = 'rubrics' - ordering = ['id'] - - class MetaEx: - children = [ - 'metrics', - 'digital_objects', - ] - -class MetricNew(IdentifiableModelMixinNew): - type = models.CharField(max_length=16, blank=True, null=False, default='yesnobut', choices=( - ('yesnobut', 'Yes no or but question'), - ('text', 'Simple textbox input'), - ('url', 'A url input'), - )) - - license = models.CharField(max_length=255, blank=True, null=False, default='') - - rationale = models.TextField(blank=True, null=False, default='') - principle = models.CharField(max_length=16, blank=True, null=False, default='', choices=( - ('F', 'Findability',), - ('A', 'Accessibility',), - ('I', 'Interoperability',), - ('R', 'Reusability',), - )) - fairmetrics = models.CharField(max_length=255, blank=True, null=False, default='') - - class Meta: - verbose_name = 'metric' - verbose_name_plural = 'metrics' - ordering = ['id'] - - class MetaEx: - children = [ - 'rubrics', - ] - -class AssessmentNew(Versionable): - project = models.ForeignKey('ProjectNew', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='assessments') - target = models.ForeignKey('DigitalObjectNew', on_delete=models.CASCADE, editable=False, related_name='assessments') - rubric = models.ForeignKey('RubricNew', on_delete=models.CASCADE, editable=False, related_name='assessments') - methodology = models.TextField(max_length=16, blank=True, choices=( - ('self', 'Digital Object Creator Assessment'), - ('user', 'Independent User Assessment'), - ('auto', 'Automatic Assessment'), - ('test', 'Test Assessment'), - )) - assessor = models.ForeignKey('Author', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='+') - - def has_permission(self, user, perm): - if perm in ['list', 'retrieve']: - return True - elif perm in ['create', 'add']: - return user.is_authenticated or user.is_staff - elif perm in ['modify', 'remove', 'delete']: - if self is None: - return user.is_authenticated - else: - return (self and self.assessor == user) or user.is_staff - else: - logging.warning('perm %s not handled' % (perm)) - return user.is_staff - - def __str__(self): - return '{methodology} assessment on Target[{target}] for Project[{project}] with Rubric[{rubric}] ({id})'.format( - id=self.id, - project=self.project, - target=self.target, - rubric=self.rubric, - methodology=self.methodology - ) - - class Meta: - verbose_name = 'assessment' - verbose_name_plural = 'assessments' - ordering = ['id'] - - class MetaEx: - children = [ - 'answers', - ] - -class AnswerNew(Versionable): - assessment = models.ForeignKey('AssessmentNew', on_delete=models.CASCADE, editable=False, related_name='answers') - metric = models.ForeignKey('MetricNew', on_delete=models.CASCADE, editable=False, related_name='answers') - answer = models.TextField(blank=True, null=False, default='') - comment = models.TextField(blank=True, null=False, default='') - url_comment = models.TextField(blank=True, null=False, default='') - -# yesnomaybe (depends on metric__type) - def value(self): - return { - 'yes': 1, - 'yesbut': 0.75, - 'nobut': 0.25, - 'no': 0, - '': 0, - }.get(self.answer, 1) - - def inverse(self): - return { - 1: 'yes', - 0.75: 'yesbut', - 0.25: 'nobut', - 0: 'no', - }.get(self.answer, 'yes') - - def has_permission(self, user, perm): - return (self and self.assessment.has_permission(user, perm)) or user.is_staff - - def __str__(self): - return 'Answer to Metric[{metric}] for assessment[{assessment}]: {answer} ({id})'.format( - id=self.id, - assessment=self.assessment, - metric=self.metric, - answer=self.answer, - ) - - class Meta: - verbose_name = 'answer' - verbose_name_plural = 'answers' - ordering = ['id'] - -class IdentifiableModelMixin(models.Model): - id = models.AutoField(primary_key=True) - - title = models.CharField(max_length=255, blank=False) - url = models.CharField(max_length=255, blank=True, null=False, default='') - description = models.TextField(blank=True, null=False, default='') - image = models.CharField(max_length=255, blank=True, null=False, default='') - tags = models.CharField(max_length=255, blank=True, null=False, default='') - - type = models.CharField(max_length=16, blank=True, null=False, default='', choices=( - ('', 'Other'), - ('any', 'Any Digital Object'), - ('data', 'Dataset'), - ('repo', 'Repository'), - ('test', 'Test Object'), - ('tool', 'Tool'), - )) - - authors = models.ManyToManyField('Author', blank=True) - - def tags_as_list(self): - return self.tags.split() - - def model_name(self): - return self._meta.verbose_name_raw - - def has_permission(self, user, perm): - if perm in ['list', 'retrieve', 'stats']: - return True - elif perm in ['create', 'add']: - return user.is_authenticated or user.is_staff - elif perm in ['modify', 'remove', 'delete']: - if self is None: - return user.is_authenticated - else: - return (self.authors and self.authors.filter(id=user.id).exists()) or user.is_staff - else: - logging.warning('perm %s not handled' % (perm)) - return user.is_staff - - def __str__(self): - return '{title} ({id})'.format(id=self.id, title=self.title) - - class Meta: - abstract = True - -class Project(IdentifiableModelMixin): - digital_objects = models.ManyToManyField('DigitalObject', blank=True, related_name='projects') - - class Meta: - verbose_name = 'project' - verbose_name_plural = 'projects' - ordering = ['id'] - - class MetaEx: - children = [ - 'digital_objects', - ] - -class DigitalObject(IdentifiableModelMixin): - # A digital object's title is optional while its url is mandator, unlike the rest of the identifiables - title = models.CharField(max_length=255, blank=True, null=False, default='') - url = models.CharField(max_length=255, blank=False) - fairsharing = models.CharField(max_length=255, blank=True, null=False, default='') - - rubrics = models.ManyToManyField('Rubric', blank=True, related_name='digital_objects') + rubrics = VersionedManyToManyField('Rubric', blank=True, related_name='digital_objects') class Meta: verbose_name = 'digital_object' @@ -294,7 +87,7 @@ class MetaEx: class Rubric(IdentifiableModelMixin): license = models.CharField(max_length=255, blank=True, null=False, default='') - metrics = models.ManyToManyField('Metric', blank=True, related_name='rubrics') + metrics = VersionedManyToManyField('Metric', blank=True, related_name='rubrics') class Meta: verbose_name = 'rubric' @@ -335,19 +128,17 @@ class MetaEx: 'rubrics', ] -class Assessment(models.Model): - id = models.AutoField(primary_key=True) - project = models.ForeignKey('Project', on_delete=models.SET_NULL, blank=True, null=True, related_name='assessments') - target = models.ForeignKey('DigitalObject', on_delete=models.CASCADE, related_name='assessments') - rubric = models.ForeignKey('Rubric', on_delete=models.CASCADE, related_name='assessments') +class Assessment(Versionable): + project = models.ForeignKey('Project', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='assessments') + target = models.ForeignKey('DigitalObject', on_delete=models.CASCADE, editable=False, related_name='assessments') + rubric = models.ForeignKey('Rubric', on_delete=models.CASCADE, editable=False, related_name='assessments') methodology = models.TextField(max_length=16, blank=True, choices=( ('self', 'Digital Object Creator Assessment'), ('user', 'Independent User Assessment'), ('auto', 'Automatic Assessment'), ('test', 'Test Assessment'), )) - assessor = models.ForeignKey('Author', on_delete=models.SET_NULL, related_name='+', blank=True, null=True) - timestamp = models.DateTimeField(auto_now_add=True) + assessor = models.ForeignKey('Author', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='+') def has_permission(self, user, perm): if perm in ['list', 'retrieve']: @@ -382,10 +173,9 @@ class MetaEx: 'answers', ] -class Answer(models.Model): - id = models.AutoField(primary_key=True) - assessment = models.ForeignKey('Assessment', on_delete=models.CASCADE, related_name='answers') - metric = models.ForeignKey('Metric', on_delete=models.CASCADE, related_name='answers') +class Answer(Versionable): + assessment = models.ForeignKey('Assessment', on_delete=models.CASCADE, editable=False, related_name='answers') + metric = models.ForeignKey('Metric', on_delete=models.CASCADE, editable=False, related_name='answers') answer = models.TextField(blank=True, null=False, default='') comment = models.TextField(blank=True, null=False, default='') url_comment = models.TextField(blank=True, null=False, default='') @@ -425,7 +215,7 @@ class Meta: ordering = ['id'] class AssessmentRequest(models.Model): - id = models.AutoField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid.uuid4) assessment = models.OneToOneField('Assessment', on_delete=models.CASCADE, related_name='request') requestor = models.ForeignKey('Author', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, default='') timestamp = models.DateTimeField(auto_now_add=True) From 13432c9f8178bf5179f323b1c728b2e1ee54b53a Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 10 Sep 2018 16:31:39 -0400 Subject: [PATCH 05/23] objects -> objects.current CustomModelViewSet `get_queryset` fix --- FAIRshakeAPI/search.py | 2 +- FAIRshakeAPI/serializers.py | 12 ++++++------ FAIRshakeAPI/stats.py | 34 +++++++++++++++++----------------- FAIRshakeAPI/views.py | 27 ++++++++++++++------------- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/FAIRshakeAPI/search.py b/FAIRshakeAPI/search.py index 102dc33..9256355 100644 --- a/FAIRshakeAPI/search.py +++ b/FAIRshakeAPI/search.py @@ -4,7 +4,7 @@ class SearchVector: def __init__(self, qs=None): - self.queryset = qs or self.get_model().objects.all() + self.queryset = qs or self.get_model().objects.current.all() def get_model(self): return self.model diff --git a/FAIRshakeAPI/serializers.py b/FAIRshakeAPI/serializers.py index dc8a44d..dfb861c 100644 --- a/FAIRshakeAPI/serializers.py +++ b/FAIRshakeAPI/serializers.py @@ -55,9 +55,9 @@ class Meta: ) class AssessmentSerializer(serializers.ModelSerializer): - project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all()) - target = serializers.PrimaryKeyRelatedField(queryset=models.DigitalObject.objects.all()) - rubric = serializers.PrimaryKeyRelatedField(queryset=models.Rubric.objects.all()) + project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.current.all()) + target = serializers.PrimaryKeyRelatedField(queryset=models.DigitalObject.objects.current.all()) + rubric = serializers.PrimaryKeyRelatedField(queryset=models.Rubric.objects.current.all()) answers = AnswerSerializer(many=True) @@ -79,9 +79,9 @@ class Meta: ) class AssessmentResponseSerializer(serializers.ModelSerializer): - project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all()) - target = serializers.PrimaryKeyRelatedField(queryset=models.DigitalObject.objects.all()) - rubric = serializers.PrimaryKeyRelatedField(queryset=models.Rubric.objects.all()) + project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.current.all()) + target = serializers.PrimaryKeyRelatedField(queryset=models.DigitalObject.objects.current.all()) + rubric = serializers.PrimaryKeyRelatedField(queryset=models.Rubric.objects.current.all()) class Meta: model = models.Assessment diff --git a/FAIRshakeAPI/stats.py b/FAIRshakeAPI/stats.py index aae95a1..3ed47da 100644 --- a/FAIRshakeAPI/stats.py +++ b/FAIRshakeAPI/stats.py @@ -35,12 +35,12 @@ def RubricPieChart(assessments_with_rubric): rubrics=list(rubrics_dict.keys()) evals=list(rubrics_dict.values()) evals = [x/9 for x in evals] - rubric_names=[models.Rubric.objects.filter(id=x).values_list('title', flat=True).get() for x in rubrics] + rubric_names=[models.Rubric.objects.current.filter(id=x).values_list('title', flat=True).get() for x in rubrics] fig = [go.Pie(labels=rubric_names, values=evals, hoverinfo='label+value+percent', textinfo='percent')] yield _iplot(fig) def RubricsInProjectsOverlay(answers_within_project,projectid): - rubrics=np.unique(models.Rubric.objects.filter(assessments__project__id__in=[projectid]).values_list('id',flat=True)) + rubrics=np.unique(models.Rubric.objects.current.filter(assessments__project__id__in=[projectid]).values_list('id',flat=True)) all_trace=[] for rubric in rubrics: responses=answers_within_project.filter(assessment__rubric__id=rubric).values_list('answer',flat=True) @@ -50,7 +50,7 @@ def RubricsInProjectsOverlay(answers_within_project,projectid): if x not in scores_dict.keys(): scores_dict[x]=0 trace = go.Bar(x=['no (0)', 'no but (0.25)','yes but (0.75)','yes (1)'],y=[scores_dict['no'],scores_dict['nobut'],scores_dict['yesbut'],scores_dict['yes']], - name= models.Rubric.objects.filter(id=rubric).values_list('title', flat=True).get()) + name= models.Rubric.objects.current.filter(id=rubric).values_list('title', flat=True).get()) all_trace.append(trace) layout = {'xaxis': {'title': 'Answer'}, 'yaxis': {'title': 'Responses'}, @@ -108,47 +108,47 @@ def _DigitalObjectBarGraph(scores_dict): def DigitalObjectBarBreakdown(project): object_score_dict={} - for obj in project.digital_objects.all(): - answers=list(models.Answer.objects.filter(assessment__target__id=obj.id).values_list("answer",flat=True)) + for obj in project.digital_objects.current.all(): + answers=list(models.Answer.objects.current.filter(assessment__target__id=obj.id).values_list("answer",flat=True)) if len(answers)>0: scores=Scoring(answers) mean_score=np.mean(scores) - object_score_dict[models.DigitalObject.objects.filter(id=obj.id).values_list('title', flat=True).get()]=mean_score + object_score_dict[models.DigitalObject.objects.current.filter(id=obj.id).values_list('title', flat=True).get()]=mean_score return _DigitalObjectBarGraph(object_score_dict) # Overall scores for a particular rubric, project, metric...(***Can be placed on each rubric, project, and metric page***) # Get all scores in the database for a particular rubric, project, or metric # Input: Query Set (all answers), type of paramter, parameter ID -# example input query: models.Answer.objects.filter(assessment__project__id=11).all() +# example input query: models.Answer.objects.current.filter(assessment__project__id=11).all() def SingleQuery(querySet, PARAM, ID): if PARAM=="project": - title=models.Project.objects.filter(id=ID).values_list('title', flat=True).get() + title=models.Project.objects.current.filter(id=ID).values_list('title', flat=True).get() responses=querySet.values_list('answer', flat=True) scores=Scoring(responses) if len(scores)!=0: - print("Overall FAIR Evaluations for the project:",models.Project.objects.filter(id=ID).values_list('title', flat=True).get(),"(project id:",ID,")","\n") + print("Overall FAIR Evaluations for the project:",models.Project.objects.current.filter(id=ID).values_list('title', flat=True).get(),"(project id:",ID,")","\n") print("Mean FAIR score:",round(np.mean(scores),2)) print("Median FAIR score:",np.median(scores)) print("Total Assessments:",len(scores)/9) print("Total Questions Answered:",len(scores)) return BarGraphs(scores) if PARAM=="rubric": - title=models.Rubric.objects.filter(id=ID).values_list('title', flat=True).get() + title=models.Rubric.objects.current.filter(id=ID).values_list('title', flat=True).get() responses=querySet.values_list('answer', flat=True) scores=Scoring(responses) if len(scores)!=0: - print("Overall FAIR Evaluations for the rubric:",models.Rubric.objects.filter(id=ID).values_list('title', flat=True).get(),"(rubric id:",ID,")","\n") + print("Overall FAIR Evaluations for the rubric:",models.Rubric.objects.current.filter(id=ID).values_list('title', flat=True).get(),"(rubric id:",ID,")","\n") print("Mean FAIR score:",round(np.mean(scores),2)) print("Median FAIR score:",np.median(scores)) print("Total Assessments:",len(scores)/9) print("Total Questions Answered:",len(scores)) return BarGraphs(scores) if PARAM=="metric": - title=models.Metric.objects.filter(id=ID).values_list('title', flat=True).get() + title=models.Metric.objects.current.filter(id=ID).values_list('title', flat=True).get() responses=querySet.values_list('answer', flat=True) scores=Scoring(responses) if len(scores)!=0: - print("Overall FAIR Evaluations for the metric:",models.Metric.objects.filter(id=ID).values_list('title', flat=True).get(),"(metric id:",ID,")","\n") + print("Overall FAIR Evaluations for the metric:",models.Metric.objects.current.filter(id=ID).values_list('title', flat=True).get(),"(metric id:",ID,")","\n") print("Mean FAIR score:",round(np.mean(scores),2)) print("Median FAIR score:",np.median(scores)) print("Total Assessments:",len(scores)/9) @@ -159,24 +159,24 @@ def TablePlot(project): from django.template import Template, Context metrics = [ metric.title - for obj in project.digital_objects.all() + for obj in project.digital_objects.current.all() for assessment in obj.assessments.all() for metric in assessment.rubric.metrics.all() ] objs = [ obj.title - for obj in project.digital_objects.all() + for obj in project.digital_objects.current.all() ] scores = [ [ np.mean([ answer.value() - for answer in models.Answer.objects.filter(metric=metric, assessment__target=obj) + for answer in models.Answer.objects.current.filter(metric=metric, assessment__target=obj) ]) for assessment in obj.assessments.all() for metric in assessment.rubric.metrics.all() ] - for obj in project.digital_objects.all() + for obj in project.digital_objects.current.all() ] trace = go.Heatmap(z=scores, x=metrics, y=objs) data = [trace] diff --git a/FAIRshakeAPI/views.py b/FAIRshakeAPI/views.py index d4a04c4..db6e68b 100644 --- a/FAIRshakeAPI/views.py +++ b/FAIRshakeAPI/views.py @@ -21,7 +21,7 @@ def callback_or_redirect(request, *args, **kwargs): class CustomTemplateHTMLRenderer(renderers.TemplateHTMLRenderer): def get_template_context(self, data, renderer_context): - context = super(CustomTemplateHTMLRenderer, self).get_template_context(data, renderer_context) or {} + context = super().get_template_context(data, renderer_context) or {} view = renderer_context['view'] request = view.request return view.get_template_context(request, context) @@ -43,7 +43,7 @@ def get_model_name(self): def get_model_children(self, obj): for child in self.get_model().MetaEx.children: child_attr = getattr(obj, child) - yield (child_attr.model._meta.verbose_name_raw, child_attr.all()) + yield (child_attr.model._meta.verbose_name_raw, child_attr.current.all()) def get_form(self): return self.form @@ -54,7 +54,8 @@ def save_form(self, request, form): return instance def get_queryset(self): - return getattr(self, 'queryset', self.get_model().objects.all()) + qs = getattr(self, 'queryset', None) + return self.get_model().objects.current.all() if qs is None else qs def filter_queryset(self, qs): ''' Ensure all resulting filter sets are distinct ''' @@ -225,14 +226,14 @@ def save_form(self, request, form): assessment.methodology = 'user' assessment.save() if not assessment.answers.exists(): - for metric in assessment.rubric.metrics.all(): + for metric in assessment.rubric.metrics.current.all(): answer = models.Answer( assessment=assessment, metric=metric, ) answer.save() - for answer in assessment.answers.all(): + for answer in assessment.answers.current.all(): answer_form = forms.AnswerForm( request.POST, instance=answer, @@ -253,7 +254,7 @@ def get_template_context(self, request, context): assessment_form = forms.AssessmentForm(instance=assessment) answers = [] - for answer in assessment.answers.all(): + for answer in assessment.answers.current.all(): answer_form = forms.AnswerForm( prefix=answer.metric.id, instance=answer, @@ -287,9 +288,9 @@ def get_template_context(self, request, context): else: rubrics = None if target is not None: - rubrics = targets.first().rubrics.all() + rubrics = targets.first().rubrics.current.all() if rubrics is None or not rubrics.exists(): - rubrics = models.Rubric.objects.all() + rubrics = models.Rubric.objects.current.all() if rubrics.count() == 1: rubric = rubrics.first().id @@ -298,9 +299,9 @@ def get_template_context(self, request, context): else: projects = None if target is not None: - projects = targets.first().projects.all() + projects = targets.first().projects.current.all() if projects is None or projects.exists(): - projects = models.Project.objects.all() + projects = models.Project.objects.current.all() if projects.count() == 1: project = projects.first().id @@ -331,7 +332,7 @@ def get_template_context(self, request, context): assessment.assessor = request.user answers = [] - for metric in assessment.rubric.metrics.all(): + for metric in assessment.rubric.metrics.current.all(): answer = models.Answer( assessment=assessment, metric=metric, @@ -372,7 +373,7 @@ class ScoreViewSet( ): ''' Request an score for a digital resource ''' - queryset = models.Assessment.objects.all() + queryset = models.Assessment.objects.current.all() serializer_class = serializers.AssessmentSerializer filter_class = filters.ScoreFilterSet pagination_class = None @@ -425,7 +426,7 @@ def hist(self, request): if answers is None: answers = {} for assessment in self.filter_queryset(self.get_queryset()): - for answer in assessment.answers.all(): + for answer in assessment.answers.current.all(): value = answer.value() answers[value] = answers.get(value, 0) + 1 cache.set(key, answers, 60 * 60) From c81ce765dd624be53bcbdaf6a54e0edf96a456d6 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 10 Sep 2018 17:49:43 -0400 Subject: [PATCH 06/23] WIP: refactoring of FAIRshakeAPI.views for version controlled assessment References #43 References #46 --- FAIRshakeAPI/forms.py | 57 ++- FAIRshakeAPI/views.py | 401 +++++++++--------- .../templates/fairshake/assessment/add.html | 9 +- .../templates/fairshake/generic/element.html | 2 +- .../templates/fairshake/generic/retrieve.html | 2 +- 5 files changed, 260 insertions(+), 211 deletions(-) diff --git a/FAIRshakeAPI/forms.py b/FAIRshakeAPI/forms.py index fc89da4..46f8f8e 100644 --- a/FAIRshakeAPI/forms.py +++ b/FAIRshakeAPI/forms.py @@ -33,37 +33,58 @@ class Meta: class ProjectForm(IdentifiableForm): class Meta: model = models.Project - exclude = ('authors',) + fields = ( + 'title', + 'url', + 'description', + 'image', + 'tags', + 'type', + 'digital_objects', + ) class DigitalObjectForm(IdentifiableForm): class Meta: model = models.DigitalObject - exclude = ('authors',) + fields = ( + 'title', + 'url', + 'description', + 'image', + 'tags', + 'type', + 'fairsharing', + 'rubrics', + ) class RubricForm(IdentifiableForm): class Meta: model = models.Rubric - exclude = ('authors',) + fields = ( + 'title', + 'url', + 'description', + 'image', + 'tags', + 'type', + 'license', + 'metrics', + ) class MetricForm(IdentifiableForm): class Meta: model = models.Metric - exclude = ('authors',) - -class AssessmentForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - super(AssessmentForm, self).__init__(*args, **kwargs) - - self.fields['target'].widget = forms.HiddenInput() - self.fields['rubric'].widget = forms.HiddenInput() - self.fields['project'].widget = forms.HiddenInput() - - class Meta: - model = models.Assessment fields = ( - 'target', - 'rubric', - 'project', + 'title', + 'url', + 'description', + 'image', + 'tags', + 'type', + 'license', + 'rationale', + 'principle', + 'fairmetrics', ) class AnswerForm(forms.ModelForm): diff --git a/FAIRshakeAPI/views.py b/FAIRshakeAPI/views.py index db6e68b..b5c76bc 100644 --- a/FAIRshakeAPI/views.py +++ b/FAIRshakeAPI/views.py @@ -19,6 +19,12 @@ def callback_or_redirect(request, *args, **kwargs): else: return shortcuts.redirect(callback) +def get_or_create(model, **kwargs): + try: + return model.objects.current.get(**kwargs) + except: + return model.objects.create(**kwargs) + class CustomTemplateHTMLRenderer(renderers.TemplateHTMLRenderer): def get_template_context(self, data, renderer_context): context = super().get_template_context(data, renderer_context) or {} @@ -39,19 +45,6 @@ def get_model(self): def get_model_name(self): return self.get_model()._meta.verbose_name_raw - - def get_model_children(self, obj): - for child in self.get_model().MetaEx.children: - child_attr = getattr(obj, child) - yield (child_attr.model._meta.verbose_name_raw, child_attr.current.all()) - - def get_form(self): - return self.form - - def save_form(self, request, form): - instance = form.save() - instance.authors.add(request.user) - return instance def get_queryset(self): qs = getattr(self, 'queryset', None) @@ -64,55 +57,37 @@ def filter_queryset(self, qs): def get_template_names(self): return ['fairshake/generic/page.html'] - def get_detail_template_context(self, request, context): - paginator_cls = self.paginator.django_paginator_class - page_size = settings.REST_FRAMEWORK['VIEW_PAGE_SIZE'] - item = self.get_object() - form_cls = self.get_form() - form = form_cls(instance=item) - - return { - 'item': item, - 'form': form, - 'children': { - child: paginator_cls( - child_attr, - page_size, - ).get_page( - request.GET.get('page') - ) - for child, child_attr in self.get_model_children(item) - }, - } + def get_template_context(self, request, context): + return getattr( + self, + 'get_%s_template_context' % (self.action), + getattr( + self, + 'get_%s_template_context' % ('detail' if self.detail else 'list'), + lambda request, context: context + ) + )( + request, + dict( + context, + model=self.get_model_name(), + action=self.action, + ), + ) - def get_list_template_context(self, request, context): - paginator_cls = self.paginator.django_paginator_class - page_size = settings.REST_FRAMEWORK['VIEW_PAGE_SIZE'] - form_cls = self.get_form() - form = form_cls(request.GET) +class IdentifiableModelViewSet(CustomModelViewSet): + def get_form(self): + return self.form - return { - 'form': form, - 'items': paginator_cls( - self.filter_queryset( - self.get_queryset() - ), - page_size, - ).get_page( - request.GET.get('page') - ), - } + def get_model_children(self, obj): + for child in self.get_model().MetaEx.children: + child_attr = getattr(obj, child) + yield (child_attr.model._meta.verbose_name_raw, child_attr.current.all()) - def get_template_context(self, request, context): - return dict(context, - model=self.get_model_name(), - action=self.action, - **getattr(self, 'get_%s_template_context' % (self.action), - getattr(self, 'get_%s_template_context' % ('detail' if self.detail else 'list'), - lambda *args: args - ) - )(request, context), - ) + def save_form(self, request, form): + instance = form.save() + instance.authors.add(request.user) + return instance @decorators.action( detail=False, methods=['get', 'post'], @@ -159,19 +134,73 @@ def remove(self, request, pk=None): self.get_model_name()+'-list' ) -class DigitalObjectViewSet(CustomModelViewSet): + def get_add_template_context(self, request, context): + form_cls = self.get_form() + form = form_cls(request.GET) + + return dict(context, **{ + 'form': form, + }) + + def get_modify_template_context(self, request, context): + item = self.get_object() + form_cls = self.get_form() + form = form_cls(instance=item) + + return dict(context, **{ + 'item': item, + 'form': form, + }) + + def get_retrieve_template_context(self, request, context): + paginator_cls = self.paginator.django_paginator_class + page_size = settings.REST_FRAMEWORK['VIEW_PAGE_SIZE'] + item = self.get_object() + + return dict(context, **{ + 'item': item, + 'children': { + child: paginator_cls( + child_attr, + page_size, + ).get_page( + request.GET.get('page') + ) + for child, child_attr in self.get_model_children(item) + }, + }) + + def get_list_template_context(self, request, context): + paginator_cls = self.paginator.django_paginator_class + page_size = settings.REST_FRAMEWORK['VIEW_PAGE_SIZE'] + form_cls = self.get_form() + form = form_cls(request.GET) + + return dict(context, **{ + 'form': form, + 'items': paginator_cls( + self.filter_queryset( + self.get_queryset() + ), + page_size, + ).get_page( + request.GET.get('page') + ), + }) + +class DigitalObjectViewSet(IdentifiableModelViewSet): model = models.DigitalObject form = forms.DigitalObjectForm serializer_class = serializers.DigitalObjectSerializer filter_class = filters.DigitalObjectFilterSet -class MetricViewSet(CustomModelViewSet): +class MetricViewSet(IdentifiableModelViewSet): model = models.Metric form = forms.MetricForm serializer_class = serializers.MetricSerializer filter_class = filters.MetricFilterSet -class ProjectViewSet(CustomModelViewSet): +class ProjectViewSet(IdentifiableModelViewSet): model = models.Project form = forms.ProjectForm serializer_class = serializers.ProjectSerializer @@ -199,7 +228,7 @@ def get_stats_template_context(self, request, context): ] }) -class RubricViewSet(CustomModelViewSet): +class RubricViewSet(IdentifiableModelViewSet): model = models.Rubric form = forms.RubricForm serializer_class = serializers.RubricSerializer @@ -207,152 +236,146 @@ class RubricViewSet(CustomModelViewSet): class AssessmentViewSet(CustomModelViewSet): model = models.Assessment - form = forms.AssessmentForm serializer_class = serializers.AssessmentSerializer filter_classes = filters.AssessmentFilterSet def get_queryset(self): if self.request.user.is_anonymous: return models.Assessment.objects.none() - return models.Assessment.objects.filter( + return models.Assessment.objects.current.filter( Q(target__authors=self.request.user) | Q(project__authors=self.request.user) | Q(assessor=self.request.user) ) + + def get_objects(self, request): + target_id = request.GET.get('target', None) + rubric_id = request.GET.get('rubric', None) + project_id = request.GET.get('project', None) + + if target_id is None or rubric_id is None: + pass # TODO redirect to prepare + + if project_id: + assessment = get_or_create(models.Assessment, + project=models.project.objects.get(id=project_id), + target=models.target.objects.get(id=target_id), + rubric=models.rubric.objects.get(id=rubric_id), + assessor=request.user, + methodology='user', + ) + else: + assessment = get_or_create(models.Assessment, + target=models.target.objects.get(id=target_id), + rubric=models.rubric.objects.get(id=rubric_id), + assessor=request.user, + methodology='user', + ) + + answers = [] + for metric in assessment.rubric.metrics.current.all(): + answer = get_or_create(models.Answer, + assessment=assessment, + metric=metric, + ) + answer_form = forms.AnswerForm( + dict(request.GET, **request.POST), + instance=answer, + prefix=answer.metric.id, + ) + answers.append({ + 'form': answer_form, + 'instance': answer, + }) - def save_form(self, request, form): - assessment = form.save(commit=False) - assessment.assessor = request.user - assessment.methodology = 'user' - assessment.save() - if not assessment.answers.exists(): - for metric in assessment.rubric.metrics.current.all(): - answer = models.Answer( - assessment=assessment, - metric=metric, - ) - answer.save() + return { + 'assessment': assessment, + 'answers': answers, + } - for answer in assessment.answers.current.all(): - answer_form = forms.AnswerForm( - request.POST, - instance=answer, - prefix=answer.metric.id, - ) - answer_form.save() + def save_form(self, request, form): + item = self.get_objects(request) + for answer in item.answers: + answer['form'].save() cache.delete_many([ ','.join(map('='.join, request.GET.items())), *map('='.join, request.GET.items()), ]) - return assessment - - def get_template_context(self, request, context): - if self.action in ['modify', 'retrieve']: - assessment = self.get_object() - assessment_form = forms.AssessmentForm(instance=assessment) - - answers = [] - for answer in assessment.answers.current.all(): - answer_form = forms.AnswerForm( - prefix=answer.metric.id, - instance=answer, - ) - answers.append({ - 'form': answer_form, - 'instance': answer, - }) - - return dict(context, **{ - 'form': assessment_form, - 'item': assessment, - 'answers': answers, - }) - elif self.action in ['add']: - assessment_form = forms.AssessmentForm(request.GET) - prepare = request.GET.get('prepare') - if not assessment_form.is_valid() or prepare is not None: - target = request.GET.get('target') - rubric = request.GET.get('rubric') - project = request.GET.get('project') - q = request.GET.get('q', '') - - if target is not None: - targets = models.DigitalObject.objects.filter(id=target) - else: - targets = search.DigitalObjectSearchVector().query(q) - - if rubric is not None: - rubrics = models.Rubric.objects.filter(id=target) - else: - rubrics = None - if target is not None: - rubrics = targets.first().rubrics.current.all() - if rubrics is None or not rubrics.exists(): - rubrics = models.Rubric.objects.current.all() - if rubrics.count() == 1: - rubric = rubrics.first().id - - if project is not None: - projects = models.Project.objects.filter(id=project) - else: - projects = None - if target is not None: - projects = targets.first().projects.current.all() - if projects is None or projects.exists(): - projects = models.Project.objects.current.all() - if projects.count() == 1: - project = projects.first().id - - if project is not None: - assessment_form = forms.AssessmentForm(dict(request.GET, **{ - 'target': targets.first().id, - 'rubric': rubrics.first().id, - 'project': projects.first().id, - })) - else: - assessment_form = forms.AssessmentForm(dict(request.GET, **{ - 'target': targets.first().id, - 'rubric': rubrics.first().id, - })) - - if prepare is not None or not assessment_form.is_valid(): - assessment_form.fields['target'] = ModelChoiceField(queryset=targets, required=True) - assessment_form.fields['rubric'] = ModelChoiceField(queryset=rubrics, required=True) - assessment_form.fields['project'] = ModelChoiceField(queryset=projects, required=False) - - return { - 'model': self.get_model_name(), - 'action': 'prepare', - 'form': assessment_form, - } + @decorators.action( + detail=False, methods=['get'], + renderer_classes=[CustomTemplateHTMLRenderer], + ) + def perform(self, request, **kwargs): + self.check_permissions(request) + + @decorators.action( + detail=False, methods=['get'], + renderer_classes=[CustomTemplateHTMLRenderer], + ) + def prepare(self, request, **kwargs): + self.check_permissions(request) - assessment = assessment_form.save(commit=False) - assessment.assessor = request.user + def get_detail_template_context(self, request, context): + item = self.get_objects(request) + return dict(context, **item) - answers = [] - for metric in assessment.rubric.metrics.current.all(): - answer = models.Answer( - assessment=assessment, - metric=metric, - ) - answer_form = forms.AnswerForm( - request.GET, - prefix=metric.id, - instance=answer, - ) - answers.append({ - 'form': answer_form, - 'instance': answer, - }) - - return dict(context, **{ - 'form': assessment_form, - 'item': assessment, - 'answers': answers, - }) - return super().get_template_context(request, context) + def get_list_template_context(self, request, context): + # List rubric objects + paginator_cls = self.paginator.django_paginator_class + page_size = settings.REST_FRAMEWORK['VIEW_PAGE_SIZE'] + + return dict(context, **{ + 'items': paginator_cls( + self.filter_queryset( + self.get_queryset() + ), + page_size, + ).get_page( + request.GET.get('page') + ), + }) + + def get_prepare_template_context(self, request, context): + if target is not None: + targets = models.DigitalObject.objects.filter(id=target) + else: + targets = search.DigitalObjectSearchVector().query(q) + + if rubric is not None: + rubrics = models.Rubric.objects.filter(id=target) + else: + rubrics = None + if target is not None: + rubrics = targets.first().rubrics.current.all() + if rubrics is None or not rubrics.exists(): + rubrics = models.Rubric.objects.current.all() + if rubrics.count() == 1: + rubric = rubrics.first().id + + if project is not None: + projects = models.Project.objects.filter(id=project) + else: + projects = None + if target is not None: + projects = targets.first().projects.current.all() + if projects is None or projects.exists(): + projects = models.Project.objects.current.all() + if projects.count() == 1: + project = projects.first().id + + if project is not None: + assessment_form = forms.AssessmentForm(dict(request.GET, **{ + 'target': targets.first().id, + 'rubric': rubrics.first().id, + 'project': projects.first().id, + })) + else: + assessment_form = forms.AssessmentForm(dict(request.GET, **{ + 'target': targets.first().id, + 'rubric': rubrics.first().id, + })) class AssessmentRequestViewSet(CustomModelViewSet): model = models.AssessmentRequest diff --git a/FAIRshakeHub/templates/fairshake/assessment/add.html b/FAIRshakeHub/templates/fairshake/assessment/add.html index 87e7927..c1f02c0 100644 --- a/FAIRshakeHub/templates/fairshake/assessment/add.html +++ b/FAIRshakeHub/templates/fairshake/assessment/add.html @@ -1,6 +1,12 @@ {% load bootstrap %} {% load filters %} -
+ {{ model|unslugify }} of {{ item.target.title }} @@ -17,7 +23,6 @@ {% csrf_token %} - {{ form|bootstrap }} {% for answer in answers %}
{% with model="metric" item=answer.instance.metric %} diff --git a/FAIRshakeHub/templates/fairshake/generic/element.html b/FAIRshakeHub/templates/fairshake/generic/element.html index af2d1db..5ecb540 100644 --- a/FAIRshakeHub/templates/fairshake/generic/element.html +++ b/FAIRshakeHub/templates/fairshake/generic/element.html @@ -15,7 +15,7 @@ {% if model == "digital_object" and user.is_authenticated %}
diff --git a/FAIRshakeHub/templates/fairshake/generic/retrieve.html b/FAIRshakeHub/templates/fairshake/generic/retrieve.html index a2c3ba7..3bb3359 100644 --- a/FAIRshakeHub/templates/fairshake/generic/retrieve.html +++ b/FAIRshakeHub/templates/fairshake/generic/retrieve.html @@ -69,7 +69,7 @@

{{ item.title }}

{% if model == "digital_object" and user.is_authenticated %}
From 6c577184150f322395022b9fbd2a7dae239e5378 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Tue, 11 Sep 2018 12:04:31 -0400 Subject: [PATCH 07/23] Base64-serialize uuid fields --- .../migrations/0011_auto_20180911_1605.py | 80 +++++++++++++++++++ FAIRshakeAPI/models.py | 7 +- extensions/versions_ex/fields.py | 10 +++ extensions/versions_ex/models.py | 5 ++ 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 FAIRshakeAPI/migrations/0011_auto_20180911_1605.py create mode 100644 extensions/versions_ex/fields.py diff --git a/FAIRshakeAPI/migrations/0011_auto_20180911_1605.py b/FAIRshakeAPI/migrations/0011_auto_20180911_1605.py new file mode 100644 index 0000000..33507a3 --- /dev/null +++ b/FAIRshakeAPI/migrations/0011_auto_20180911_1605.py @@ -0,0 +1,80 @@ +# Generated by Django 2.0.7 on 2018-09-11 16:05 + +from django.db import migrations +import extensions.versions_ex.fields +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0010_auto_20180910_1935'), + ] + + operations = [ + migrations.AlterField( + model_name='answer', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='answer', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + migrations.AlterField( + model_name='assessment', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='assessment', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + migrations.AlterField( + model_name='assessmentrequest', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='digitalobject', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='digitalobject', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + migrations.AlterField( + model_name='metric', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='metric', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + migrations.AlterField( + model_name='project', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='project', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + migrations.AlterField( + model_name='rubric', + name='id', + field=extensions.versions_ex.fields.CustomUUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='rubric', + name='identity', + field=extensions.versions_ex.fields.CustomUUIDField(), + ), + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index e7618fb..806cfbc 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -3,7 +3,8 @@ from django.db import models from django.contrib.auth.models import AbstractUser from collections import OrderedDict -from extensions.versions_ex.models import VersionableEx as Versionable, VersionedForeignKey, VersionedManyToManyField +from extensions.versions_ex.models import VersionableEx as Versionable, VersionedManyToManyField +from extensions.versions_ex.fields import CustomUUIDField class IdentifiableModelMixin(Versionable): title = models.CharField(max_length=255, blank=False) @@ -44,7 +45,7 @@ def has_permission(self, user, perm): else: logging.warning('perm %s not handled' % (perm)) return user.is_staff - + def __str__(self): return '{title} ({id})'.format(id=self.id, title=self.title) @@ -215,7 +216,7 @@ class Meta: ordering = ['id'] class AssessmentRequest(models.Model): - id = models.UUIDField(primary_key=True, default=uuid.uuid4) + id = CustomUUIDField(primary_key=True, default=uuid.uuid4) assessment = models.OneToOneField('Assessment', on_delete=models.CASCADE, related_name='request') requestor = models.ForeignKey('Author', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, default='') timestamp = models.DateTimeField(auto_now_add=True) diff --git a/extensions/versions_ex/fields.py b/extensions/versions_ex/fields.py new file mode 100644 index 0000000..f338a1c --- /dev/null +++ b/extensions/versions_ex/fields.py @@ -0,0 +1,10 @@ +import uuid +import base64 +from django.db.models import UUIDField + +class CustomUUIDField(UUIDField): + def to_python(self, value): + return uuid.UUID(bytes=base64.urlsafe_b64decode((value + '==').replace('_', '/'))) + + def from_db_value(self, value, expr, connection): + return base64.urlsafe_b64encode(value.bytes).decode("utf-8").rstrip('=\n').replace('/', '_') diff --git a/extensions/versions_ex/models.py b/extensions/versions_ex/models.py index 382842d..3951840 100644 --- a/extensions/versions_ex/models.py +++ b/extensions/versions_ex/models.py @@ -1,5 +1,7 @@ +import uuid from versions.models import Versionable, VersionManager, VersionedQuerySet from versions.fields import VersionedForeignKey, VersionedManyToManyField +from .fields import CustomUUIDField class VersionedQuerySetEx(VersionedQuerySet): def _set_item_querytime(self, item, type_check=False): @@ -32,6 +34,9 @@ def create(*args, **kwargs): return res class VersionableEx(Versionable): + id = CustomUUIDField(default=uuid.uuid4, primary_key=True) + identity = CustomUUIDField() + objects = VersionManagerEx() class Meta: From c1a5f98977418bf2bf51405b88c4425637273bf0 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Wed, 12 Sep 2018 13:29:21 -0400 Subject: [PATCH 08/23] Enabled multiple urls to be specified TODO: - make field larger - ensure they can be queried independently --- FAIRshakeAPI/models.py | 3 +++ FAIRshakeHub/templates/fairshake/generic/retrieve.html | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index 806cfbc..53fa1e2 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -26,6 +26,9 @@ class IdentifiableModelMixin(Versionable): authors = VersionedManyToManyField('Author', blank=True) + def urls_as_list(self): + return self.url.splitlines() + def tags_as_list(self): return self.tags.split() diff --git a/FAIRshakeHub/templates/fairshake/generic/retrieve.html b/FAIRshakeHub/templates/fairshake/generic/retrieve.html index 3bb3359..9d6538e 100644 --- a/FAIRshakeHub/templates/fairshake/generic/retrieve.html +++ b/FAIRshakeHub/templates/fairshake/generic/retrieve.html @@ -48,8 +48,10 @@

{{ item.title }}

{% endif %} {% if item.url %}

- URL: - {{ item.url|limit:50 }} + URL(s): + {% for url in item.urls_as_list %} + {{ url|limit:75 }} + {% endfor %}

{% endif %}
Date: Wed, 12 Sep 2018 17:18:59 -0400 Subject: [PATCH 09/23] Added cache table directions to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 35fcd5e..b434238 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,9 @@ Errors involving mysql trying to load from /tmp/sock arrise when `MYSQL_CONFIG` ### Database issues In general, if the database has changed (and there are new migration files), if you're running a local database you may need to apply new migrations with `./manage.py migrate`. +#### No Cache Table +If the cache table doesn't yet exist, you can create it with `./manage.py createcachetable`. + ### Dependency issues First try re-executing `pip install -r requirements.txt`. From 48a3d443cb85eccd138ce5037880536893b1a01d Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Wed, 12 Sep 2018 17:20:22 -0400 Subject: [PATCH 10/23] Added slug fields (defaulting to uuid) Fixes #65 --- FAIRshakeAPI/forms.py | 17 ++++++++- .../migrations/0012_auto_20180912_2050.py | 38 +++++++++++++++++++ .../migrations/0013_auto_20180912_2051.py | 36 ++++++++++++++++++ .../migrations/0014_auto_20180912_2054.py | 33 ++++++++++++++++ FAIRshakeAPI/models.py | 4 +- FAIRshakeAPI/serializers.py | 1 + FAIRshakeAPI/views.py | 16 ++++---- .../templates/fairshake/assessment/add.html | 8 ++-- .../fairshake/assessment/element.html | 10 ++--- .../fairshake/assessment/modify.html | 8 ++-- .../templates/fairshake/generic/element.html | 8 ++-- .../templates/fairshake/generic/list.html | 2 +- .../templates/fairshake/generic/modify.html | 2 +- .../templates/fairshake/generic/retrieve.html | 8 ++-- .../templates/fairshake/generic/stats.html | 2 +- 15 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 FAIRshakeAPI/migrations/0012_auto_20180912_2050.py create mode 100644 FAIRshakeAPI/migrations/0013_auto_20180912_2051.py create mode 100644 FAIRshakeAPI/migrations/0014_auto_20180912_2054.py diff --git a/FAIRshakeAPI/forms.py b/FAIRshakeAPI/forms.py index 46f8f8e..cba4dcf 100644 --- a/FAIRshakeAPI/forms.py +++ b/FAIRshakeAPI/forms.py @@ -13,7 +13,18 @@ def __init__(self, *args, **kwargs): required=False, help_text=None, ) - + + def clean_slug(self): + slug = self.cleaned_data.get('slug') + try: + if self.Meta.model.objects.current.get(slug=slug) != self.instance: + raise forms.ValidationError( + 'Slug was already taken, please try something different.' + ) + except self.Meta.model.DoesNotExist: + pass + return slug + def save(self, *args, commit=True, **kwargs): ''' Explicitly add children for children in the reverse direction. ''' @@ -40,6 +51,7 @@ class Meta: 'image', 'tags', 'type', + 'slug', 'digital_objects', ) @@ -53,6 +65,7 @@ class Meta: 'image', 'tags', 'type', + 'slug', 'fairsharing', 'rubrics', ) @@ -67,6 +80,7 @@ class Meta: 'image', 'tags', 'type', + 'slug', 'license', 'metrics', ) @@ -81,6 +95,7 @@ class Meta: 'image', 'tags', 'type', + 'slug', 'license', 'rationale', 'principle', diff --git a/FAIRshakeAPI/migrations/0012_auto_20180912_2050.py b/FAIRshakeAPI/migrations/0012_auto_20180912_2050.py new file mode 100644 index 0000000..4e57903 --- /dev/null +++ b/FAIRshakeAPI/migrations/0012_auto_20180912_2050.py @@ -0,0 +1,38 @@ +# Generated by Django 2.0.7 on 2018-09-12 20:50 + +import builtins +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0011_auto_20180911_1605'), + ] + + operations = [ + migrations.AddField( + model_name='digitalobject', + name='slug', + field=models.CharField(max_length=255, default=None, null=True), + preserve_default=False, + ), + migrations.AddField( + model_name='metric', + name='slug', + field=models.CharField(max_length=255, default=None, null=True), + preserve_default=False, + ), + migrations.AddField( + model_name='project', + name='slug', + field=models.CharField(max_length=255, default=None, null=True), + preserve_default=False, + ), + migrations.AddField( + model_name='rubric', + name='slug', + field=models.CharField(max_length=255, default=None, null=True), + preserve_default=False, + ), + ] diff --git a/FAIRshakeAPI/migrations/0013_auto_20180912_2051.py b/FAIRshakeAPI/migrations/0013_auto_20180912_2051.py new file mode 100644 index 0000000..7246fee --- /dev/null +++ b/FAIRshakeAPI/migrations/0013_auto_20180912_2051.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.7 on 2018-09-12 20:51 + +from django.db import migrations + +def migrate(apps, schema_editor): + Project = apps.get_model('FAIRshakeAPI', 'Project') + for project in Project.objects.all(): + project.slug = project.id + project.save() + + DigitalObject = apps.get_model('FAIRshakeAPI', 'DigitalObject') + for obj in DigitalObject.objects.all(): + obj.slug = obj.id + obj.save() + + Rubric = apps.get_model('FAIRshakeAPI', 'Rubric') + for rubric in Rubric.objects.all(): + rubric.slug = rubric.id + rubric.save() + + Metric = apps.get_model('FAIRshakeAPI', 'Metric') + for metric in Metric.objects.all(): + metric.slug = metric.id + metric.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0012_auto_20180912_2050'), + ] + + operations = [ + migrations.RunPython( + migrate + ) + ] diff --git a/FAIRshakeAPI/migrations/0014_auto_20180912_2054.py b/FAIRshakeAPI/migrations/0014_auto_20180912_2054.py new file mode 100644 index 0000000..dbebdc5 --- /dev/null +++ b/FAIRshakeAPI/migrations/0014_auto_20180912_2054.py @@ -0,0 +1,33 @@ +# Generated by Django 2.0.7 on 2018-09-12 20:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0013_auto_20180912_2051'), + ] + + operations = [ + migrations.AlterField( + model_name='digitalobject', + name='slug', + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name='metric', + name='slug', + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name='project', + name='slug', + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name='rubric', + name='slug', + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index 53fa1e2..e72a74f 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -24,6 +24,8 @@ class IdentifiableModelMixin(Versionable): ('tool', 'Tool'), )) + slug = models.CharField(max_length=255, unique=True, blank=False, null=False) + authors = VersionedManyToManyField('Author', blank=True) def urls_as_list(self): @@ -31,7 +33,7 @@ def urls_as_list(self): def tags_as_list(self): return self.tags.split() - + def model_name(self): return self._meta.verbose_name_raw diff --git a/FAIRshakeAPI/serializers.py b/FAIRshakeAPI/serializers.py index dfb861c..954b3be 100644 --- a/FAIRshakeAPI/serializers.py +++ b/FAIRshakeAPI/serializers.py @@ -20,6 +20,7 @@ def update(self, instance, validated_data): class Meta: abstract = True + lookup_field = 'slug' read_only_fields = ( 'id', diff --git a/FAIRshakeAPI/views.py b/FAIRshakeAPI/views.py index b5c76bc..301267c 100644 --- a/FAIRshakeAPI/views.py +++ b/FAIRshakeAPI/views.py @@ -75,7 +75,9 @@ def get_template_context(self, request, context): ), ) -class IdentifiableModelViewSet(CustomModelViewSet): +class IdentifiableModelViewSet(CustomModelViewSet): + lookup_field = 'slug' + def get_form(self): return self.form @@ -93,7 +95,7 @@ def save_form(self, request, form): detail=False, methods=['get', 'post'], renderer_classes=[CustomTemplateHTMLRenderer], ) - def add(self, request, pk=None, **kwargs): + def add(self, request, slug=None, **kwargs): self.check_permissions(request) if request.method == 'GET': return response.Response() @@ -102,7 +104,7 @@ def add(self, request, pk=None, **kwargs): instance = self.save_form(request, form) return callback_or_redirect(request, self.get_model_name()+'-detail', - pk=instance.id, + slug=instance.slug, ) @decorators.action( @@ -110,7 +112,7 @@ def add(self, request, pk=None, **kwargs): methods=['get', 'post'], renderer_classes=[CustomTemplateHTMLRenderer], ) - def modify(self, request, pk=None): + def modify(self, request, slug=None): item = self.get_object() if request.method == 'GET': return response.Response() @@ -119,14 +121,14 @@ def modify(self, request, pk=None): instance = self.save_form(request, form) return callback_or_redirect(request, self.get_model_name()+'-detail', - pk=pk, + slug=instance.slug, ) @decorators.action( detail=True, methods=['get'], ) - def remove(self, request, pk=None): + def remove(self, request, slug=None): item = self.get_object() self.check_object_permissions(request, item) item.delete() @@ -211,7 +213,7 @@ class ProjectViewSet(IdentifiableModelViewSet): methods=['get'], renderer_classes=[CustomTemplateHTMLRenderer], ) - def stats(self, request, pk=None): + def stats(self, request, slug=None): item = self.get_object() self.check_object_permissions(request, item) return response.Response() diff --git a/FAIRshakeHub/templates/fairshake/assessment/add.html b/FAIRshakeHub/templates/fairshake/assessment/add.html index c1f02c0..e141941 100644 --- a/FAIRshakeHub/templates/fairshake/assessment/add.html +++ b/FAIRshakeHub/templates/fairshake/assessment/add.html @@ -2,17 +2,17 @@ {% load filters %} {{ model|unslugify }} - of {{ item.target.title }} - with {{ item.rubric.title }} + of {{ item.target.title }} + with {{ item.rubric.title }} {% if item.project %} - for {{ item.project.title }} + for {{ item.project.title }} {% endif %}   diff --git a/FAIRshakeHub/templates/fairshake/assessment/element.html b/FAIRshakeHub/templates/fairshake/assessment/element.html index 54c63f2..8d2b93b 100644 --- a/FAIRshakeHub/templates/fairshake/assessment/element.html +++ b/FAIRshakeHub/templates/fairshake/assessment/element.html @@ -4,18 +4,18 @@
{{ model|unslugify }} - of {{ item.target.title }} - with {{ item.rubric.title }} + of {{ item.target.title }} + with {{ item.rubric.title }} {% if item.project %} - for {{ item.project.title }} + for {{ item.project.title }} {% endif %}   - + + {{ model|unslugify }} - of {{ item.target.title }} - with {{ item.rubric.title }} + of {{ item.target.title }} + with {{ item.rubric.title }} {% if item.project %} - for {{ item.project.title }} + for {{ item.project.title }} {% endif %} {% csrf_token %} diff --git a/FAIRshakeHub/templates/fairshake/generic/element.html b/FAIRshakeHub/templates/fairshake/generic/element.html index 5ecb540..b25a264 100644 --- a/FAIRshakeHub/templates/fairshake/generic/element.html +++ b/FAIRshakeHub/templates/fairshake/generic/element.html @@ -3,7 +3,7 @@
@@ -61,7 +61,7 @@ {% has_permission item 'modify' as edit_perm %} {% if edit_perm %}
- +
- +
diff --git a/FAIRshakeHub/templates/fairshake/generic/modify.html b/FAIRshakeHub/templates/fairshake/generic/modify.html index abae5be..55a1db9 100644 --- a/FAIRshakeHub/templates/fairshake/generic/modify.html +++ b/FAIRshakeHub/templates/fairshake/generic/modify.html @@ -6,6 +6,6 @@ {{ form|bootstrap }} diff --git a/FAIRshakeHub/templates/fairshake/generic/retrieve.html b/FAIRshakeHub/templates/fairshake/generic/retrieve.html index 9d6538e..4389c93 100644 --- a/FAIRshakeHub/templates/fairshake/generic/retrieve.html +++ b/FAIRshakeHub/templates/fairshake/generic/retrieve.html @@ -57,12 +57,12 @@

{{ item.title }}

{% if model == "project" %} View Analytics @@ -71,7 +71,7 @@

{{ item.title }}

{% if model == "digital_object" and user.is_authenticated %}
@@ -83,7 +83,7 @@

{{ item.title }}

{% if edit_perm %}
{{ item.title }}: Analytics
From 766d729160cd6bc44bedb6f3a08e8673faee768c Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Wed, 12 Sep 2018 18:14:10 -0400 Subject: [PATCH 11/23] Made FAIRsharing automated assessment use DOI References #37 References #28 References #9 - Removed fairsharing field (now determined from doi url) --- .../assessments/fairsharing/__init__.py | 94 +++++++++---------- FAIRshakeAPI/forms.py | 1 - .../0015_remove_digitalobject_fairsharing.py | 17 ++++ FAIRshakeAPI/models.py | 6 -- 4 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 FAIRshakeAPI/migrations/0015_remove_digitalobject_fairsharing.py diff --git a/FAIRshakeAPI/assessments/fairsharing/__init__.py b/FAIRshakeAPI/assessments/fairsharing/__init__.py index 9fc0753..58e9cbf 100644 --- a/FAIRshakeAPI/assessments/fairsharing/__init__.py +++ b/FAIRshakeAPI/assessments/fairsharing/__init__.py @@ -1,61 +1,59 @@ +import re from django.conf import settings from scripts.pyswagger_wrapper import SwaggerClient +url_re = re.compile(r'^https?://doi.org/(.+)$') + +metric_to_attr = { + 'metric:9': 'licence', + 'metric:60': 'homepage', + 'metric:101': 'taxonomies', + 'metric:102': 'domains', +} + class Assessment: inputs = [ - 'target:fairsharing' + 'target:url' ] outputs = [ # 'target:url', # 'target:description', # 'target:title', - # 'target:doi', # 'target:authors' - 'metric:9', # license - 'metric:60', # title - 'metric:101', # taxonomies - 'metric:102', # domains - ] + ] + list(metric_to_attr.keys()) + @classmethod def perform(kls, inputs): - client = SwaggerClient( - 'https://fairsharing.org/api/?format=openapi', - headers={ - 'Api-Key': settings.ASSESSMENT_CONFIG['fairsharing']['api-key'], - } - ) - results = client.actions.database_summary_read.call( - bsg_id=inputs['target:fairsharing'] - ) - return { - # 'target:url': { - # 'answer': results['data'].get('homepage'), - # 'comment': results['data'].get('homepage'), - # }, - # 'target:description': { - # 'answer': results['data'].get('description'), - # 'comment': results['data'].get('description'), - # }, - # 'target:title': { - # 'answer': results['data'].get('name'), - # 'comment': results['data'].get('name'), - # }, - 'metric:9': { - 'answer': 'yes' if results['data'].get('licence') else 'no', - 'comment': results['data'].get('licence'), - }, - 'metric:60': { - 'answer': 'yes' if results['data'].get('homepage') is not None else 'no', - 'comment': results['data'].get('homepage'), - }, - 'metric:101': { - 'answer': 'yes' if results['data'].get('taxonomies') else 'no', - 'comment': results['data'].get('taxonomies'), - }, - 'metric:102': { - 'answer': 'yes' if results['data'].get('domains') else 'no', - 'comment': results['data'].get('domains'), - }, - # 'target:doi': results['data'].get('doi'), - # 'target:authors': results['data'].get('maintainers'), - } + url = inputs['target:url'] + dois = [m.group(1) for m in map(url_re.match, url.splitlines()) if m] + + if dois: + client = SwaggerClient( + 'https://fairsharing.org/api/?format=openapi', + headers={ + 'Api-Key': settings.ASSESSMENT_CONFIG['fairsharing']['api-key'], + } + ) + + results = [ + result + for doi in dois + for result in client.actions.database_summary_list.call( + doi=doi, + )['results'] + ] + + if len(results) > 1: + logging.warn('More than 1 DOI was identified in the fairsharing database! (%s)' % (url)) + if len(results) >= 1: + data = results[0]['data'] + else: + data = None + + return { + key: { + 'answer': 'yes' if data.get(attr) else 'no', + 'comment': data.get(attr), + } + for key, attr in metric_to_attr.items() + } if data else {key: {} for key in metric_to_attr.keys()} diff --git a/FAIRshakeAPI/forms.py b/FAIRshakeAPI/forms.py index cba4dcf..9aef436 100644 --- a/FAIRshakeAPI/forms.py +++ b/FAIRshakeAPI/forms.py @@ -66,7 +66,6 @@ class Meta: 'tags', 'type', 'slug', - 'fairsharing', 'rubrics', ) diff --git a/FAIRshakeAPI/migrations/0015_remove_digitalobject_fairsharing.py b/FAIRshakeAPI/migrations/0015_remove_digitalobject_fairsharing.py new file mode 100644 index 0000000..2d63bd9 --- /dev/null +++ b/FAIRshakeAPI/migrations/0015_remove_digitalobject_fairsharing.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.7 on 2018-09-12 22:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0014_auto_20180912_2054'), + ] + + operations = [ + migrations.RemoveField( + model_name='digitalobject', + name='fairsharing', + ), + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index dccbd19..c15bbac 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -88,15 +88,9 @@ class DigitalObject(IdentifiableModelMixin): url = models.CharField(max_length=255, blank=False) # urls = ArrayField(models.CharField(max_length=255), blank=False) title = models.CharField(max_length=255, blank=True, null=False, default='') - fairsharing = models.CharField(max_length=255, blank=True, null=False, default='') rubrics = VersionedManyToManyField('Rubric', blank=True, related_name='digital_objects') - def attrs(self): - return dict(super().attrs(), **{ - 'fairsharing': self.fairsharing, - }) - class Meta: verbose_name = 'digital_object' verbose_name_plural = 'digital_objects' From dc754888a9e89c47be5b25eb4ee6f9506e8ecb46 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 12:03:59 -0400 Subject: [PATCH 12/23] Removed author name search vector Not that necessary and causes an issue with cleanerversion --- FAIRshakeAPI/search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FAIRshakeAPI/search.py b/FAIRshakeAPI/search.py index 9256355..5383504 100644 --- a/FAIRshakeAPI/search.py +++ b/FAIRshakeAPI/search.py @@ -31,7 +31,6 @@ class IdentifiableSearchVector(SearchVector): lambda q: Q(description__icontains=q), lambda q: Q(tags__icontains=q), lambda q: Q(type__icontains=q), - lambda q: Q(authors__first_name__icontains=q), ] class ProjectSearchVector(IdentifiableSearchVector): From bb8aaa6170527852c776fe54ef834b4daae06ecb Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 12:09:04 -0400 Subject: [PATCH 13/23] Added quotes to data-target ids--now slugs --- FAIRshakeHub/templates/fairshake/generic/element.html | 2 +- FAIRshakeHub/templates/fairshake/generic/retrieve.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FAIRshakeHub/templates/fairshake/generic/element.html b/FAIRshakeHub/templates/fairshake/generic/element.html index b25a264..a1a441a 100644 --- a/FAIRshakeHub/templates/fairshake/generic/element.html +++ b/FAIRshakeHub/templates/fairshake/generic/element.html @@ -27,7 +27,7 @@
{% endif %}
diff --git a/FAIRshakeHub/templates/fairshake/generic/retrieve.html b/FAIRshakeHub/templates/fairshake/generic/retrieve.html index 4389c93..e5357ed 100644 --- a/FAIRshakeHub/templates/fairshake/generic/retrieve.html +++ b/FAIRshakeHub/templates/fairshake/generic/retrieve.html @@ -57,7 +57,7 @@

{{ item.title }}

{% if model == "project" %}
Date: Thu, 13 Sep 2018 13:13:32 -0400 Subject: [PATCH 14/23] Ensure slugs don't collide with object ids as that will be a fallback --- FAIRshakeAPI/forms.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FAIRshakeAPI/forms.py b/FAIRshakeAPI/forms.py index 9aef436..523edb8 100644 --- a/FAIRshakeAPI/forms.py +++ b/FAIRshakeAPI/forms.py @@ -17,7 +17,10 @@ def __init__(self, *args, **kwargs): def clean_slug(self): slug = self.cleaned_data.get('slug') try: - if self.Meta.model.objects.current.get(slug=slug) != self.instance: + if any([ + self.Meta.model.objects.current.get(slug=slug) != self.instance, + self.Meta.model.objects.get(id=slug) != self.instance, + ]): raise forms.ValidationError( 'Slug was already taken, please try something different.' ) From e29bc8beb2aea49c3e4cd8fd4e613cd762f4526a Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:41:05 -0400 Subject: [PATCH 15/23] Added coverage and information on running tests --- .gitignore | 3 ++- README.md | 8 ++++++++ requirements.txt | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f734479..8684ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ __pycache__ +.coverage .DS_Store .idea/ *.pyc db.sqlite3 my.cnf -ssl/ +ssl/ \ No newline at end of file diff --git a/README.md b/README.md index b434238..8b5adf4 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,14 @@ Django keeps track of database migrations. When modifying `models` it is imperat Note that this will try but not always succeed to detect renamed fields and such and migrate the backend database accordingly. If it is unable to, it may require manual intervention. For more information https://docs.djangoproject.com/en/2.0/topics/migrations/. +### Testing +```bash +# Run Tests +./manage.py test +# Run Tests with Coverage +coverage run --source='.' manage.py test && coverage report +``` + ## Database Backup & Restore ### Backup ```bash diff --git a/requirements.txt b/requirements.txt index ecfb963..26ebc61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ coreapi +coverage django-ajax-selects django-analytical django-bootstrap-form From c63b084710c9468555adc8b000e677223a70aac5 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:42:33 -0400 Subject: [PATCH 16/23] Wrote some FAIRshake API unit tests - Test database setup - Test all viewset-list endpoints (html & json) - Test all viewset-retrieve endpoints (html & json) --- FAIRshakeAPI/tests.py | 344 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 341 insertions(+), 3 deletions(-) diff --git a/FAIRshakeAPI/tests.py b/FAIRshakeAPI/tests.py index 9dc891a..59d8fe2 100644 --- a/FAIRshakeAPI/tests.py +++ b/FAIRshakeAPI/tests.py @@ -1,4 +1,342 @@ -from django.test import TestCase +from django.test import TestCase, Client +from django.urls import reverse +from . import models -# TODO: validate against swagger spec -# TODO: test assessments process +class ViewsFunctionTestCase(TestCase): + def setUp(self): + user = models.Author.objects.create( + username='test', + password='test', + ) + metrics = [ + models.Metric.objects.create( + title='yesnobut test', + type='yesnobut', + slug='yesnobut', + ), + models.Metric.objects.create( + title='text test', + type='text', + slug='text', + ), + models.Metric.objects.create( + title='url test', + type='url', + slug='url', + ), + ] + for metric in metrics: + metric.authors.add(user) + rubric = models.Rubric.objects.create( + title='rubric test', + slug='test', + ) + rubric.authors.add(user) + for metric in metrics: + rubric.metrics.add(metric) + obj = models.DigitalObject.objects.create( + url='https://fairshake.cloud/', + slug='fairshake', + ) + obj.rubrics.add(rubric) + project = models.Project.objects.create( + title='project test', + slug='test', + ) + project.authors.add(user) + project.digital_objects.add(obj) + assessment = models.Assessment.objects.create( + project=project, + target=obj, + rubric=rubric, + assessor=user, + ) + for metric in metrics: + models.Answer.objects.create( + assessment=assessment, + metric=metric, + answer='yes', + ) + + self.anonymous_client = Client() + self.authenticated_client = Client() + self.authenticated_client.force_login(user) + + def test_project_viewset_list(self): + response = self.anonymous_client.get(reverse('project-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get(reverse('project-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get(reverse('project-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('project-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_digital_object_viewset_list(self): + response = self.anonymous_client.get(reverse('digital_object-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get(reverse('digital_object-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get(reverse('digital_object-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('digital_object-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_rubric_viewset_list(self): + response = self.anonymous_client.get(reverse('rubric-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get(reverse('rubric-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get(reverse('rubric-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('rubric-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_metric_viewset_list(self): + response = self.anonymous_client.get(reverse('metric-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get(reverse('metric-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get(reverse('metric-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('metric-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_assessment_viewset_list(self): + response = self.anonymous_client.get(reverse('assessment-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 401) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get(reverse('assessment-list'), HTTP_ACCEPT='text/html') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get(reverse('assessment-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 401) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('assessment-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_score_viewset_list(self): + response = self.anonymous_client.get(reverse('score-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get(reverse('score-list'), HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_project_viewset_detail(self): + response = self.anonymous_client.get( + reverse('project-detail', kwargs=dict( + slug=models.Project.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get( + reverse('project-detail', kwargs=dict( + slug=models.Project.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get( + reverse('project-detail', kwargs=dict( + slug=models.Project.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get( + reverse('project-detail', kwargs=dict( + slug=models.Project.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_digital_object_viewset_detail(self): + response = self.anonymous_client.get( + reverse('digital_object-detail', kwargs=dict( + slug=models.DigitalObject.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get( + reverse('digital_object-detail', kwargs=dict( + slug=models.DigitalObject.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get( + reverse('digital_object-detail', kwargs=dict( + slug=models.DigitalObject.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get( + reverse('digital_object-detail', kwargs=dict( + slug=models.DigitalObject.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_rubric_viewset_detail(self): + response = self.anonymous_client.get( + reverse('rubric-detail', kwargs=dict( + slug=models.Rubric.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get( + reverse('rubric-detail', kwargs=dict( + slug=models.Rubric.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get( + reverse('rubric-detail', kwargs=dict( + slug=models.Rubric.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get( + reverse('rubric-detail', kwargs=dict( + slug=models.Rubric.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_metric_viewset_detail(self): + response = self.anonymous_client.get( + reverse('metric-detail', kwargs=dict( + slug=models.Metric.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get( + reverse('metric-detail', kwargs=dict( + slug=models.Metric.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get( + reverse('metric-detail', kwargs=dict( + slug=models.Metric.objects.current.first().slug + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get( + reverse('metric-detail', kwargs=dict( + slug=models.Metric.objects.current.first().slug + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) + + def test_assessment_viewset_detail(self): + response = self.anonymous_client.get( + reverse('assessment-detail', kwargs=dict( + pk=models.Assessment.objects.current.first().pk + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.anonymous_client.get( + reverse('assessment-detail', kwargs=dict( + pk=models.Assessment.objects.current.first().pk + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 401) + self.assertEqual(response['Content-Type'], 'application/json', response) + + response = self.authenticated_client.get( + reverse('assessment-detail', kwargs=dict( + pk=models.Assessment.objects.current.first().pk + )), + HTTP_ACCEPT='text/html', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8', response) + + response = self.authenticated_client.get( + reverse('assessment-detail', kwargs=dict( + pk=models.Assessment.objects.current.first().pk + )), + HTTP_ACCEPT='application/json', + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json', response) From ff012281c703a502e25b9cf99b3288d7a0729b6f Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:43:18 -0400 Subject: [PATCH 17/23] Wrote some FAIRshakeHub unit tests - Use FAIRshakeAPI's test database - Test all standard views and search - Test project stas view with all plots --- FAIRshakeHub/tests.py | 71 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/FAIRshakeHub/tests.py b/FAIRshakeHub/tests.py index 7ce503c..0b9c0bb 100644 --- a/FAIRshakeHub/tests.py +++ b/FAIRshakeHub/tests.py @@ -1,3 +1,70 @@ -from django.test import TestCase +from django.test import TestCase, Client +from django.urls import reverse +from FAIRshakeAPI import tests, models -# Create your tests here. +class ViewsFunctionTestCase(TestCase): + setUp = tests.ViewsFunctionTestCase.setUp + + def test_index_view(self): + response = self.anonymous_client.get(reverse('index')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('index')) + self.assertEqual(response.status_code, 200) + + def test_search_view(self): + response = self.anonymous_client.get(reverse('index'), dict(q='test')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('index'), dict(q='test')) + self.assertEqual(response.status_code, 200) + + def test_bookmarklet_view(self): + response = self.anonymous_client.get(reverse('bookmarklet')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('bookmarklet')) + self.assertEqual(response.status_code, 200) + + def test_chrome_extension_view(self): + response = self.anonymous_client.get(reverse('chrome_extension')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('chrome_extension')) + self.assertEqual(response.status_code, 200) + + def test_api_documentation_view(self): + response = self.anonymous_client.get(reverse('api_documentation')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('api_documentation')) + self.assertEqual(response.status_code, 200) + + def test_terms_of_service_view(self): + response = self.anonymous_client.get(reverse('terms_of_service')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('terms_of_service')) + self.assertEqual(response.status_code, 200) + + def test_contributors_and_partners_view(self): + response = self.anonymous_client.get(reverse('contributors_and_partners')) + self.assertEqual(response.status_code, 200) + + response = self.authenticated_client.get(reverse('contributors_and_partners')) + self.assertEqual(response.status_code, 200) + + def test_project_stats_view(self): + item = models.Project.objects.current.first() + for plot in [ + 'TablePlot', + 'RubricPieChart', + 'RubricsInProjectsOverlay', + 'DigitalObjectBarBreakdown', + ]: + response = self.anonymous_client.get(reverse('stats'), { + 'model': 'project', + 'item': item.id, + 'plot': plot, + }) + self.assertEqual(response.status_code, 200) From affc675e4b8239d2eeb0b5a99e89d35139e4c05b Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:44:04 -0400 Subject: [PATCH 18/23] Permissions fix for Assessment list/retrieve --- FAIRshakeAPI/models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index c15bbac..103bde1 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -159,13 +159,9 @@ class Assessment(Versionable): assessor = models.ForeignKey('Author', on_delete=models.SET_NULL, editable=False, blank=True, null=True, related_name='+') def has_permission(self, user, perm): - if perm in ['list', 'retrieve']: - return True - elif perm in ['create', 'add']: - return user.is_authenticated or user.is_staff - elif perm in ['modify', 'remove', 'delete']: + if perm in ['list', 'create', 'add', 'modify', 'remove', 'delete', 'retrieve']: if self is None: - return user.is_authenticated + return user.is_authenticated or user.is_staff else: return (self and self.assessor == user) or user.is_staff else: From cad977fc10e3ef2eab7b509e1333821742270bf8 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:44:40 -0400 Subject: [PATCH 19/23] CustomUUIDField handle None value --- extensions/versions_ex/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/versions_ex/fields.py b/extensions/versions_ex/fields.py index f338a1c..45e1656 100644 --- a/extensions/versions_ex/fields.py +++ b/extensions/versions_ex/fields.py @@ -4,7 +4,11 @@ class CustomUUIDField(UUIDField): def to_python(self, value): + if value is None: + return None return uuid.UUID(bytes=base64.urlsafe_b64decode((value + '==').replace('_', '/'))) def from_db_value(self, value, expr, connection): + if value is None: + return None return base64.urlsafe_b64encode(value.bytes).decode("utf-8").rstrip('=\n').replace('/', '_') From 7ae2bb0a51fb64f32e6a1a9058ffc1644fc7a533 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 15:45:26 -0400 Subject: [PATCH 20/23] Fixed VersionQueryEx edge case (instance is str) --- extensions/versions_ex/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/extensions/versions_ex/models.py b/extensions/versions_ex/models.py index 3951840..2279673 100644 --- a/extensions/versions_ex/models.py +++ b/extensions/versions_ex/models.py @@ -13,8 +13,9 @@ def _set_item_querytime(self, item, type_check=False): """ if isinstance(item, VersionedQuerySet) or isinstance(item, VersionedQuerySetEx): item.querytime = self.querytime - else: #if isinstance(item, Versionable) or isinstance(item, VersionableEx): + elif not isinstance(item, str): #if isinstance(item, Versionable) or isinstance(item, VersionableEx): # Note: This will fall back to a `fake` model during django migrations, hence the else + # Note: Item can be a string as testing has shown item._querytime = self.querytime return item @@ -27,10 +28,10 @@ def get_queryset(self): qs.querytime = self.instance._querytime return qs - def create(*args, **kwargs): - res = VersionManager.create(*args, **kwargs) - res.is_current = VersionableEx.is_current - res.querytime = VersionableEx.is_current + def create(self, *args, **kwargs): + res = VersionManager.create(self, *args, **kwargs) + if getattr(res, 'is_current', None) is None: + setattr(res, 'is_current', Versionable.is_current) return res class VersionableEx(Versionable): From 96fd16c0b73dda1273036614ca894932c5bad722 Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 17:08:39 -0400 Subject: [PATCH 21/23] Moved CustomRouter to extensions.rest_framework_ex --- FAIRshake/settings.py | 1 + FAIRshakeAPI/urls.py | 4 ++-- extensions/rest_framework_ex/__init__.py | 0 {FAIRshakeAPI => extensions/rest_framework_ex}/routers.py | 0 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 extensions/rest_framework_ex/__init__.py rename {FAIRshakeAPI => extensions/rest_framework_ex}/routers.py (100%) diff --git a/FAIRshake/settings.py b/FAIRshake/settings.py index 9fa0d50..847f7e6 100644 --- a/FAIRshake/settings.py +++ b/FAIRshake/settings.py @@ -75,6 +75,7 @@ 'extensions.drf_yasg_ex', 'extensions.rest_auth_ex', 'extensions.versions_ex', + 'extensions.rest_framework_ex', 'FAIRshakeHub', 'FAIRshakeAPI', ] diff --git a/FAIRshakeAPI/urls.py b/FAIRshakeAPI/urls.py index dc8ea7c..9dde875 100644 --- a/FAIRshakeAPI/urls.py +++ b/FAIRshakeAPI/urls.py @@ -1,7 +1,7 @@ from django.urls import path, re_path, include -from rest_framework import routers +from extensions.rest_framework_ex import routers from rest_framework.documentation import include_docs_urls -from . import views, routers +from . import views router = routers.CustomRouter() router.register(r'assessment_request', views.AssessmentRequestViewSet, base_name='assessment_request') diff --git a/extensions/rest_framework_ex/__init__.py b/extensions/rest_framework_ex/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/FAIRshakeAPI/routers.py b/extensions/rest_framework_ex/routers.py similarity index 100% rename from FAIRshakeAPI/routers.py rename to extensions/rest_framework_ex/routers.py From 6e0d1d57ce7af06b9d0e3b9faa084d391bbe723b Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Thu, 13 Sep 2018 17:09:00 -0400 Subject: [PATCH 22/23] Fixed assessment object query model names --- FAIRshakeAPI/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FAIRshakeAPI/views.py b/FAIRshakeAPI/views.py index 0974bb8..5bf19ca 100644 --- a/FAIRshakeAPI/views.py +++ b/FAIRshakeAPI/views.py @@ -261,16 +261,16 @@ def get_objects(self, request): if project_id: assessment = get_or_create(models.Assessment, - project=models.project.objects.get(id=project_id), - target=models.target.objects.get(id=target_id), - rubric=models.rubric.objects.get(id=rubric_id), + project=models.Project.objects.get(id=project_id), + target=models.DigitalObject.objects.get(id=target_id), + rubric=models.Rubric.objects.get(id=rubric_id), assessor=request.user, methodology='user', ) else: assessment = get_or_create(models.Assessment, - target=models.target.objects.get(id=target_id), - rubric=models.rubric.objects.get(id=rubric_id), + target=models.DigitalObject.objects.get(id=target_id), + rubric=models.Rubric.objects.get(id=rubric_id), assessor=request.user, methodology='user', ) From 8983efcd1db75c0590a3075ac381cafeb2c2467b Mon Sep 17 00:00:00 2001 From: "Daniel J. B. Clarke" Date: Mon, 17 Sep 2018 15:36:40 -0400 Subject: [PATCH 23/23] Added unique_together constraints on assessment and answers --- .../migrations/0017_auto_20180917_1936.py | 21 +++++++++++++++++++ FAIRshakeAPI/models.py | 11 ++++++++++ 2 files changed, 32 insertions(+) create mode 100644 FAIRshakeAPI/migrations/0017_auto_20180917_1936.py diff --git a/FAIRshakeAPI/migrations/0017_auto_20180917_1936.py b/FAIRshakeAPI/migrations/0017_auto_20180917_1936.py new file mode 100644 index 0000000..2d58171 --- /dev/null +++ b/FAIRshakeAPI/migrations/0017_auto_20180917_1936.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.7 on 2018-09-17 19:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAIRshakeAPI', '0016_auto_20180917_1713'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='answer', + unique_together={('assessment', 'metric')}, + ), + migrations.AlterUniqueTogether( + name='assessment', + unique_together={('project', 'target', 'rubric', 'methodology', 'assessor')}, + ), + ] diff --git a/FAIRshakeAPI/models.py b/FAIRshakeAPI/models.py index 03b304a..74bb726 100644 --- a/FAIRshakeAPI/models.py +++ b/FAIRshakeAPI/models.py @@ -182,6 +182,13 @@ class Meta: verbose_name = 'assessment' verbose_name_plural = 'assessments' ordering = ['id'] + unique_together = ( + 'project', + 'target', + 'rubric', + 'methodology', + 'assessor', + ) class MetaEx: children = [ @@ -230,6 +237,10 @@ class Meta: verbose_name = 'answer' verbose_name_plural = 'answers' ordering = ['id'] + unique_together = ( + 'assessment', + 'metric', + ) class AssessmentRequest(models.Model): id = CustomUUIDField(primary_key=True, default=uuid.uuid4)