Skip to content

Commit

Permalink
Merge pull request #3340 from LiteFarmOrg/Merge_main
Browse files Browse the repository at this point in the history
Backport Patch 3.6.5
  • Loading branch information
kathyavini authored Jul 25, 2024
2 parents 87762c1 + 16aaf64 commit d3e0f69
Show file tree
Hide file tree
Showing 61 changed files with 995 additions and 693 deletions.
36 changes: 33 additions & 3 deletions .github/workflows/crowdin-download.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ on:
jobs:
download-translations:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1 # Should be 1 to avoid parallel builds
matrix:
locales: [
# Frontend core translations
{
name: webapp_locales,
source: packages/webapp/public/locales/en/*.json,
translation: packages/webapp/public/locales/%two_letters_code%/%original_file_name%,
},
# Consent Forms
{
name: webapp_consent,
source: packages/webapp/src/containers/Consent/locales/en/*.md,
translation: packages/webapp/src/containers/Consent/locales/%two_letters_code%/%original_file_name%,
},
# Backend tranlsations - skipping pdf (crop.json is copied jobs scheduler init during build)
{
name: api_job_locales,
source: packages/api/src/jobs/locales/en/*.json,
translation: packages/api/src/jobs/locales/%two_letters_code%/%original_file_name%,
},
# email templates
{
name: api_email_templates,
source: packages/api/src/templates/locales/en.json,
translation: packages/api/src/templates/locales/%two_letters_code%.%file_extension%,
},
]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -18,9 +48,9 @@ jobs:
download_translations: true
skip_untranslated_strings: true
export_only_approved: true
source: packages/webapp/public/locales/en/*.json # Sources pattern
translation: packages/webapp/public/locales/%two_letters_code%/%original_file_name% # Translations pattern
localization_branch_name: l10n_crowdin_translations_${{ env.BRANCH_NAME }}
source: ${{ matrix.locales.source }} # Sources pattern
translation: ${{ matrix.locales.translation }} # Translations pattern
localization_branch_name: l10n_crowdin_translations_${{ env.BRANCH_NAME }}_${{ matrix.locales.name }}
create_pull_request: true
pull_request_title: "New Crowdin translations"
pull_request_body: "New Crowdin pull request with translations"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

export const up = async function (knex) {
/*----------------------------------------
Add uniqueness check for (task_id, product_id) composite - data deletion for local + beta only
----------------------------------------*/
const taskProductsWithDuplicates = await knex
.select('task_id', 'product_id')
.count('*', { as: 'cnt' })
.from('soil_amendment_task_products')
.groupBy('task_id', 'product_id')
.havingRaw('COUNT(*) > 1')
.orderBy('task_id', 'asc');
for (const taskProduct of taskProductsWithDuplicates) {
const duplicates = await knex
.select('*')
.table('soil_amendment_task_products')
.where({ task_id: taskProduct.task_id, product_id: taskProduct.product_id })
.orderBy('created_at', 'asc');
const notDeletedDuplicates = duplicates.filter((dupe) => !dupe.deleted);
let keep;
let softDelete;
if (notDeletedDuplicates.length === 0) {
keep = duplicates.pop();
await knex('soil_amendment_task_products').where('id', keep.id).update({ deleted: false });
softDelete = duplicates;
} else if (notDeletedDuplicates.length >= 1) {
//Choose only or latest task_product with deleted false
keep = notDeletedDuplicates.pop();
softDelete = duplicates.filter((dupe) => dupe.id !== keep.id);
}
for (const deleteable of softDelete) {
await knex('soil_amendment_task_products')
.update({ deleted: true })
.where('id', deleteable.id);
}
}

// Add the partial unique index using raw SQL
// Knex partial indexes not working correctly
await knex.raw(`
CREATE UNIQUE INDEX task_product_uniqueness_composite
ON soil_amendment_task_products(task_id, product_id)
WHERE deleted = false;
`);
};

export const down = async function (knex) {
/*----------------------------------------
Add uniqueness check for (task_id, product_id) composite
----------------------------------------*/
await knex.schema.alterTable('soil_amendment_task_products', (table) => {
table.dropIndex(['task_id', 'product_id'], 'task_product_uniqueness_composite');
});
};
4 changes: 2 additions & 2 deletions packages/api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "litefarm-api",
"version": "3.6.4.1",
"version": "3.6.5",
"description": "LiteFarm API server",
"main": "./api/src/server.js",
"type": "module",
Expand Down
24 changes: 20 additions & 4 deletions packages/api/src/controllers/taskController.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ManagementTasksModel from '../models/managementTasksModel.js';
import TransplantTaskModel from '../models/transplantTaskModel.js';
import PlantTaskModel from '../models/plantTaskModel.js';
import HarvestUse from '../models/harvestUseModel.js';
import SoilAmendmentTaskProductsModel from '../models/soilAmendmentTaskProductsModel.js';
import NotificationUser from '../models/notificationUserModel.js';
import User from '../models/userModel.js';
import { typesOfTask } from './../middleware/validation/task.js';
Expand All @@ -39,8 +40,8 @@ async function getTaskAssigneeAndFinalWage(farm_id, user_id, task_id) {
assignee_role_id,
wage_at_moment,
override_hourly_wage,
} = await TaskModel.getTaskAssignee(task_id);
const { role_id } = await UserFarmModel.getUserRoleId(user_id);
} = await TaskModel.getTaskAssignee(task_id, farm_id);
const { role_id } = await UserFarmModel.getUserRoleId(user_id, farm_id);
if (!canCompleteTask(assignee_user_id, assignee_role_id, user_id, role_id)) {
throw new Error("Not authorized to complete other people's task");
}
Expand All @@ -67,8 +68,23 @@ async function updateTaskWithCompletedData(
typeOfTask,
) {
if (typeOfTask === 'soil_amendment_task') {
const { soil_amendment_task_products } = data;

if (soil_amendment_task_products) {
// Temporarily soft delete all with task_id since there is no constraint on deletions
await SoilAmendmentTaskProductsModel.query(trx)
.context({ user_id })
.update({ deleted: true })
.where('task_id', task_id);

// Set deleted false for all in update query
soil_amendment_task_products.forEach((taskProduct) => {
taskProduct.deleted = false;
});
}

// Allows the insertion of missing data if no id present
// Soft deletes tables with soft delete option and hard deletes ones without
// Soft deletes table rows with soft delete option and hard deletes ones without
const task = await TaskModel.query(trx)
.context({ user_id })
.upsertGraph(
Expand Down Expand Up @@ -697,7 +713,7 @@ const taskController = {
const graphTasks = await TaskModel.query()
.whereNotDeleted()
.withGraphFetched(
`[locations.[location_defaults], managementPlans, soil_amendment_task, soil_amendment_task_products.[purpose_relationships], field_work_task.[field_work_task_type], cleaning_task, pest_control_task, harvest_task.[harvest_use], plant_task, transplant_task, irrigation_task.[irrigation_type]]
`[locations.[location_defaults], managementPlans, soil_amendment_task, soil_amendment_task_products(filterDeleted).[purpose_relationships], field_work_task.[field_work_task_type], cleaning_task, pest_control_task, harvest_task.[harvest_use], plant_task, transplant_task, irrigation_task.[irrigation_type]]
`,
)
.whereIn('task_id', taskIds);
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/jobs/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@
},
"Y": "S",
"YES": ""
}
}
4 changes: 2 additions & 2 deletions packages/api/src/jobs/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"NEW_AREA": "Zone (Ajoutée cette année)",
"NON_ORGANIC": "Surface non-biologique en",
"NON_PRODUCING": "Inexploitées",
"OPERATION NAME": "NOM DE L'EXPLOITATION",
"OPERATION_NAME": "NOM DE L'EXPLOITATION",
"ORGANIC_AREA": "Zone",
"PLEASE_VERIFY": "Veuillez vérifier les détails et apporter les modifications nécessaires chaque année. Si l’opération se déroule sur plusieurs sites, veuillez vous assurer de décrire chaque site séparément. Veuillez vous référer à l'exemple d'onglet ci-dessous pour savoir comment remplir ce formulaire.",
"PRODUCTION": "production",
Expand Down Expand Up @@ -90,7 +90,7 @@
"CROP_FIELD_APPLIED_TO": "Culture/champ appliqué à ou unité de production utilisée dans",
"DATE_USED": "Date utilisée",
"LISTED_IN_PSL": "Figurant sur la liste des substances permises? (O/N)",
"Notes": "Notes",
"NOTES": "Notes",
"PRODUCT_NAME": "Nom du produit",
"QUANTITY": "Quantité",
"SUPPLIER": "Nom commercial ou source/fournisseur"
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/jobs/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@
},
"Y": "S",
"YES": "Sim"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export function checkSoilAmendmentTaskProducts() {
return res.status(400).send('at least one task product is required');
}

if (soil_amendment_task_products.some((rel) => rel.deleted)) {
res.status(400).send('Deleted task products should be omitted from request body');
}

for (const product of soil_amendment_task_products) {
if (!product.product_id) {
return res.status(400).send('product_id is required');
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/models/productModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ProductModel extends baseModel {
product_id: { type: 'integer' },
name: { type: 'string' },
product_translation_key: { type: 'string' },
supplier: { type: 'string' },
supplier: { type: ['string', 'null'], maxLength: 255 },
on_permitted_substances_list: {
type: ['string', 'null'],
enum: ['YES', 'NO', 'NOT_SURE', null],
Expand Down
23 changes: 22 additions & 1 deletion packages/api/src/models/soilAmendmentTaskModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ class SoilAmendmentTaskModel extends Model {
return 'task_id';
}

async $beforeUpdate(queryContext) {
await super.$beforeUpdate(queryContext);

if (this.method_id) {
const { key } = await soilAmendmentMethodModel
.query(queryContext.transaction)
.findById(this.method_id)
.select('key')
.first();

if (key !== 'OTHER') {
this.other_application_method = null;
}

if (key !== 'FURROW_HOLE') {
this.furrow_hole_depth = null;
this.furrow_hole_depth_unit = null;
}
}
}

// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
Expand All @@ -42,7 +63,7 @@ class SoilAmendmentTaskModel extends Model {
type: ['string', 'null'],
enum: [...furrowHoleDepthUnits, null],
},
other_application_method: { type: ['string', 'null'] },
other_application_method: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
},
additionalProperties: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class soilAmendmentTaskProductPurposeRelationshipModel extends Model {
properties: {
task_products_id: { type: 'integer' },
purpose_id: { type: 'integer' },
other_purpose: { type: ['string', 'null'] },
other_purpose: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
},
additionalProperties: false,
};
Expand Down
7 changes: 7 additions & 0 deletions packages/api/src/models/soilAmendmentTaskProductsModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ class SoilAmendmentTaskProducts extends BaseModel {
};
}

static modifiers = {
filterDeleted(query) {
const { ref } = SoilAmendmentTaskProducts;
query.where(ref('deleted'), false);
},
};

// Custom function used in copy crop plan
// Should contain all jsonSchema() and relationMappings() keys
static get templateMappingSchema() {
Expand Down
8 changes: 2 additions & 6 deletions packages/api/src/models/taskModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,6 @@ class TaskModel extends BaseModel {
from: 'task.task_id',
to: 'soil_amendment_task_products.task_id',
},
modify: (query) =>
query.select('*').whereIn('id', function () {
this.select('id').from('soil_amendment_task_products').where('deleted', false);
}),
},
pest_control_task: {
relation: Model.HasOneRelation,
Expand Down Expand Up @@ -282,7 +278,7 @@ class TaskModel extends BaseModel {
* @async
* @returns {Object} - Object {assignee_user_id, assignee_role_id, wage_at_moment, override_hourly_wage}
*/
static async getTaskAssignee(taskId) {
static async getTaskAssignee(taskId, farmId) {
return await TaskModel.query()
.whereNotDeleted()
.join('users', 'task.assignee_user_id', 'users.user_id')
Expand All @@ -293,7 +289,7 @@ class TaskModel extends BaseModel {
'users.user_id as assignee_user_id, role.role_id as assignee_role_id, task.wage_at_moment, task.override_hourly_wage',
),
)
.where('task.task_id', taskId)
.where({ 'task.task_id': taskId, 'uf.farm_id': farmId })
.first();
}

Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/models/userFarmModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ class userFarm extends Model {
* @async
* @returns {number} Number corresponding to role_id.
*/
static async getUserRoleId(userId) {
static async getUserRoleId(userId, farmId) {
return userFarm
.query()
.join('role', 'userFarm.role_id', 'role.role_id')
.select('role.role_id')
.where('userFarm.user_id', userId)
.where({ 'userFarm.user_id': userId, 'userFarm.farm_id': farmId })
.first();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ if (process.env.SENTRY_DSN && environment !== 'development') {
// Automatically instrument Node.js libraries and frameworks
...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),
],
release: '3.6.4.1',
release: '3.6.5',
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
Expand Down
Loading

0 comments on commit d3e0f69

Please sign in to comment.