diff --git a/apps/entry/admin.py b/apps/entry/admin.py index 13512843aa..c481e39a57 100644 --- a/apps/entry/admin.py +++ b/apps/entry/admin.py @@ -9,6 +9,7 @@ from entry.models import ( Entry, Attribute, + EntryAttachment, FilterData, ExportData, EntryComment, @@ -86,4 +87,6 @@ class ProjectEntryLabelAdmin(VersionAdmin): list_display = ('__str__', 'color') +admin.site.register(EntryAttachment) + reversion.register(LeadEntryGroup) diff --git a/apps/entry/migrations/0038_auto_20240609_0904.py b/apps/entry/migrations/0038_auto_20240609_0904.py new file mode 100644 index 0000000000..14029c166b --- /dev/null +++ b/apps/entry/migrations/0038_auto_20240609_0904.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.25 on 2024-06-09 09:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('entry', '0037_merge_20220401_0527'), + ] + + operations = [ + migrations.AlterField( + model_name='entry', + name='entry_type', + field=models.CharField(choices=[('excerpt', 'Excerpt'), ('image', 'Image'), ('attachment', 'Attachment'), ('dataSeries', 'Data Series')], default='excerpt', max_length=10), + ), + migrations.CreateModel( + name='EntryAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entry_file_type', models.PositiveSmallIntegerField(choices=[('1', 'XLSX')], default='1')), + ('file', models.FileField(upload_to='entry/attachment/')), + ('file_preview', models.FileField(upload_to='entry/attachment-preview')), + ('entry', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='entry.entry')), + ], + ), + ] diff --git a/apps/entry/migrations/0039_auto_20240609_0933.py b/apps/entry/migrations/0039_auto_20240609_0933.py new file mode 100644 index 0000000000..53a8985094 --- /dev/null +++ b/apps/entry/migrations/0039_auto_20240609_0933.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-06-09 09:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('entry', '0038_auto_20240609_0904'), + ] + + operations = [ + migrations.RemoveField( + model_name='entryattachment', + name='entry', + ), + migrations.AddField( + model_name='entry', + name='entry_attachment', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='entry.entryattachment'), + ), + ] diff --git a/apps/entry/migrations/0040_alter_entryattachment_entry_file_type.py b/apps/entry/migrations/0040_alter_entryattachment_entry_file_type.py new file mode 100644 index 0000000000..ad7aeccb31 --- /dev/null +++ b/apps/entry/migrations/0040_alter_entryattachment_entry_file_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-06-09 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('entry', '0039_auto_20240609_0933'), + ] + + operations = [ + migrations.AlterField( + model_name='entryattachment', + name='entry_file_type', + field=models.PositiveSmallIntegerField(choices=[(1, 'XLSX')], default=1), + ), + ] diff --git a/apps/entry/models.py b/apps/entry/models.py index 1771b2798a..c7b98da209 100644 --- a/apps/entry/models.py +++ b/apps/entry/models.py @@ -21,6 +21,18 @@ from assisted_tagging.models import DraftEntry +class EntryAttachment(models.Model): + class EntryFileType(models.IntegerChoices): + XLSX = 1, 'XLSX' + + entry_file_type = models.PositiveSmallIntegerField( + choices=EntryFileType.choices, + default=EntryFileType.XLSX + ) + file = models.FileField(upload_to='entry/attachment/') + file_preview = models.FileField(upload_to='entry/attachment-preview') + + class Entry(UserResource, ProjectEntityMixin): """ Entry belonging to a lead @@ -32,6 +44,7 @@ class Entry(UserResource, ProjectEntityMixin): class TagType(models.TextChoices): EXCERPT = 'excerpt', 'Excerpt', IMAGE = 'image', 'Image', + ATTACHMENT = 'attachment', 'Attachment', DATA_SERIES = 'dataSeries', 'Data Series' # NOTE: data saved as tabular_field id lead = models.ForeignKey(Lead, on_delete=models.CASCADE) @@ -47,6 +60,7 @@ class TagType(models.TextChoices): image = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True) image_raw = models.TextField(blank=True) tabular_field = models.ForeignKey('tabular.Field', on_delete=models.CASCADE, null=True, blank=True) + entry_attachment = models.OneToOneField(EntryAttachment, on_delete=models.CASCADE, null=True, blank=True) dropped_excerpt = models.TextField(blank=True) # NOTE: Original Exceprt. Modified version is stored in excerpt excerpt_modified = models.BooleanField(default=False) diff --git a/apps/entry/schema.py b/apps/entry/schema.py index f9a369aa9b..41b9f309a5 100644 --- a/apps/entry/schema.py +++ b/apps/entry/schema.py @@ -6,7 +6,7 @@ from utils.common import has_prefetched from utils.graphene.enums import EnumDescription -from utils.graphene.types import CustomDjangoListObjectType, ClientIdMixin +from utils.graphene.types import CustomDjangoListObjectType, ClientIdMixin, FileFieldType from utils.graphene.fields import DjangoPaginatedListObjectField, DjangoListField from user_resource.schema import UserResourceMixin from deep.permissions import ProjectPermissions as PP @@ -20,6 +20,7 @@ from .models import ( Entry, Attribute, + EntryAttachment, ) from .enums import EntryTagTypeEnum from .filter_set import EntryGQFilterSet @@ -84,6 +85,14 @@ def resolve_geo_selected_options(root, info, **_): ) +class EntryAttachmentType(DjangoObjectType): + file = graphene.Field(FileFieldType) + file_preview = graphene.Field(FileFieldType) + + class Meta: + model = EntryAttachment + + class EntryType(UserResourceMixin, ClientIdMixin, DjangoObjectType): class Meta: model = Entry @@ -92,7 +101,7 @@ class Meta: 'lead', 'project', 'analysis_framework', 'information_date', 'order', 'excerpt', 'dropped_excerpt', 'image', 'tabular_field', 'highlight_hidden', 'controlled', 'controlled_changed_by', - 'client_id', + 'client_id', 'entry_attachment' ) entry_type = graphene.Field(EntryTagTypeEnum, required=True) @@ -103,6 +112,7 @@ class Meta: verified_by_count = graphene.Int(required=True) review_comments_count = graphene.Int(required=True) draft_entry = graphene.ID(source="draft_entry_id") + entry_attachment = graphene.Field(EntryAttachmentType, required=False) # project_labels TODO: # tabular_field TODO: diff --git a/apps/entry/serializers.py b/apps/entry/serializers.py index 1340b53897..99d53721da 100644 --- a/apps/entry/serializers.py +++ b/apps/entry/serializers.py @@ -29,6 +29,7 @@ from .models import ( Attribute, Entry, + EntryAttachment, EntryComment, EntryCommentText, ExportData, @@ -591,12 +592,12 @@ class EntryGqSerializer(ProjectPropertySerializerMixin, TempClientIdMixin, UserR ' This will be changed into gallery image and supplied back in image field.' ) ) - lead_image = serializers.PrimaryKeyRelatedField( + lead_attachment = serializers.PrimaryKeyRelatedField( required=False, write_only=True, queryset=LeadPreviewAttachment.objects.all(), help_text=( - 'This is used to add images from Lead Preview Images.' + 'This is used to add attachment from Lead Preview Attachment.' ' This will be changed into gallery image and supplied back in image field.' ) ) @@ -611,7 +612,7 @@ class Meta: 'entry_type', 'image', 'image_raw', - 'lead_image', + 'lead_attachment', 'tabular_field', 'excerpt', 'dropped_excerpt', @@ -643,7 +644,7 @@ def validate(self, data): request = self.context['request'] image = data.get('image') image_raw = data.pop('image_raw', None) - lead_image = data.pop('lead_image', None) + lead_attachment = data.pop('lead_attachment', None) # ---------------- Lead lead = data['lead'] @@ -685,12 +686,21 @@ def validate(self, data): 'image': f'You don\'t have permission to attach image: {image}', }) # If lead image is provided make sure lead are same - elif lead_image: - if lead_image.lead != lead: + elif lead_attachment: + if lead_attachment.lead != lead: raise serializers.ValidationError({ - 'lead_image': f'You don\'t have permission to attach lead image: {lead_image}', + 'lead_attachment': f'You don\'t have permission to attach lead attachment: {lead_attachment}', }) - data['image'] = lead_image.clone_as_deep_file(request.user) + + if lead_attachment.type == LeadPreviewAttachment.AttachementFileType.XLSX: + data['entry_attachment'] = EntryAttachment.objects.create( + file=lead_attachment.file, + file_preview=lead_attachment.file_preview + ) + data['entry_type'] = Entry.TagType.ATTACHMENT + else: + data['image'] = lead_attachment.clone_as_deep_file(request.user) + data['entry_type'] = Entry.TagType.IMAGE elif image_raw: generated_image = base64_to_deep_image(image_raw, lead, request.user) if isinstance(generated_image, File): diff --git a/apps/lead/filter_set.py b/apps/lead/filter_set.py index 28eeac509b..49bb47b94d 100644 --- a/apps/lead/filter_set.py +++ b/apps/lead/filter_set.py @@ -22,7 +22,7 @@ from entry.filter_set import EntryGQFilterSet, EntriesFilterDataInputType, EntriesFilterDataType from user_resource.filters import UserResourceGqlFilterSet -from .models import Lead, LeadGroup, LeadDuplicates +from .models import Lead, LeadGroup, LeadDuplicates, LeadPreviewAttachment from .enums import ( LeadConfidentialityEnum, LeadStatusEnum, @@ -30,6 +30,7 @@ LeadSourceTypeEnum, LeadOrderingEnum, LeadExtractionStatusEnum, + LeadPreviewAttachmentTypeEnum, ) @@ -552,6 +553,14 @@ def filter_title(self, qs, name, value): return qs.filter(title__icontains=value).distinct() +class LeadPreviewAttachmentGQFilterSet(UserResourceGqlFilterSet): + type = MultipleInputFilter(LeadPreviewAttachmentTypeEnum, field_name='type') + + class Meta: + model = LeadPreviewAttachment + fields = ['lead', 'page_number'] + + LeadsFilterDataType, LeadsFilterDataInputType = generate_type_for_filter_set( LeadGQFilterSet, 'lead.schema.LeadListType', diff --git a/apps/lead/migrations/0053_alter_leadpreviewattachment_type.py b/apps/lead/migrations/0053_alter_leadpreviewattachment_type.py new file mode 100644 index 0000000000..a47c92bd98 --- /dev/null +++ b/apps/lead/migrations/0053_alter_leadpreviewattachment_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-06-09 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0052_alter_leadpreviewattachment_type'), + ] + + operations = [ + migrations.AlterField( + model_name='leadpreviewattachment', + name='type', + field=models.PositiveSmallIntegerField(choices=[(1, 'XLSX'), (2, 'Image')], default=1), + ), + ] diff --git a/apps/lead/models.py b/apps/lead/models.py index f941e2b4f4..21f1221ab6 100644 --- a/apps/lead/models.py +++ b/apps/lead/models.py @@ -376,7 +376,7 @@ class LeadPreviewAttachment(models.Model): """ NOTE: File can be only used by gallery (when attached to a entry) """ - class AttachementFileType(models.TextChoices): + class AttachementFileType(models.IntegerChoices): XLSX = 1, 'XLSX' IMAGE = 2, 'Image' diff --git a/apps/lead/schema.py b/apps/lead/schema.py index 24c3107373..c0a83ec5f4 100644 --- a/apps/lead/schema.py +++ b/apps/lead/schema.py @@ -47,6 +47,7 @@ from .filter_set import ( LeadGQFilterSet, LeadGroupGQFilterSet, + LeadPreviewAttachmentGQFilterSet, ) @@ -219,15 +220,16 @@ class Meta: class LeadPreviewAttachmentType(DjangoObjectType): - file = graphene.Field(FileFieldType) - file_preview = graphene.Field(FileFieldType) - type = graphene.Field(LeadPreviewAttachmentTypeEnum) + file = graphene.Field(FileFieldType, required=True) + file_preview = graphene.Field(FileFieldType, required=True) + type = graphene.Field(LeadPreviewAttachmentTypeEnum, required=True) class Meta: model = LeadPreviewAttachment only_fields = ( - 'page_number', + 'id', 'order', + 'page_number', ) @@ -362,7 +364,6 @@ class Meta: extraction_status = graphene.Field(LeadExtractionStatusEnum) lead_preview = graphene.Field(LeadPreviewType) - lead_preview_attachment = graphene.List(graphene.NonNull(LeadPreviewAttachmentType), required=True) source = graphene.Field(OrganizationType) authors = DjangoListField(OrganizationType) assignee = graphene.Field(UserType) @@ -472,6 +473,12 @@ class Meta: filterset_class = LeadGQFilterSet +class LeadPreviewAttachmentListType(CustomDjangoListObjectType): + class Meta: + model = LeadPreviewAttachment + filterset_class = LeadPreviewAttachmentGQFilterSet + + class Query: lead = DjangoObjectField(LeadDetailType) leads = DjangoPaginatedListObjectField( @@ -499,6 +506,13 @@ class Query: page_size_query_param='pageSize' ) ) + lead_preview_attachments = DjangoPaginatedListObjectField( + LeadPreviewAttachmentListType, + pagination=PageGraphqlPagination( + page_size_query_param='pageSize', + ) + ) + # TODO: Add Pagination emm_keywords = graphene.List(graphene.NonNull(EmmKeyWordType)) emm_risk_factors = graphene.List(graphene.NonNull(EmmKeyRiskFactorType)) diff --git a/apps/lead/tests/test_apis.py b/apps/lead/tests/test_apis.py index 5a813e2537..6a98639dbb 100644 --- a/apps/lead/tests/test_apis.py +++ b/apps/lead/tests/test_apis.py @@ -1802,11 +1802,18 @@ def test_extractor_callback_url(self, get_file_mock, get_text_mock, index_lead_f 'page_number': 1, 'images': [ 'http://random.com/image1.jpeg', - 'http://random.com/image1.jpeg' + 'http://random.com/image2.jpeg' ], } ], - 'tables_path': [], + 'tables_path': [ + { + "page_number": 1, + "order": 0, + "image_link": "http://random.com/timetable.png", + "content_link": "http://random.com/table_timetable.xlsx" + } + ], 'text_path': 'http://random.com/extracted_file.txt', 'url': 'http://random.com/pdf_file.pdf', 'total_words_count': 300, @@ -1835,7 +1842,10 @@ def test_extractor_callback_url(self, get_file_mock, get_text_mock, index_lead_f self.assertEqual(lead_preview.text_extract, 'Extracted text') self.assertEqual(lead_preview.word_count, 300) self.assertEqual(lead_preview.page_count, 4) - self.assertEqual(LeadPreviewAttachment.objects.filter(lead=self.lead).count(), 2) + self.assertEqual(LeadPreviewAttachment.objects.filter(lead=self.lead).count(), 3) + self.assertEqual(LeadPreviewAttachment.objects.filter( + lead=self.lead, type=LeadPreviewAttachment.AttachementFileType.IMAGE).count(), 2 + ) index_lead_func.assert_called_once_with(self.lead.id) diff --git a/schema.graphql b/schema.graphql index b36971ecf3..d943389417 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3268,7 +3268,7 @@ input BulkEntryInputType { entryType: EntryTagTypeEnum image: ID imageRaw: String - leadImage: ID + leadAttachment: ID tabularField: ID excerpt: String droppedExcerpt: String @@ -3925,6 +3925,18 @@ type EntriesFilterDataType { filterableData: [EntryFilterDataType!] } +enum EntryAttachmentEntryFileType { + A_1 +} + +type EntryAttachmentType { + id: ID! + entryFileType: EntryAttachmentEntryFileType! + file: FileFieldType + filePreview: FileFieldType + entry: EntryType +} + input EntryFilterDataInputType { filterKey: ID! value: String @@ -3968,7 +3980,7 @@ input EntryInputType { entryType: EntryTagTypeEnum image: ID imageRaw: String - leadImage: ID + leadAttachment: ID tabularField: ID excerpt: String droppedExcerpt: String @@ -4050,6 +4062,7 @@ enum EntryReviewCommentTypeEnum { enum EntryTagTypeEnum { EXCERPT IMAGE + ATTACHMENT DATA_SERIES } @@ -4063,6 +4076,7 @@ type EntryType { informationDate: Date excerpt: String! image: GalleryFileType + entryAttachment: EntryAttachmentType droppedExcerpt: String! highlightHidden: Boolean! controlled: Boolean @@ -4561,7 +4575,6 @@ type LeadDetailType { statusDisplay: EnumDescription! extractionStatus: LeadExtractionStatusEnum leadPreview: LeadPreviewType - leadPreviewAttachment: [LeadPreviewAttachmentType!]! source: OrganizationType authors: [OrganizationType!] emmEntities: [EmmEntityType!] @@ -4693,12 +4706,20 @@ enum LeadOrderingEnum { DESC_ENTRIES_COUNT } +type LeadPreviewAttachmentListType { + results: [LeadPreviewAttachmentType!] + totalCount: Int + page: Int + pageSize: Int +} + type LeadPreviewAttachmentType { + id: ID! order: Int! pageNumber: Int! - file: FileFieldType - filePreview: FileFieldType - type: LeadPreviewAttachmentTypeEnum + file: FileFieldType! + filePreview: FileFieldType! + type: LeadPreviewAttachmentTypeEnum! } enum LeadPreviewAttachmentTypeEnum { @@ -4766,7 +4787,6 @@ type LeadType { statusDisplay: EnumDescription! extractionStatus: LeadExtractionStatusEnum leadPreview: LeadPreviewType - leadPreviewAttachment: [LeadPreviewAttachmentType!]! source: OrganizationType authors: [OrganizationType!] emmEntities: [EmmEntityType!] @@ -5266,6 +5286,7 @@ type ProjectDetailType { leadGroups(createdAt: DateTime, createdAtGte: DateTime, createdAtLte: DateTime, modifiedAt: DateTime, modifiedAtGte: DateTime, modifiedAtLte: DateTime, createdBy: [ID!], modifiedBy: [ID!], search: String, page: Int = 1, ordering: String, pageSize: Int): LeadGroupListType emmEntities(name: String, page: Int = 1, ordering: String, pageSize: Int): EmmEntityListType leadEmmTriggers(lead: ID, emmKeyword: String, emmRiskFactor: String, count: Int, page: Int = 1, ordering: String, pageSize: Int): LeadEmmTriggerListType + leadPreviewAttachments(lead: ID, pageNumber: Int, createdAt: DateTime, createdAtGte: DateTime, createdAtLte: DateTime, modifiedAt: DateTime, modifiedAtGte: DateTime, modifiedAtLte: DateTime, createdBy: [ID!], modifiedBy: [ID!], type: [LeadPreviewAttachmentTypeEnum!], page: Int = 1, ordering: String, pageSize: Int): LeadPreviewAttachmentListType emmKeywords: [EmmKeyWordType!] emmRiskFactors: [EmmKeyRiskFactorType!] userSavedLeadFilter: UserSavedLeadFilterType