Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove previously chained models from choices & fix circular deps #2

Merged
merged 8 commits into from
Aug 18, 2021
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "djangoql-completion",
"description": "DjangoQL completion widget",
"version": "0.3.3",
"version": "0.4.0",
"license": "MIT",
"homepage": "https://github.com/ivelum/djangoql-completion",
"author": "Denis Stebunov (https://github.com/stebunovd)",
Expand Down
32 changes: 28 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,9 @@ DjangoQL.prototype = {
let model = this.currentModel;
let field = null;

const modelStack = [];
if (model) {
modelStack.push(model);
for (i = 0, l = nameParts.length; i < l; i++) {
f = this.models[model][nameParts[i]];
if (!f) {
Expand All @@ -608,13 +610,14 @@ DjangoQL.prototype = {
break;
} else if (f.type === 'relation') {
model = f.relation;
modelStack.push(model);
field = null;
} else {
field = nameParts[i];
}
}
}
return { model, field };
return { modelStack, model, field };
},

getContext(text, cursorPos) {
Expand All @@ -623,6 +626,8 @@ DjangoQL.prototype = {
let scope = null; // 'field', 'comparison', 'value', 'logical' or null
let model = null; // model, set for 'field', 'comparison' and 'value'
let field = null; // field, set for 'comparison' and 'value'
// Stack of models that includes all entered models
let modelStack = [this.currentModel];

let nameParts;
let resolvedName;
Expand Down Expand Up @@ -658,14 +663,15 @@ DjangoQL.prototype = {
prefix = '';
}

const logicalTokens = ['AND', 'OR'];
if (prefix === ')' && !whitespace) {
// Nothing to suggest right after right paren
} else if (!lastToken
|| (['AND', 'OR'].indexOf(lastToken.name) >= 0 && whitespace)
|| (logicalTokens.indexOf(lastToken.name) >= 0 && whitespace)
|| (prefix === '.' && lastToken && !whitespace)
|| (lastToken.name === 'PAREN_L'
&& (!nextToLastToken
|| ['AND', 'OR'].indexOf(nextToLastToken.name) >= 0))) {
|| logicalTokens.indexOf(nextToLastToken.name) >= 0))) {
scope = 'field';
model = this.currentModel;
if (prefix === '.') {
Expand All @@ -678,6 +684,7 @@ DjangoQL.prototype = {
resolvedName = this.resolveName(nameParts.join('.'));
if (resolvedName.model && !resolvedName.field) {
model = resolvedName.model;
modelStack = resolvedName.modelStack;
} else {
// if resolvedName.model is null that means that model wasn't found.
// if resolvedName.field is NOT null that means that the name
Expand All @@ -698,6 +705,7 @@ DjangoQL.prototype = {
scope = 'value';
model = resolvedName.model;
field = resolvedName.field;
modelStack = resolvedName.modelStack;
if (prefix[0] === '"' && (this.models[model][field].type === 'str'
|| this.models[model][field].options)) {
prefix = prefix.slice(1);
Expand All @@ -709,19 +717,22 @@ DjangoQL.prototype = {
scope = 'comparison';
model = resolvedName.model;
field = resolvedName.field;
modelStack = resolvedName.modelStack;
}
} else if (lastToken
&& whitespace
&& ['PAREN_R', 'INT_VALUE', 'FLOAT_VALUE', 'STRING_VALUE']
.indexOf(lastToken.name) >= 0) {
scope = 'logical';
}

return {
prefix,
scope,
model,
field,
currentFullToken,
modelStack,
};
},

Expand Down Expand Up @@ -887,13 +898,26 @@ DjangoQL.prototype = {
this.highlightCaseSensitive = true;

const context = this.getContext(input.value, input.selectionStart);
const { modelStack } = context;
this.prefix = context.prefix;
const model = this.models[context.model];
const field = context.field && model[context.field];

switch (context.scope) {
case 'field':
this.suggestions = Object.keys(model).map((f) => (
this.suggestions = Object.keys(model).filter((f) => {
const { relation } = model[f];
if ((model[f].type === 'relation')
// Check that the model from a field relation wasn't in the stack
&& modelStack.includes(relation)
// Last element in the stack could be equal to context model.
// E.g. an "author" can have the "authors_in_genre" relation
&& (modelStack.slice(-1)[0] !== relation)
) {
return false;
}
return true;
}).map((f) => (
suggestion(f, '', model[f].type === 'relation' ? '.' : ' ')
));
break;
Expand Down
119 changes: 108 additions & 11 deletions tests/DjangoQL.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,25 +192,67 @@ describe('test DjangoQL completion', () => {
describe('.resolveName()', () => {
it('should properly resolve known names', () => {
expect(djangoQL.resolveName('price'))
.toStrictEqual({ model: 'core.book', field: 'price' });
.toStrictEqual({
model: 'core.book',
field: 'price',
modelStack: ['core.book'],
});
expect(djangoQL.resolveName('author'))
.toStrictEqual({ model: 'auth.user', field: null });
.toStrictEqual({
model: 'auth.user',
field: null,
modelStack: ['core.book', 'auth.user'],
});
expect(djangoQL.resolveName('author.first_name'))
.toStrictEqual({ model: 'auth.user', field: 'first_name' });
.toStrictEqual({
model: 'auth.user',
field: 'first_name',
modelStack: ['core.book', 'auth.user'],
});
expect(djangoQL.resolveName('author.groups'))
.toStrictEqual({ model: 'auth.group', field: null });
.toStrictEqual({
model: 'auth.group',
field: null,
modelStack: ['core.book', 'auth.user', 'auth.group'],
});
expect(djangoQL.resolveName('author.groups.id'))
.toStrictEqual({ model: 'auth.group', field: 'id' });
.toStrictEqual({
model: 'auth.group',
field: 'id',
modelStack: ['core.book', 'auth.user', 'auth.group'],
});
expect(djangoQL.resolveName('author.groups.user'))
.toStrictEqual({ model: 'auth.user', field: null });
.toStrictEqual({
model: 'auth.user',
field: null,
modelStack: ['core.book', 'auth.user', 'auth.group', 'auth.user'],
});
expect(djangoQL.resolveName('author.groups.user.email'))
.toStrictEqual({ model: 'auth.user', field: 'email' });
.toStrictEqual({
model: 'auth.user',
field: 'email',
modelStack: ['core.book', 'auth.user', 'auth.group', 'auth.user'],
});
});
it('should return nulls for unknown names', () => {
['gav', 'author.gav', 'author.groups.gav'].forEach((name) => {
expect(djangoQL.resolveName(name))
.toStrictEqual({ model: null, field: null });
});
expect(djangoQL.resolveName('gav'))
.toStrictEqual({
model: null,
field: null,
modelStack: ['core.book'],
});
expect(djangoQL.resolveName('author.gav'))
.toStrictEqual({
model: null,
field: null,
modelStack: ['core.book', 'auth.user'],
});
expect(djangoQL.resolveName('author.groups.gav'))
.toStrictEqual({
model: null,
field: null,
modelStack: ['core.book', 'auth.user', 'auth.group'],
});
});
});

Expand Down Expand Up @@ -384,6 +426,9 @@ describe('test DjangoQL completion', () => {
examples.forEach((e) => {
const result = djangoQL.getContext(...e.args);
delete result.currentFullToken; // it's not relevant in this case
// Model Stack properly builds only for continiously interaction
// with textarea for now
delete result.modelStack;
expect(result).toStrictEqual(e.result);
});
});
Expand All @@ -403,4 +448,56 @@ describe('test DjangoQL completion', () => {
});
});
});

describe('.generateSuggestions()', () => {
it('should not have circular deps', () => {
djangoQL.textarea.value = 'author.';
djangoQL.generateSuggestions();
// "book.author.book" sholdn't be suggested
expect(djangoQL.suggestions).toStrictEqual(
expect.not.arrayContaining([{
snippetAfter: '.',
snippetBefore: '',
suggestionText: 'book',
text: 'book',
}]),
);

// Change model and test in reverse side
djangoQL.currentModel = 'auth.user';
djangoQL.textarea.value = 'book.';
djangoQL.generateSuggestions();
expect(djangoQL.suggestions).toStrictEqual(
expect.not.arrayContaining([{
snippetAfter: '.',
snippetBefore: '',
suggestionText: 'author',
text: 'author',
}]),
);

djangoQL.currentModel = 'auth.group';
djangoQL.textarea.value = 'user.';
djangoQL.generateSuggestions();
expect(djangoQL.suggestions).toStrictEqual(
expect.arrayContaining([{
snippetAfter: '.',
snippetBefore: '',
suggestionText: 'book',
text: 'book',
}]),
);
djangoQL.textarea.value = 'user.book.';
djangoQL.generateSuggestions();
// "User" model is already in the Model Stack
expect(djangoQL.suggestions).toStrictEqual(
expect.not.arrayContaining([{
snippetAfter: '.',
snippetBefore: '',
suggestionText: 'author',
text: 'author',
}]),
);
});
});
});