89-poster-defaults (#96)

* print defaults in printproducts

* Add default to printproduct mutation

* added insertDefaults when changing productgroup

* fixed bug for square in square

* cleanup, and tests for calculations

* small fixes, added return values for update
This commit is contained in:
Arwid Thornström
2021-12-08 11:00:15 +01:00
committed by GitHub
parent b556bb8df2
commit 6741e82479
9 changed files with 3669 additions and 20 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
"exportSQL": "cd src/__test__/db_generator/ && npm install && node index.js && cd ../../../",
"test": "npm run exportSQL && jest --detectOpenHandles",
"test:dev": "npm run exportSQL && jest --watch",
"unit:dev": "jest --watch src/__test__/unit.test.ts",
"unit": "jest --testPathIgnorePatterns=\"src/__test__/all.test.ts\"",
"unit:dev": "jest --testPathIgnorePatterns=\"src/__test__/all.test.ts\" --watch src/**/*",
"test:manual": "cd manual-tests && ts-node test-scopes.ts"
},
"engines": {
+2
View File
@@ -48,6 +48,7 @@ describe('GraphQL Apollo unit tests', () => {
expect(() => checkAccess(['orders.read'], auth)).not.toThrow();
});
});
describe('Test auth not ok for test', () => {
it('user bla is accepted', () => {
const auth: ClaimVerifyResult = {
@@ -86,6 +87,7 @@ describe('GraphQL Apollo unit tests', () => {
).not.toThrow();
});
});
describe('Test id auth no enough', () => {
it('user test is accepted', () => {
const auth = JSON.parse(JSON.stringify(idTokenAuth)) as ClaimVerifyResult;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
import { Maybe } from 'graphql/jsutils/Maybe';
import {
BorderType,
PrintProductDefaults,
} from '../types/printproduct-defaults-types';
import { BaseSQLDataSource } from './BaseSQLDataSource';
type TPosterDefaultPositioning = {
width_mm: number;
height_mm: number;
crop_x: number;
crop_y: number;
};
export const calculateDefaultPositioning = (
productWidth: number,
productHeight: number,
focusXpoint2: number,
focusYpoint2: number,
): TPosterDefaultPositioning => {
// Because we dont want to be dependent on different environments
// number of supported decimals to round to 10 decimal places.
// And I know 70/50 is 1.4 so its not needed but added for clarity.
const size70x50ratio = parseFloat((70 / 50).toFixed(10));
const size50x70ratio = parseFloat((50 / 70).toFixed(10));
const width_height_ratio = productWidth / productHeight;
// If focuspoints are null we set to 50 to make calculations
// below a bit clearer.
const fpX = focusXpoint2 === null ? 50 : focusXpoint2;
const fpY = focusYpoint2 === null ? 50 : focusYpoint2;
const width_mm = width_height_ratio >= size70x50ratio ? 700 : 500;
const height_mm = width_height_ratio <= size50x70ratio ? 700 : 500;
// Calculate where to put the crop_x percent value
// We place the center of the crop area as close to
// the focusXpoint2 and focusYpoint2 values. We do
// different calculations depending on if the poster size
// orientation (landscape, portrait or square) is inside
// the motif orientation.
//
// The goal for the calculations is to get the smallest amount of area
// that we crop away.
const crop_x = (() => {
if (width_height_ratio >= size70x50ratio) {
const centerOfRatioArea = (productHeight * 1.4) / 2;
let x = Math.max(centerOfRatioArea, (fpX / 100) * productWidth); // Place center close to focuspointX but not outside the left edge
x = Math.min(productWidth - centerOfRatioArea, x); // Dont place outside the right edge
x = x - centerOfRatioArea; // Convert center to left edge
x = x / productWidth; // Convert to percent;
return x;
} else if (
// Square in landscape
width_height_ratio > 1 &&
width_height_ratio < size70x50ratio
) {
const centerOfRatioArea = productHeight / 2;
let x = Math.max(centerOfRatioArea, (fpX / 100) * productWidth); // Place center close to focuspointX but not outside the left side
x = Math.min(productWidth - centerOfRatioArea, x); // Dont place outside the right edge
x = x - centerOfRatioArea; // Convert center to left edge
x = x / productWidth; // Convert to percent
return x;
} else {
// If the scenario is, square in portrait, portrait in portrait or square
// in square then we are placing vertical, therefor x will be 0.
return 0;
}
})();
// Read comment for crop_x above.
const crop_y = (() => {
if (width_height_ratio < 1 && width_height_ratio > size50x70ratio) {
// Square in portrait
const centerOfRatioArea = productWidth / 2;
let y = Math.max(centerOfRatioArea, (fpY / 100) * productHeight); // Place center close to focuspointY but not ouside top edge
y = Math.min(productHeight - centerOfRatioArea, y); // Dont place outside bottom edge
y = y - centerOfRatioArea; // Convert center to top edge
y = y / productHeight; // Convert to percent
return y;
} else if (width_height_ratio <= size50x70ratio) {
// Portrait in portrait
const centerOfRatioArea = productWidth / size50x70ratio / 2;
let y = Math.max(centerOfRatioArea, (fpY / 100) * productHeight); // Place center close to focuspointY but not ouside top edge
y = Math.min(productHeight - centerOfRatioArea, y); // Dont place outside bottom edge
y = y - centerOfRatioArea; // Convert center to top edge
y = y / productHeight; // Convert to percent
return y;
} else {
// If the scenario is landscape in landscape, square in landscape or square in square
// the we are placing horizontal and therefor y will be 0.
return 0;
}
})();
return { width_mm, height_mm, crop_x, crop_y };
};
export class PrintProductDefaultsAPI extends BaseSQLDataSource {
constructor(config) {
super(config);
}
/**
* @function getBorderType
* @description Will convert lowercase none | white | black to
* uppercase and define it as a BorderType
*/
getBorderType(dbValue: string): string {
switch (dbValue) {
case 'white':
return BorderType[BorderType.WHITE];
case 'black':
return BorderType[BorderType.BLACK];
default:
return BorderType[BorderType.NONE];
}
}
/**
* @function getDefaults
* @description This function will fetch the default values from printproducts_defaults table
* with the printId as a key. Border is defined in lowercase in DB but returned in uppercase
* to keep consistency in both DB and graphql.
*/
async getDefaults(printId: number): Promise<Maybe<PrintProductDefaults>> {
return this.knex
.column(
{ printId: 'print_id' },
{ widthMm: 'width_mm' },
{ heightMm: 'height_mm' },
{ cropX: 'crop_x' },
{ cropY: 'crop_y' },
'border',
)
.select()
.from('printproducts_defaults')
.where('print_id', printId)
.first()
.then((row) => {
if (row) {
return Object.assign(
{
...row,
},
{ border: this.getBorderType(row.border) },
);
} else {
return null;
}
});
}
/**
* @function insertDefaults
* @description Inserts a new printproduct into printproducts_defaults, sets the default
* values the same way we did when settings default values for all older printproducts.
*/
async insertDefaults(printId: number): Promise<0 | 1> {
const product = await this.knex.raw(
`
SELECT width, height, "focusXpoint2", "focusYpoint2" FROM
"product-printproducts" ppp
JOIN v_product vp ON ppp.productid = vp.id
WHERE printid = ?`,
[printId],
);
const { width, height, focusXpoint2, focusYpoint2 } = product.rows[0];
// GUARDS
if (!width || !height)
throw new Error(
'PrintProductDefaultsAPI.insertDefaults: width and height must be defined for a motif to be able to set defaults',
);
const { width_mm, height_mm, crop_x, crop_y } = calculateDefaultPositioning(
width,
height,
focusXpoint2,
focusYpoint2,
);
return this.knex
.raw(
`
INSERT INTO printproducts_defaults (print_id, width_mm, height_mm, crop_x, crop_y, border)
VALUES (?, ?, ?, ?, ?, 'none');
`,
[printId, width_mm, height_mm, crop_x, crop_y],
)
.catch((e) => {
throw new Error(e);
});
}
// ---------------------------------------------------------
// MUTATIONS
// ---------------------------------------------------------
/**
* @function updateDefaults
* @description Updates or inserts default values for a printproduct in the
* table printproducts_defaults.
*/
async updateDefaults(
printId: number,
widthMm: number,
heightMm: number,
cropX: number,
cropY: number,
border: 'none' | 'white' | 'black',
): Promise<PrintProductDefaults> {
return this.knex
.table('printproducts_defaults')
.update(
{
width_mm: widthMm,
height_mm: heightMm,
crop_x: cropX,
crop_y: cropY,
border: border.toLocaleLowerCase(),
},
['print_id', 'width_mm', 'height_mm', 'crop_x', 'crop_y', 'border'],
)
.where({ print_id: printId })
.then((row) => {
if (row) {
const first = row[0];
return Object.assign(
{
...first,
},
{ border: this.getBorderType(first.border) },
);
} else {
return null;
}
})
.catch((e) => {
throw new Error(
`Something went wrong when trying to update with printid [${printId}][${e.message}]`,
);
});
}
}
+29 -2
View File
@@ -30,14 +30,18 @@ import {
import { UserInputError } from 'apollo-server';
import { sendPathChangedCmd } from '../bernard/client';
import { TextsAPI } from './texts-api';
import { PrintProductDefaultsAPI } from './printproduct-defaults-api';
const MINUTE = 60;
export class ProductAPI extends BaseSQLDataSource {
textsApi: TextsAPI;
constructor(config, textsApi) {
printProductDefaultsApi: PrintProductDefaultsAPI;
constructor(config, textsApi, printProductDefaultsApi) {
super(config);
this.textsApi = textsApi;
this.printProductDefaultsApi = printProductDefaultsApi;
}
// async getMaterial(printId: number): Promise<any> {
@@ -607,12 +611,20 @@ export class ProductAPI extends BaseSQLDataSource {
// Delete from product-printproducts_materials
const printProductIds = shouldDelete.map((item) => item.printid);
// Delete from materials first, must be in sequence since we have constraint in materials to printproducts
// Delete from materials first, must be in sequence
// since we have constraint in materials to printproducts
await this.knex
.from('product-printproducts_materials')
.whereIn('printid', printProductIds)
.del();
// Delete from printproducts_defaults, same here run
// this first because of constraint.
await this.knex
.from('printproducts_defaults')
.whereIn('printId', printProductIds)
.del();
// Delete from product-printproducts
await this.knex
.from('product-printproducts')
@@ -648,6 +660,21 @@ export class ProductAPI extends BaseSQLDataSource {
.where('productid', productId)
.whereIn('groupid', shouldAdd);
// Add printproduct-defaults for all new printIds
const insertDefaults = [];
newPrintIds.forEach(({ printid, groupid }) => {
// Possible to add for canvas and framed-print later
if (groupid === ProductGroup.POSTER) {
// Add printproduct defaults if poster
insertDefaults.push(
this.printProductDefaultsApi.insertDefaults(printid),
);
}
});
// Run parallell
await Promise.all(insertDefaults);
// Insert printIds into product-printproducts_materials
const insertMaterials = [];
const insertMaterialQuery = `INSERT INTO "product-printproducts_materials" (printid, materialid) VALUES (?, ?)`;
+9 -1
View File
@@ -19,6 +19,8 @@ import { verifyToken } from './cognito/cognito-client';
import { readFileSync } from 'fs';
import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api';
import { TextsAPI } from './datasources/texts-api';
import { PrintProductDefaultsAPI } from './datasources/printproduct-defaults-api';
const path = require('path');
const typeDefs = readFileSync(
path.join(__dirname, './schema.graphql'),
@@ -35,14 +37,20 @@ const designerApi = new DesignerAPI(knexConfig);
const imageServerApi = new ImageServerApi();
const interiorsLambdaApi = new InteriorsLambdaAPI();
const interiorApi = new InteriorAPI(knexConfig, imageServerApi);
const printProductDefaultsApi = new PrintProductDefaultsAPI(knexConfig);
const keywordApi = new KeywordAPI(knexConfig);
const orderApi = new OrderAPI(knexConfig);
const productApi = new ProductAPI(knexConfig, textsApi);
const productApi = new ProductAPI(
knexConfig,
textsApi,
printProductDefaultsApi,
);
const dataSources = () => ({
categoryApi,
designerApi,
interiorApi,
printProductDefaultsApi,
interiorsLambdaApi,
imageServerApi,
keywordApi,
+23
View File
@@ -1,5 +1,6 @@
import { ProductAPI } from '../datasources/product-api';
import {
PrintProduct,
Product,
ProductListResult,
ProductsFilterInput,
@@ -17,6 +18,8 @@ import { IdNumberResult } from '../types/types';
import { moveS3File } from '../aws/s3';
import { DesignerAPI } from '../datasources/designer-api';
import { checkAccess } from '../cognito/access-control';
import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api';
import { PrintProductDefaults } from '../types/printproduct-defaults-types';
const ProductBlacklist = {
async market(parent, _args, { dataSources }) {
@@ -50,6 +53,11 @@ const PrintProduct = {
async interiors({ id }, _args, { dataSources }) {
return (<InteriorAPI>dataSources.interiorApi).getPrintProductInteriors(id);
},
async defaults({ id }, _args, { dataSources }) {
return (<PrintProductDefaultsAPI>(
dataSources.printProductDefaultsApi
)).getDefaults(id);
},
};
async function getProductsResult(
@@ -276,6 +284,21 @@ export const productMutationTypeDefs = {
);
},
/**
* @function printProductDefaults
* @description Add default values to a printproduct, this is used through admin gui.
*/
async printProductDefaults(
_,
{ printId, widthMm, heightMm, cropX, cropY, border },
{ dataSources, auth },
): Promise<PrintProductDefaults> {
checkAccess(['products.write'], auth);
return (<PrintProductDefaultsAPI>(
dataSources.printProductDefaultsApi
)).updateDefaults(printId, widthMm, heightMm, cropX, cropY, border);
},
/**
* @function productWallpaperType
* @description Set the wallpapertype for a wallpaper product
+51 -16
View File
@@ -390,6 +390,7 @@ type PrintProduct {
inserted: DateTime!
updated: DateTime
interiors: [Interior]
defaults: PrintProductDefaults
}
enum InteriorType {
@@ -413,6 +414,21 @@ type Interior {
orientation: Orientation
}
enum BorderType {
NONE
WHITE
BLACK
}
type PrintProductDefaults {
printId: ID
widthMm: Float
heightMm: Float
cropX: Float
cropY: Float
border: BorderType
}
type Designer {
id: ID!
name: String
@@ -476,32 +492,29 @@ type IdNumberResult {
}
type Mutation {
""" Product """
addProduct(name: String!, batch: String): Product
productInfo(productId: Int!, info: ProductInfoInput!): Product
productFocusPoint(
productWallpaperType(
productId: Int!
focusXpoint2: Float!
focusYpoint2: Float!
wallpaperTypes: [ProductWallpaperType]!
): Product
productBlacklisting(
productId: Int!
markets: [ProductBlacklistingInput]
): Product
productGroup(productId: Int!, groupIds: [Int]): Product
productGroupInteriors(productId: Int!, groupId: Int!, uris: [String]): Product
productKeywords(productId: Int!, keywordIds: [Int]): Product
""" Product Image """
productFocusPoint(
productId: Int!
focusXpoint2: Float!
focusYpoint2: Float!
): Product
productProportionsWarning(
productId: Int!
proportions: ProductProportionsInput!
): Product
addOwnInteriorToPrintProduct(
printId: Int!
uploadedS3Key: String!
): IdNumberResult
productWallpaperType(
productId: Int!
wallpaperTypes: [ProductWallpaperType]!
): Product
updateProductImage(
productId: Int!
uploadedS3Key: String!
@@ -509,16 +522,38 @@ type Mutation {
height: Int!
): String
""" Product Relations """
addRelatedProducts(productId: Int!, articleNumbers: [String]!): Product
removeRelatedProducts(productId: Int!, relatedProductIds: [Int]!): Product
addKeyword(name: String!, type: KeywordType): Keyword
""" Product Categories """
addCategoriesToProduct(productId: Int!, categoryIds: [Int]!): Product
removeCategoriesFromProduct(productId: Int!, categoryIds: [Int]!): Product
""" Product Keywords """
productKeywords(productId: Int!, keywordIds: [Int]): Product
addKeyword(name: String!, type: KeywordType): Keyword
""" Product Other """
updateProductComments(productId: Int!, comments: String!): Product
""" Printproducts """
productGroup(productId: Int!, groupIds: [Int]): Product
productGroupInteriors(productId: Int!, groupId: Int!, uris: [String]): Product
addOwnInteriorToPrintProduct(
printId: Int!
uploadedS3Key: String!
): IdNumberResult
printProductDefaults(
printId: Int!
widthMm: Int!
heightMm: Int!
cropX: Float!
cropY: Float!
border: BorderType!
): PrintProductDefaults
""" Mass Update """
massUpdatePublishing(
productIds: [Int]!
visible: Boolean
+14
View File
@@ -0,0 +1,14 @@
export enum BorderType {
NONE = 0,
WHITE = 1,
BLACK = 2,
}
export interface PrintProductDefaults {
printId: number;
widthMm: number;
heightMm: number;
cropX: number;
cropY: number;
border: string;
}