Skip to content

Commit

Permalink
Merge pull request #1635 from UUDigitalHumanitieslab/feature/download…
Browse files Browse the repository at this point in the history
…-tab

Feature/download tab
  • Loading branch information
lukavdplas authored Jul 22, 2024
2 parents 9515c99 + b5fbd5d commit 1fd6116
Show file tree
Hide file tree
Showing 26 changed files with 454 additions and 246 deletions.
13 changes: 11 additions & 2 deletions backend/addcorpus/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,18 @@ class VisualizationType(Enum):
'scan',
'tab-scan'
'p',
'tags',
'context',
'tab',
]
'''
Field names that cannot be used because they are also query parameters in frontend routes.
Field names that cannot be used because they interfere with other functionality.
Using them would make routing ambiguous.
This is usually because they are also query parameters in frontend routes, and using them
would make routing ambiguous.
`query` is also forbidden because it is a reserved column in CSV downloads. Likewise,
`context` is forbidden because it's used in download requests.
`scan` and `tab-scan` are added because they interfere with element IDs in the DOM.
'''
19 changes: 19 additions & 0 deletions backend/download/tests/test_download_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,22 @@ def test_query_text_in_csv(db, client, basic_mock_corpus, basic_corpus_public, i
reader = csv.DictReader(stream, delimiter=';')
row = next(reader)
assert row['query'] == 'ghost'

@pytest.mark.xfail(reason='query in context download does not work')
def test_download_with_query_in_context(
db, admin_client, small_mock_corpus, index_small_mock_corpus
):
es_query = query.set_query_text(query.MATCH_ALL, 'the')
es_query['highlight'] = { 'fragment_size': 200, 'fields': { 'content': {} } }
es_query['size'] = 3
request_json = {
'corpus': small_mock_corpus,
'es_query': es_query,
'fields': ['date', 'content', 'context'],
'route': f"/search/{small_mock_corpus}?query=the&highlight=200",
'encoding': 'utf-8'
}
response = admin_client.post(
'/api/download/search_results', request_json, content_type='application/json'
)
assert status.is_success(response.status_code)
115 changes: 96 additions & 19 deletions frontend/src/app/download/download.component.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,99 @@
<div class="level-item">
<div class="field has-addons">
<p class="control" iaBalloon="Only the first {{downloadLimit || directDownloadLimit}} results will be in the file!"
[iaBalloonVisible]="hasLimitedResults ? undefined : false" iaBalloonLength="fit">
<button class="button is-primary" [ngClass]="{'is-loading':isDownloading}" type="download"
(click)="chooseDownloadMethod()" [disabled]="downloadDisabled">
<span class="icon">
<fa-icon [icon]="actionIcons.download" aria-hidden="true"></fa-icon>
</span>
<span>Download csv</span>
</button>
</p>
<p class="control" iaBalloon="Click here to select which fields should appear in the csv" iaBalloonLength="medium">
<ia-select-field [corpusFields]="availableCsvFields" filterCriterion="downloadable"
(selection)="selectCsvFields($event)">
</ia-select-field>
<ng-container *ngIf="totalResults.result$ | async as total">
<p class="block" aria-live="polite" aria-atomic="true">{{total}} results.</p>

<p class="block">
You can download your search results as a CSV file. View the
<a [routerLink]="['/manual', 'download']">manual</a>
for more information.
</p>

<div class="message" *ngIf="downloadLimit < total">
<p class="message-body">
Only the first {{downloadLimit}} results will be included in the file.
</p>
</div>
</div>
<form>
<div class="field">
<label class="label" id="fields-select-label">Fields</label>
<div class="control">
<p-multiSelect [options]="availableCsvFields"
[(ngModel)]="selectedCsvFields"
optionLabel="displayName"
placeholder="select fields"
name="searchFields"
ariaLabelledBy="fields-select-label">
</p-multiSelect>
</div>
<p class="help">
Select which fields should be included as columns in the CSV file.
</p>
</div>

<div class="field">
<p class="label">
Sort results
</p>
<ia-search-sorting [pageResults]="resultsConfig"></ia-search-sorting>
</div>

<!-- TODO: show this option when query-in-context download is fixed -->
<div class="field" *ngIf="queryModel.queryText" hidden>
<div role="group" class="control">
<legend class="label">
Additional columns
</legend>
<label>
<!-- checkbox: include query? -->
<!-- checkbox: include user tags? -->
<input type="checkbox"
[checked]="(resultsConfig.highlight$ | async) !== undefined"
(change)="onHighlightChange($event)">
Include "query in context" snippets
</label>
</div>
</div>

<ia-download-options [download]="pendingDownload" [isDownloading]="isDownloading" (cancel)="pendingDownload = undefined"
(confirm)="confirmDirectDownload($event)"></ia-download-options>
<ng-container *ngIf="(canDownloadDirectly$ | async); else longDownloadSubmit">
<div class="field">
<div role="group" class="control">
<legend class="label">File encoding</legend>
<label class="radio" *ngFor="let encodingOption of encodingOptions">
<input type="radio" name="encoding" (click)="encoding=encodingOption" [checked]="encoding===encodingOption">
{{encodingOption}}
</label>
</div>
<p class="help">
We recommend using utf-8 encoding for most applications, including Python and R.
For importing files in Microsoft Excel, we recommend utf-16.
</p>
</div>
<div class="block">
<button class="button is-primary" [ngClass]="{'is-loading':isDownloading}"
type="submit"
(click)="confirmDirectDownload()" [disabled]="total == 0">
<span class="icon">
<fa-icon [icon]="actionIcons.download" aria-hidden="true"></fa-icon>
</span>
<span>Download</span>
</button>
</div>
</ng-container>
<ng-template #longDownloadSubmit>
<div class="message">
<p class="message-body">
Your download contains too many documents to be immediately available.
You can request the download now, and receive an email when it's
ready.
</p>
</div>
<div class="block">
<button class="button is-primary" type="submit" (click)="longDownload()">
<span class="icon">
<fa-icon [icon]="actionIcons.wait"></fa-icon>
</span>
<span>Request download</span>
</button>
</div>
</ng-template>
</form>
</ng-container>
18 changes: 10 additions & 8 deletions frontend/src/app/download/download.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { commonTestBed } from '../common-test-bed';
import { QueryModel } from '../models';

import { DownloadComponent } from './download.component';
import { SimpleChange } from '@angular/core';

describe('DownloadComponent', () => {
let component: DownloadComponent;
Expand All @@ -19,7 +20,9 @@ describe('DownloadComponent', () => {
component = fixture.componentInstance;
component.corpus = mockCorpus;
component.queryModel = new QueryModel(mockCorpus);
component.ngOnChanges();
component.ngOnChanges({
queryModel: new SimpleChange(undefined, component.queryModel, true)
});
fixture.detectChanges();
});

Expand All @@ -29,16 +32,15 @@ describe('DownloadComponent', () => {

it('should respond to field selection', () => {
// Start with a single field
expect(component['getCsvFields']()).toEqual(mockCorpus.fields);
expect(component['getColumnNames']()).toEqual(['great_field', 'speech']);

// Deselect all
component.selectCsvFields([]);
expect(component['getCsvFields']()).toEqual([]);
component.selectedCsvFields = [];
expect(component['getColumnNames']()).toEqual([]);

// Select two
component.selectCsvFields([mockField, mockField2]);
const expected_fields = [mockField, mockField2];
expect(component['getCsvFields']()).toEqual(expected_fields);
expect(component.selectedCsvFields).toEqual(expected_fields);
component.selectedCsvFields = [mockField, mockField2];
const expected_fields = ['great_field', 'speech'];
expect(component['getColumnNames']()).toEqual(expected_fields);
});
});
Loading

0 comments on commit 1fd6116

Please sign in to comment.