Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions backend/ianalyzer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@
api_router.register('indexing/jobs', IndexJobViewset, basename='index-job')

if settings.PROXY_FRONTEND:
spa_url = re_path(r'^(?P<path>.*)$', proxy_frontend)
spa_url = re_path(r'^(?P<path>(?!api\/)(?!users\/).*)$', proxy_frontend)
else:
spa_url = re_path(r'', index)
spa_url = re_path(r'^((?!api\/)(?!users\/).*)', index)


urlpatterns = [
path('admin', RedirectView.as_view(url='/admin/', permanent=True)),
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/app/services/api.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';

import { ApiService } from './api.service';
import { ApiService, joinURLPath } from './api.service';
import { HttpClient, HttpErrorResponse, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { fakeNgramResult } from '@mock-data/api';
import { Subject, from, throwError } from 'rxjs';
Expand Down Expand Up @@ -73,3 +73,23 @@ describe('ApiService', () => {
}));

});

describe('joinURLPath', () => {
it('joins path segments', () => {
expect(joinURLPath('foo', 'bar', 'baz')).toBe('foo/bar/baz');
});

it('removes double slashes', () => {
expect(joinURLPath('foo/', 'bar')).toBe('foo/bar');
expect(joinURLPath('foo', '/bar')).toBe('foo/bar');
expect(joinURLPath('foo/', '/bar')).toBe('foo/bar');
});

it('does not remove leading slash', () => {
expect(joinURLPath('/foo', 'bar', 'baz')).toBe('/foo/bar/baz');
});

it('does not remove trailing slash', () => {
expect(joinURLPath('foo', 'bar', 'baz/')).toBe('foo/bar/baz/');
});
});
115 changes: 79 additions & 36 deletions frontend/src/app/services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ import * as _ from 'lodash';
import {
APIEditableCorpus,
CorpusDataFile,
DataFileInfo,
} from '@models/corpus-definition';
import { APIIndexHealth, APIIndexJob, isComplete, JobStatus } from '@models/indexing';
import { APIIndexHealth, APIIndexJob, isComplete } from '@models/indexing';
import { ImageInfo } from '@models/image';

interface SolisLoginResponse {
Expand All @@ -48,42 +47,71 @@ interface SolisLoginResponse {
queries: QueryDb[];
}

export const joinURLPath = (...parts: string[]) => {
const cleanParts = parts.map((part, i) => {
// remove trailing/leading slashes from segments

// easy exception to provide '/' as an explicit segment; should never be trimmed.
if (part == '/') {
return part;
}

const cleanStart: (s: string) => string = (i == 0) ?
_.identity :
s => _.trimStart(s, '/');

const cleanEnd: (s: string) => string =
(i == parts.length - 1) ?
_.identity :
s => _.trimEnd(s, '/');

return cleanStart(cleanEnd(part));
});

return cleanParts.join('/');
};

@Injectable({
providedIn: 'root',
})
export class ApiService {
private apiUrl = environment.apiUrl;

private authApiUrl = 'users';
private authApiUrl = '/users';
private visApiURL = 'visualization';
private downloadApiURL = 'download';
private corpusApiUrl = 'corpus';
private tagApiUrl = 'tag';
private indexApiUrl = 'indexing';

private authApiRoute = (route: string): string =>
`/${this.authApiUrl}/${route}/`;
joinURLPath(this.authApiUrl, route);

private apiRoute = (subApi: string, route: string): string =>
`${this.apiUrl}/${subApi}/${route}`;
joinURLPath(this.apiUrl, subApi, route);

constructor(private http: HttpClient) {}

public deleteSearchHistory(): Observable<any> {
return this.http.post('/api/search_history/delete_all/', {});
return this.http.post(
joinURLPath(this.apiUrl, 'search_history/delete_all/'), {});
}

public searchHistory() {
return this.http.get<QueryDb[]>('/api/search_history/').toPromise();
return this.http.get<QueryDb[]>(
joinURLPath(this.apiUrl, 'search_history/')
).toPromise();
}

public downloads(): Promise<Download[]> {
return this.http.get<Download[]>('/api/download/').toPromise();
return this.http.get<Download[]>(
joinURLPath(this.apiUrl, 'download/')
).toPromise();
}

// Media
public getMedia(data: { args: string }): Promise<any> {
const url = `/api/get_media${data.args}`;
const url = `${joinURLPath(this.apiUrl, 'get_media')}${data.args}`;
return this.http
.get(url, { observe: 'response', responseType: 'arraybuffer' })
.toPromise();
Expand All @@ -103,7 +131,7 @@ export class ApiService {
};
return this.http
.post<{ media: string[]; info?: ImageInfo }>(
'/api/request_media',
joinURLPath(this.apiUrl, 'request_media'),
requestData
)
.toPromise();
Expand All @@ -113,18 +141,23 @@ export class ApiService {
corpus_index: string;
filepath: string;
}): Promise<any> {
const url = `/api/download_pdf/${data.corpus_index}/${data.filepath}`;
const url = joinURLPath(
this.apiUrl, 'download_pdf', data.corpus_index, data.filepath
);
return this.http.get(url).toPromise();
}

// Tasks
public getTasksStatus(tasks: TaskResult): Observable<TasksOutcome> {
return this.http.post<TasksOutcome>('/api/task_status', tasks);
return this.http.post<TasksOutcome>(
joinURLPath(this.apiUrl, 'task_status'), tasks
);
}

public abortTasks(data: TaskResult): Promise<TaskSuccess> {
return this.http
.post<TaskSuccess>('/api/abort_tasks', data)
.post<TaskSuccess>(
joinURLPath(this.apiUrl, 'abort_tasks'), data)
.toPromise();
}

Expand Down Expand Up @@ -177,6 +210,7 @@ export class ApiService {
data: DateTermFrequencyParameters
): Promise<TaskResult> {
const url = this.apiRoute(this.visApiURL, 'date_term_frequency');
console.log(url);
return this.http.post<TaskResult>(url, data).toPromise();
}

Expand Down Expand Up @@ -259,26 +293,28 @@ export class ApiService {

/** fetch a list of all corpora available for searching */
public corpus() {
return this.http.get<Corpus[]>('/api/corpus/');
return this.http.get<Corpus[]>(joinURLPath(this.apiUrl, 'corpus/'));
}

// Corpus definitions

public corpusDefinitions(): Observable<APIEditableCorpus[]> {
return this.http.get<APIEditableCorpus[]>('/api/corpus/definitions/');
return this.http.get<APIEditableCorpus[]>(
joinURLPath(this.apiUrl, 'corpus/definitions/')
);
}

public corpusDefinition(corpusID: number): Observable<APIEditableCorpus> {
return this.http.get<APIEditableCorpus>(
`/api/corpus/definitions/${corpusID}/`
joinURLPath(this.apiUrl, `/corpus/definitions/${corpusID}/`)
);
}

public createCorpus(
data: APIEditableCorpus
): Observable<APIEditableCorpus> {
return this.http.post<APIEditableCorpus>(
'/api/corpus/definitions/',
joinURLPath(this.apiUrl, 'corpus/definitions/'),
data
);
}
Expand All @@ -288,17 +324,20 @@ export class ApiService {
data: APIEditableCorpus
): Observable<APIEditableCorpus> {
return this.http.put<APIEditableCorpus>(
`/api/corpus/definitions/${corpusID}/`,
joinURLPath(this.apiUrl, `corpus/definitions/${corpusID}/`),
data
);
}

public deleteCorpus(corpusID: number): Observable<any> {
return this.http.delete(`/api/corpus/definitions/${corpusID}/`);
return this.http.delete(
joinURLPath(this.apiUrl, `corpus/definitions/${corpusID}/`));
}

public corpusSchema(): Observable<any> {
return this.http.get('/api/corpus/definition-schema');
return this.http.get(
joinURLPath(this.apiUrl, 'corpus/definition-schema')
);
}

public updateCorpusImage(corpusName: string, file: File): Observable<any> {
Expand All @@ -323,21 +362,21 @@ export class ApiService {
formData.append('corpus', String(corpusId));
formData.append('is_sample', 'True');
return this.http.post<CorpusDataFile>(
`/api/corpus/datafiles/`,
this.apiRoute(this.corpusApiUrl, `datafiles/`),
formData
);
}

public deleteDataFile(dataFile: CorpusDataFile): Observable<null> {
const url = `/api/corpus/datafiles/${dataFile.id}/`;
const url = this.apiRoute(this.corpusApiUrl, `datafiles/${dataFile.id}/`);
return this.http.delete<null>(url);
}

public patchDataFile(
fileId: number,
data: Partial<CorpusDataFile>
): Observable<CorpusDataFile> {
const url = `/api/corpus/datafiles/${fileId}/`;
const url = this.apiRoute(this.corpusApiUrl, `datafiles/${fileId}/`);
return this.http.patch<CorpusDataFile>(url, data);
}

Expand All @@ -348,15 +387,17 @@ export class ApiService {
const params = new HttpParams()
.set('corpus', corpusId)
.set('samples', samples);
return this.http.get<CorpusDataFile[]>('/api/corpus/datafiles/', {
params: params,
});
return this.http.get<CorpusDataFile[]>(
this.apiRoute(this.corpusApiUrl, 'datafiles/'),
{ params: params }
);
}

// Tagging

public userTags(): Observable<Tag[]> {
const url = this.apiRoute(this.tagApiUrl, 'tags/');
console.log(url);
return this.http.get<Tag[]>(url);
}

Expand Down Expand Up @@ -398,21 +439,21 @@ export class ApiService {

// Authentication API
public login(username: string, password: string) {
return this.http.post<{ key: string }>(this.authApiRoute('login'), {
return this.http.post<{ key: string }>(this.authApiRoute('login/'), {
username,
password,
});
}

public logout() {
return this.http.post<{ detail: string }>(
this.authApiRoute('logout'),
this.authApiRoute('logout/'),
{}
);
}

public getUser() {
return this.http.get<UserResponse>(this.authApiRoute('user'));
return this.http.get<UserResponse>(this.authApiRoute('user/'));
}

public register(details: {
Expand All @@ -421,26 +462,26 @@ export class ApiService {
password1: string;
password2: string;
}) {
return this.http.post<any>(this.authApiRoute('registration'), details);
return this.http.post<any>(this.authApiRoute('registration/'), details);
}

public verify(key: string) {
return this.http.post<any>(
this.authApiRoute('registration/verify-email'),
this.authApiRoute('registration/verify-email/'),
{ key }
);
}

public keyInfo(key: string) {
return this.http.post<{ username: string; email: string }>(
this.authApiRoute('registration/key-info'),
this.authApiRoute('registration/key-info/'),
{ key }
);
}

public requestResetPassword(email: string) {
return this.http.post<{ detail: string }>(
this.authApiRoute('password/reset'),
this.authApiRoute('password/reset/'),
{ email }
);
}
Expand All @@ -452,7 +493,7 @@ export class ApiService {
newPassword2: string
) {
return this.http.post<{ detail: string }>(
this.authApiRoute('password/reset/confirm'),
this.authApiRoute('password/reset/confirm/'),
{
uid,
token,
Expand Down Expand Up @@ -482,13 +523,15 @@ export class ApiService {
details: Partial<UserResponse>
): Observable<UserResponse> {
return this.http.patch<UserResponse>(
this.authApiRoute('user'),
this.authApiRoute('user/'),
details
);
}

public solisLogin(data: any): Promise<SolisLoginResponse> {
return this.http.get<SolisLoginResponse>('/api/solislogin').toPromise();
return this.http.get<SolisLoginResponse>(
joinURLPath(this.apiUrl, 'solislogin'),
).toPromise();
}

// INDEXING
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/services/elastic-search.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { TagService } from './tag.service';
import { APIQuery } from '@models/search-requests';
import { PageResultsParameters } from '@models/page-results';
import { resultsParamsToAPIQuery } from '@utils/es-query';
import { joinURLPath } from './api.service';
import { environment } from '@environments/environment';


@Injectable()
Expand Down Expand Up @@ -73,7 +75,7 @@ export class ElasticSearchService {
* Execute an ElasticSearch query and return a dictionary containing the results.
*/
private async execute(corpus: Corpus, body: APIQuery) {
const url = `/api/es/${corpus.name}/_search`;
const url = joinURLPath(environment.apiUrl, 'es', corpus.name, '_search');
return this.http.post<SearchResponse>(url, body).toPromise();
}

Expand Down
Loading
Loading