From e6d28ce36c0d0db2930494ca2a1cf7103f8f1b41 Mon Sep 17 00:00:00 2001 From: Kalev Takkis Date: Mon, 2 Dec 2024 13:41:30 +0000 Subject: [PATCH 1/4] fix: changing the foreign key of the ComputedSet Instead of the former natural key (name), there's now numeric id. Migrations done. TODO: go through the code and fix errors. --- viewer/migrations/0085_auto_20241202_1157.py | 32 ++++++++++++++++++ viewer/migrations/0086_auto_20241202_1205.py | 23 +++++++++++++ viewer/migrations/0087_auto_20241202_1206.py | 24 ++++++++++++++ viewer/migrations/0088_auto_20241202_1224.py | 33 +++++++++++++++++++ viewer/migrations/0089_auto_20241202_1230.py | 23 +++++++++++++ viewer/migrations/0090_auto_20241202_1231.py | 23 +++++++++++++ viewer/migrations/0091_auto_20241202_1234.py | 32 ++++++++++++++++++ viewer/migrations/0092_auto_20241202_1322.py | 29 ++++++++++++++++ viewer/migrations/0093_auto_20241202_1324.py | 28 ++++++++++++++++ viewer/migrations/0094_auto_20241202_1333.py | 31 +++++++++++++++++ viewer/migrations/0095_auto_20241202_1336.py | 22 +++++++++++++ ...mputedset_unique_computedsetname_target.py | 17 ++++++++++ viewer/models.py | 13 +++++++- 13 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 viewer/migrations/0085_auto_20241202_1157.py create mode 100644 viewer/migrations/0086_auto_20241202_1205.py create mode 100644 viewer/migrations/0087_auto_20241202_1206.py create mode 100644 viewer/migrations/0088_auto_20241202_1224.py create mode 100644 viewer/migrations/0089_auto_20241202_1230.py create mode 100644 viewer/migrations/0090_auto_20241202_1231.py create mode 100644 viewer/migrations/0091_auto_20241202_1234.py create mode 100644 viewer/migrations/0092_auto_20241202_1322.py create mode 100644 viewer/migrations/0093_auto_20241202_1324.py create mode 100644 viewer/migrations/0094_auto_20241202_1333.py create mode 100644 viewer/migrations/0095_auto_20241202_1336.py create mode 100644 viewer/migrations/0096_computedset_unique_computedsetname_target.py diff --git a/viewer/migrations/0085_auto_20241202_1157.py b/viewer/migrations/0085_auto_20241202_1157.py new file mode 100644 index 00000000..1f4b1535 --- /dev/null +++ b/viewer/migrations/0085_auto_20241202_1157.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.25 on 2024-12-02 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0084_auto_20241108_1119'), + ] + + operations = [ + migrations.RemoveField( + model_name='computedset', + name='computed_molecules', + ), + migrations.AlterField( + model_name='computedsetcomputedmolecule', + name='computed_set', + field=models.TextField(null=True), + ), + migrations.AlterField( + model_name='jobrequest', + name='computed_set', + field=models.TextField(null=True), + ), + migrations.AlterField( + model_name='scoredescription', + name='computed_set', + field=models.TextField(null=True), + ), + ] diff --git a/viewer/migrations/0086_auto_20241202_1205.py b/viewer/migrations/0086_auto_20241202_1205.py new file mode 100644 index 00000000..d9078ae8 --- /dev/null +++ b/viewer/migrations/0086_auto_20241202_1205.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0085_auto_20241202_1157'), + ] + + operations = [ + migrations.AddField( + model_name='computedset', + name='new_id', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='historicalcomputedset', + name='new_id', + field=models.IntegerField(default=0), + ), + ] diff --git a/viewer/migrations/0087_auto_20241202_1206.py b/viewer/migrations/0087_auto_20241202_1206.py new file mode 100644 index 00000000..40d41908 --- /dev/null +++ b/viewer/migrations/0087_auto_20241202_1206.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0086_auto_20241202_1205'), + ] + + def populate_ids(apps, schema_editor): + ComputedSet = apps.get_model('viewer', 'ComputedSet') + + for i, obj in enumerate(ComputedSet.objects.all()): + obj.new_id = i + 1 + obj.save() + + def reverse_populate_ids(apps, schema_editor): + pass + + operations = [ + migrations.RunPython(populate_ids, reverse_populate_ids), + ] diff --git a/viewer/migrations/0088_auto_20241202_1224.py b/viewer/migrations/0088_auto_20241202_1224.py new file mode 100644 index 00000000..f863b8f2 --- /dev/null +++ b/viewer/migrations/0088_auto_20241202_1224.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0087_auto_20241202_1206'), + ] + + operations = [ + migrations.AlterField( + model_name='computedset', + name='name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='computedset', + name='new_id', + field=models.IntegerField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='historicalcomputedset', + name='name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='historicalcomputedset', + name='new_id', + field=models.IntegerField(db_index=True), + ), + ] diff --git a/viewer/migrations/0089_auto_20241202_1230.py b/viewer/migrations/0089_auto_20241202_1230.py new file mode 100644 index 00000000..10a8eeeb --- /dev/null +++ b/viewer/migrations/0089_auto_20241202_1230.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0088_auto_20241202_1224'), + ] + + operations = [ + migrations.RenameField( + model_name='computedset', + old_name='new_id', + new_name='id', + ), + migrations.RenameField( + model_name='historicalcomputedset', + old_name='new_id', + new_name='id', + ), + ] diff --git a/viewer/migrations/0090_auto_20241202_1231.py b/viewer/migrations/0090_auto_20241202_1231.py new file mode 100644 index 00000000..0630ff5d --- /dev/null +++ b/viewer/migrations/0090_auto_20241202_1231.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0089_auto_20241202_1230'), + ] + + operations = [ + migrations.AlterField( + model_name='computedset', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='historicalcomputedset', + name='id', + field=models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID'), + ), + ] diff --git a/viewer/migrations/0091_auto_20241202_1234.py b/viewer/migrations/0091_auto_20241202_1234.py new file mode 100644 index 00000000..b08b6fe5 --- /dev/null +++ b/viewer/migrations/0091_auto_20241202_1234.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.25 on 2024-12-02 12:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0090_auto_20241202_1231'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='computedsetcomputedmolecule', + name='unique_computedsetcomputedmolecule', + ), + migrations.RenameField( + model_name='computedsetcomputedmolecule', + old_name='computed_set', + new_name='computed_set_txt', + ), + migrations.RenameField( + model_name='jobrequest', + old_name='computed_set', + new_name='computed_set_txt', + ), + migrations.RenameField( + model_name='scoredescription', + old_name='computed_set', + new_name='computed_set_txt', + ), + ] diff --git a/viewer/migrations/0092_auto_20241202_1322.py b/viewer/migrations/0092_auto_20241202_1322.py new file mode 100644 index 00000000..b667556e --- /dev/null +++ b/viewer/migrations/0092_auto_20241202_1322.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.25 on 2024-12-02 13:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0091_auto_20241202_1234'), + ] + + operations = [ + migrations.AddField( + model_name='computedsetcomputedmolecule', + name='computed_set', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='viewer.computedset'), + ), + migrations.AddField( + model_name='jobrequest', + name='computed_set', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='viewer.computedset'), + ), + migrations.AddField( + model_name='scoredescription', + name='computed_set', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='viewer.computedset'), + ), + ] diff --git a/viewer/migrations/0093_auto_20241202_1324.py b/viewer/migrations/0093_auto_20241202_1324.py new file mode 100644 index 00000000..ad800a98 --- /dev/null +++ b/viewer/migrations/0093_auto_20241202_1324.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.25 on 2024-12-02 13:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0092_auto_20241202_1322'), + ] + + def populate_fks(apps, schema_editor): + ComputedSet = apps.get_model('viewer', 'ComputedSet') + ComputedSetComputedMolecule = apps.get_model('viewer', 'ComputedSetComputedMolecule') + ScoreDescription = apps.get_model('viewer', 'ScoreDescription') + JobRequest = apps.get_model('viewer', 'JobRequest') + + for model in (ComputedSetComputedMolecule, ScoreDescription, JobRequest): + for obj in model.objects.all(): + obj.computed_set = ComputedSet.objects.get(name=obj.computed_set_txt) + obj.save() + + def reverse_populate_fks(apps, schema_editor): + pass + + operations = [ + migrations.RunPython(populate_fks, reverse_populate_fks), + ] diff --git a/viewer/migrations/0094_auto_20241202_1333.py b/viewer/migrations/0094_auto_20241202_1333.py new file mode 100644 index 00000000..d89a5ee0 --- /dev/null +++ b/viewer/migrations/0094_auto_20241202_1333.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.25 on 2024-12-02 13:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0093_auto_20241202_1324'), + ] + + operations = [ + migrations.RemoveField( + model_name='computedsetcomputedmolecule', + name='computed_set_txt', + ), + migrations.RemoveField( + model_name='jobrequest', + name='computed_set_txt', + ), + migrations.RemoveField( + model_name='scoredescription', + name='computed_set_txt', + ), + migrations.AlterField( + model_name='computedsetcomputedmolecule', + name='computed_set', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='viewer.computedset'), + ), + ] diff --git a/viewer/migrations/0095_auto_20241202_1336.py b/viewer/migrations/0095_auto_20241202_1336.py new file mode 100644 index 00000000..e7c82384 --- /dev/null +++ b/viewer/migrations/0095_auto_20241202_1336.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.25 on 2024-12-02 13:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0094_auto_20241202_1333'), + ] + + operations = [ + migrations.AddField( + model_name='computedset', + name='computed_molecules', + field=models.ManyToManyField(related_name='computed_set', through='viewer.ComputedSetComputedMolecule', to='viewer.ComputedMolecule'), + ), + migrations.AddConstraint( + model_name='computedsetcomputedmolecule', + constraint=models.UniqueConstraint(fields=('computed_set', 'computed_molecule'), name='unique_computedsetcomputedmolecule'), + ), + ] diff --git a/viewer/migrations/0096_computedset_unique_computedsetname_target.py b/viewer/migrations/0096_computedset_unique_computedsetname_target.py new file mode 100644 index 00000000..242064f2 --- /dev/null +++ b/viewer/migrations/0096_computedset_unique_computedsetname_target.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.25 on 2024-12-02 13:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0095_auto_20241202_1336'), + ] + + operations = [ + migrations.AddConstraint( + model_name='computedset', + constraint=models.UniqueConstraint(fields=('name', 'target'), name='unique_computedsetname_target'), + ), + ] diff --git a/viewer/models.py b/viewer/models.py index ac01da39..113885eb 100644 --- a/viewer/models.py +++ b/viewer/models.py @@ -944,7 +944,7 @@ class ComputedSet(models.Model): LENGTH_METHOD_IN_NAME: int = 20 - name = models.CharField(max_length=50, unique=True, primary_key=True) + name = models.TextField(null=False) target = models.ForeignKey(Target, null=True, on_delete=models.CASCADE) submitted_sdf = models.FileField( upload_to='computed_set_data/', @@ -1016,6 +1016,17 @@ class ComputedSet(models.Model): objects = models.Manager() history = HistoricalRecords() + class Meta: + constraints = [ + models.UniqueConstraint( + fields=[ + "name", + "target", + ], + name="unique_computedsetname_target", + ), + ] + def __str__(self) -> str: target_title: str = self.target.title if self.target else "None" return f"{self.name} {target_title}" From 7ae8bfcecb59950c42f9100635ce1ab646253e09 Mon Sep 17 00:00:00 2001 From: Kalev Takkis Date: Wed, 4 Dec 2024 13:44:32 +0000 Subject: [PATCH 2/4] fix: CompoundSet primary key changed from name to numeric id Removed api/computedset-download endpoint and moved the download to api/compound-sets --- api/urls.py | 10 +- viewer/cset_upload.py | 43 ++++--- viewer/filters.py | 13 ++ viewer/managers.py | 21 ++++ viewer/models.py | 4 +- viewer/serializers.py | 15 ++- viewer/tasks.py | 18 ++- viewer/templates/viewer/upload-cset.html | 2 +- viewer/views.py | 153 ++++++++++------------- 9 files changed, 161 insertions(+), 118 deletions(-) diff --git a/api/urls.py b/api/urls.py index d894d0d6..e270ec1c 100644 --- a/api/urls.py +++ b/api/urls.py @@ -124,11 +124,11 @@ router.register( "metadata_upload", viewer_views.UploadMetadataView, basename='metadata_upload' ) -router.register( - "computedset_download", - viewer_views.DownloadComputedSetView, - basename='computedset_download', -) +# router.register( +# "computedset_download", +# viewer_views.DownloadComputedSetView, +# basename='computedset_download', +# ) # Squonk Jobs router.register( diff --git a/viewer/cset_upload.py b/viewer/cset_upload.py index 87b124ff..3bd4e40b 100644 --- a/viewer/cset_upload.py +++ b/viewer/cset_upload.py @@ -162,7 +162,7 @@ def __init__( version, zfile, zfile_hashvals, - computed_set_name, + computed_set_id, ): self.user_id = user_id self.sdf_filename = sdf_filename @@ -172,7 +172,7 @@ def __init__( self.version = version self.zfile = zfile self.zfile_hashvals = zfile_hashvals - self.computed_set_name = computed_set_name + self.computed_set_id = computed_set_id # using the same mechanism to pass messages as validation self.messages: dict[str, Any] = { @@ -708,12 +708,13 @@ def task(self) -> tuple[ComputedSet, dict]: # Do we have any existing ComputedSets? try: - computed_set = ComputedSet.objects.get(name=self.computed_set_name) + computed_set = ComputedSet.objects.get(pk=self.computed_set_id) # refresh some attributes computed_set.md_ordinal = F('md_ordinal') + 1 computed_set.upload_date = datetime.date.today() computed_set.save() - except ComputedSet.DoesNotExist: + except (ValueError, ComputedSet.DoesNotExist): + # ValueError when pk is None # no, create new today: datetime.date = datetime.date.today() @@ -732,16 +733,30 @@ def task(self) -> tuple[ComputedSet, dict]: f'{truncated_submitter_method}-{str(today)}-' + f'{get_column_letter(new_ordinal)}' ) - logger.info('Creating new ComputedSet "%s"', cs_name) - - computed_set = ComputedSet( - name=cs_name, - md_ordinal=new_ordinal, - upload_date=today, - method=self.submitter_method[: ComputedSet.LENGTH_METHOD], - target=target, - spec_version=float(self.version.strip('ver_')), - ) + + # now that I have a name, I can check whether this + # target already has this set + try: + # this feels wrong, I think it's better if the + # object is resolved in the view.. or maybe in + # validate task.. + computed_set = ComputedSet.objects.get( + name=cs_name, + target=target, + ) + except ComputedSet.DoesNotExist: + # and only now create new set + logger.info('Creating new ComputedSet "%s"', cs_name) + + computed_set = ComputedSet( + name=cs_name, + md_ordinal=new_ordinal, + upload_date=today, + method=self.submitter_method[: ComputedSet.LENGTH_METHOD], + target=target, + spec_version=float(self.version.strip('ver_')), + ) + if self.user_id: try: computed_set.owner_user = User.objects.get(id=self.user_id) diff --git a/viewer/filters.py b/viewer/filters.py index 4c24ba70..99ab41a8 100644 --- a/viewer/filters.py +++ b/viewer/filters.py @@ -7,6 +7,7 @@ CanonSite, CanonSiteConf, Compound, + ComputedSet, Experiment, Pose, QuatAssembly, @@ -125,3 +126,15 @@ class AssemblyFilter(TargetFilterMixin): class Meta: model = QuatAssembly fields = ("target",) + + +class ComputedSetFilter(filters.FilterSet): + project = django_filters.CharFilter( + field_name="project", + lookup_expr="icontains", + label="Project", + ) + + class Meta: + model = ComputedSet + fields = ("name", "target", "project") diff --git a/viewer/managers.py b/viewer/managers.py index 29985fe3..6629f7c5 100644 --- a/viewer/managers.py +++ b/viewer/managers.py @@ -413,3 +413,24 @@ def filter_qs(self): def by_target(self, target): return self.get_queryset().filter_qs().filter(target=target.id) + + +class ComputedSetQueryset(QuerySet): + def filter_qs(self): + ComputedSet = apps.get_model("viewer", "ComputedSet") + qs = ComputedSet.objects.annotate( + project=F("target__project__title"), + ) + + return qs + + +class ComputedSetDataManager(Manager): + def get_queryset(self): + return ComputedSetQueryset(self.model, using=self._db) + + def filter_qs(self): + return self.get_queryset().filter_qs() + + def by_target(self, target): + return self.get_queryset().filter_qs().filter(target=target.id) diff --git a/viewer/models.py b/viewer/models.py index 113885eb..0c0accf4 100644 --- a/viewer/models.py +++ b/viewer/models.py @@ -18,6 +18,7 @@ CanonSiteDataManager, CompoundDataManager, CompoundIdentifierDataManager, + ComputedSetDataManager, ExperimentDataManager, PoseDataManager, QuatAssemblyDataManager, @@ -1014,6 +1015,7 @@ class ComputedSet(models.Model): ) objects = models.Manager() + filter_manager = ComputedSetDataManager() history = HistoricalRecords() class Meta: @@ -1032,7 +1034,7 @@ def __str__(self) -> str: return f"{self.name} {target_title}" def __repr__(self) -> str: - return "" % (self.name, self.target) + return "" % (self.id, self.name, self.target) class ComputedMolecule(models.Model): diff --git a/viewer/serializers.py b/viewer/serializers.py index e002840c..2bf30239 100644 --- a/viewer/serializers.py +++ b/viewer/serializers.py @@ -657,10 +657,21 @@ class Meta: class ComputedSetDownloadSerializer(serializers.ModelSerializer): - # validation is not called, so no reason to use it + project = serializers.CharField(source='target.project.title', read_only=True) + + class Meta: + model = models.ComputedSet + fields = ('id', 'name', 'target', 'project') + + +class ComputedSetCreateSerializer(serializers.ModelSerializer): + # id = serializers.IntegerField(read_only=True) class Meta: model = models.ComputedSet - fields = ('name',) + fields = ('id',) + extra_kwargs = { + "id": {"read_only": True}, + } class ComputedMoleculeSerializer(serializers.ModelSerializer): diff --git a/viewer/tasks.py b/viewer/tasks.py index 049358cb..a8c61550 100644 --- a/viewer/tasks.py +++ b/viewer/tasks.py @@ -85,7 +85,7 @@ def process_compound_set(validate_output): logger.warning('process_compound_set() EXIT params=%s (not validated)', params) return process_stage, validate_dict, validated - computed_set_name = params.get('update', None) + computed_set_id = params.get('update', None) submitter_name, submitter_method, blank_version = blank_mol_vals(params['sdf']) zfile, zfile_hashvals = PdbOps().run(params) @@ -100,14 +100,12 @@ def process_compound_set(validate_output): version=blank_version, zfile=zfile, zfile_hashvals=zfile_hashvals, - computed_set_name=computed_set_name, + computed_set_id=computed_set_id, ) compound_set, process_messages = save_mols.task() - logger.info( - 'process_compound_set() EXIT (CompoundSet.name="%s")', compound_set.name - ) - return 'process', compound_set.name, process_messages + logger.info('process_compound_set() EXIT (CompoundSet.id="%s")', compound_set.id) + return 'process', compound_set.id, process_messages @shared_task @@ -645,14 +643,12 @@ def erase_compound_set_job_material(task_params, job_request_id=0): # Task linking is a bit of a mess atm, # if something went wrong we'll get a tuple, not a dictionary. if isinstance(task_params, list) and task_params[0] == 'process': - cs_name: str = task_params[2] - logger.info( - 'Upload successful (%d) ComputedSet.name="%s"', job_request_id, cs_name - ) + cs_id: str = task_params[2] + logger.info('Upload successful (%d) ComputedSet.id="%s"', job_request_id, cs_id) job_request.upload_status = 'SUCCESS' # We're given a compound set name. # Get its record and put that into the JobRequest... - cs = ComputedSet.objects.get(name=cs_name) + cs = ComputedSet.objects.get(pk=cs_id) assert cs job_request.computed_set = cs else: diff --git a/viewer/templates/viewer/upload-cset.html b/viewer/templates/viewer/upload-cset.html index 9f51de20..c567b8a7 100644 --- a/viewer/templates/viewer/upload-cset.html +++ b/viewer/templates/viewer/upload-cset.html @@ -38,7 +38,7 @@

Compound Set Upload

diff --git a/viewer/views.py b/viewer/views.py index 54fd883e..3a4ae3f2 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -21,6 +21,7 @@ from django.urls import reverse from django.views import View from rest_framework import generics, mixins, permissions, status, viewsets +from rest_framework.decorators import action from rest_framework.parsers import BaseParser from rest_framework.response import Response from rest_framework.views import APIView @@ -419,10 +420,9 @@ def post(self, request): # and the user has to be the owner. selected_set: Optional[models.ComputedSet] = None if update_set and update_set != 'None': - computed_set_query = models.ComputedSet.objects.filter(name=update_set) - if computed_set_query: - selected_set = computed_set_query[0] - else: + try: + selected_set = models.ComputedSet.objects.get(pk=update_set) + except models.ComputedSet.DoesNotExist: request.session[_SESSION_ERROR] = 'The set could not be found' logger.warning( '- UploadComputedSetView POST error_msg="%s"', @@ -847,8 +847,8 @@ def get(self, request, upload_task_id): # Upload/Update output tasks send back a tuple response_data['results'] = {} response_data['validated'] = 'Validated' - cset_name = results[1] - cset = models.ComputedSet.objects.get(name=cset_name) + cset_id = int(results[1]) + cset = models.ComputedSet.objects.get(pk=cset_id) if ( settings.AUTHENTICATE_UPLOAD @@ -866,13 +866,11 @@ def get(self, request, upload_task_id): }, ) - name = cset.name - response_data['results']['cset_download_url'] = ( - '/viewer/compound_set/%s' % name + '/viewer/compound_set/%s' % cset.id ) response_data['results']['pset_download_url'] = ( - '/viewer/protein_set/%s' % name + '/viewer/protein_set/%s' % cset.id ) process_messages = results[2] @@ -903,13 +901,12 @@ def get(self, request, upload_task_id): status=status.HTTP_403_FORBIDDEN, ) - name = cset.name response_data['results'] = {} response_data['results']['cset_download_url'] = ( - '/viewer/compound_set/%s' % name + '/viewer/compound_set/%s' % cset.id ) response_data['results']['pset_download_url'] = ( - '/viewer/protein_set/%s' % name + '/viewer/protein_set/%s' % cset.id ) return JsonResponse(response_data) @@ -1097,11 +1094,12 @@ class ComputedSetView( ): """Retrieve information about and delete computed sets.""" - queryset = models.ComputedSet.objects.filter() + queryset = models.ComputedSet.filter_manager.filter_qs() serializer_class = serializers.ComputedSetSerializer filter_permissions = "target__project" - filterset_fields = ('target', 'target__title', 'target__project') permission_classes = [IsObjectProposalMember] + # permission_classes = [permissions.IsAuthenticated, IsObjectProposalMember] + filterset_class = filters.ComputedSetFilter http_method_names = ['get', 'head', 'delete'] @@ -1116,6 +1114,62 @@ def destroy(self, request, pk=None): computed_set.delete() return HttpResponse(status=204) + @action(detail=True, methods=['get']) + def download(self, request, pk=None): + """Download a specific ComputedSet by ID.""" + + try: + computed_set = models.ComputedSet.objects.get(id=pk) + except models.ComputedSet.DoesNotExist: + return Response( + # {'error': f"ComputedSet '{computed_set_id}' not found"}, + {'error': f"ComputedSet '{pk}' not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + if ( + computed_set.target.project.title + not in _ISPYB_SAFE_QUERY_SET.get_proposals_for_user( + request.user, restrict_public_to_membership=False + ) + ): + return Response( + {'error': "You have no access to the Project"}, + status=status.HTTP_403_FORBIDDEN, + ) + + zip_buffer = BytesIO() + + sdfs = models.ComputedSet.history.filter( + written_sdf_filename__isnull=False, + ) + pdbs = computed_set.computed_molecules.filter(pdb__isnull=True) + + # so now, get the file, and get the pdbs + with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED) as ziparchive: + for sdf in sdfs: + sdf_file = Path(sdf.written_sdf_filename) + if sdf_file.exists(): + with open(sdf_file, 'rb') as contents: + ziparchive.writestr(str(sdf.submitted_sdf), contents.read()) + else: + ziparchive.writestr(f'{str(sdf.submitted_sdf)}_MISSING', r'') + + for f in pdbs: + fpath = Path(settings.MEDIA_ROOT).joinpath(f.pdb_info.name) + if fpath.is_file(): + with open(fpath, 'rb') as contents: + ziparchive.writestr(f.get_filename(), contents.read()) + else: + ziparchive.writestr(f'{f.get_filename()}_MISSING', r'') + + response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip') + response['Content-Disposition'] = ( + 'attachment; filename="%s"' % f'{computed_set.name}.zip' + ) + response['Content-Length'] = zip_buffer.getbuffer().nbytes + return response + class ComputedMoleculesView(ISPyBSafeQuerySet): """Retrieve information about computed molecules - 3D info (api/compound-molecules).""" @@ -2649,72 +2703,3 @@ def create(self, request, *args, **kwargs): return Response({"errors": errors}, status=status.HTTP_400_BAD_REQUEST) else: return Response({'success': True}, status=status.HTTP_200_OK) - - -class DownloadComputedSetView(ISPyBSafeQuerySet): - queryset = models.ComputedSet.objects.all() - filter_permissions = "target__project" - serializer_class = serializers.ComputedSetDownloadSerializer - permission_class = [permissions.IsAuthenticated] - - def get_view_name(self): - return "Computed set download" - - def post(self, request, *args, **kwargs): - logger.info("+ DownloadComputedSetView.create called") - del args, kwargs - - logger.debug('data: %s', request.data) - - # If done like this, it's bypassing the validation.. - computed_set_name = request.data['name'] - try: - computed_set = models.ComputedSet.objects.get(name=computed_set_name) - except models.ComputedSet.DoesNotExist: - return Response( - {'error': f"ComputedSet '{computed_set_name}' not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if ( - computed_set.target.project.title - not in _ISPYB_SAFE_QUERY_SET.get_proposals_for_user( - request.user, restrict_public_to_membership=False - ) - ): - return Response( - {'error': "You have no access to the Project"}, - status=status.HTTP_403_FORBIDDEN, - ) - - zip_buffer = BytesIO() - - sdfs = models.ComputedSet.history.filter( - written_sdf_filename__isnull=False, - ) - pdbs = computed_set.computed_molecules.filter(pdb__isnull=True) - - # so now, get the file, and get the pdbs - with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED) as ziparchive: - for sdf in sdfs: - sdf_file = Path(sdf.written_sdf_filename) - if sdf_file.exists(): - with open(sdf_file, 'rb') as contents: - ziparchive.writestr(str(sdf.submitted_sdf), contents.read()) - else: - ziparchive.writestr(f'{str(sdf.submitted_sdf)}_MISSING', r'') - - for f in pdbs: - fpath = Path(settings.MEDIA_ROOT).joinpath(f.pdb_info.name) - if fpath.is_file(): - with open(fpath, 'rb') as contents: - ziparchive.writestr(f.get_filename(), contents.read()) - else: - ziparchive.writestr(f'{f.get_filename()}_MISSING', r'') - - response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip') - response['Content-Disposition'] = ( - 'attachment; filename="%s"' % f'{computed_set.name}.zip' - ) - response['Content-Length'] = zip_buffer.getbuffer().nbytes - return response From 7abbc2a6ef21939f5b125208101853723c4c50ea Mon Sep 17 00:00:00 2001 From: Kalev Takkis Date: Thu, 12 Dec 2024 15:21:05 +0000 Subject: [PATCH 3/4] fix: object membership permission class project lookup --- viewer/permissions.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/viewer/permissions.py b/viewer/permissions.py index d105a6fb..8420c0e8 100644 --- a/viewer/permissions.py +++ b/viewer/permissions.py @@ -32,14 +32,18 @@ def has_object_permission(self, request, view, obj): raise AttributeError( "The view object must define a 'filter_permissions' property" ) - # The object's proposal records (one or many) can be obtained via - # the view's 'filter_permissions' property. A standard - # django property reference, e.g. 'target__project'. - object_proposals = [] - attr_value = getattr(obj, view.filter_permissions) + # The object's proposal records (one or many) can be obtained + # via the view's 'filter_permissions' property. A standard + # django property reference, e.g. 'target__project'. Adding + # '__title' because values_list lookup does not resolve + # objects but returns attribute value. try: - attr_value = getattr(obj, view.filter_permissions) + object_proposals = list( + obj.__class__.objects.filter( + pk=obj.pk, + ).values_list(f'{view.filter_permissions}__title', flat=True) + ) except AttributeError as exc: # Something's gone wrong trying to lookup the project. # Log some 'interesting' contextual information... @@ -54,12 +58,6 @@ def has_object_permission(self, request, view, obj): ) raise PermissionDenied(msg) from exc - if attr_value.__class__.__name__ == "ManyRelatedManager": - # Potential for many proposals... - object_proposals = [p.title for p in attr_value.all()] - else: - # Only one proposal... - object_proposals = [attr_value.title] if not object_proposals: raise PermissionDenied( detail="Authority cannot be granted - the object is not a part of any Project" From 3e37a57a5050034925be64028565f96dcb983f73 Mon Sep 17 00:00:00 2001 From: Kalev Takkis Date: Thu, 12 Dec 2024 15:36:51 +0000 Subject: [PATCH 4/4] chore: merge migrations --- viewer/migrations/0097_merge_20241212_1048.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 viewer/migrations/0097_merge_20241212_1048.py diff --git a/viewer/migrations/0097_merge_20241212_1048.py b/viewer/migrations/0097_merge_20241212_1048.py new file mode 100644 index 00000000..a8e66bd1 --- /dev/null +++ b/viewer/migrations/0097_merge_20241212_1048.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.25 on 2024-12-12 10:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0085_target_alias_order'), + ('viewer', '0096_computedset_unique_computedsetname_target'), + ] + + operations = [ + ]