Skip to content
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
18 changes: 18 additions & 0 deletions src/core/migrations/0043_alter_batchcommand_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2026-03-19 20:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0042_alter_batchcommand_operation'),
]

operations = [
migrations.AlterField(
model_name='batchcommand',
name='operation',
field=models.TextField(blank=True, choices=[('create_item', 'Create item'), ('create_property', 'Create property'), ('set_statement', 'Set statement'), ('create_statement', 'Create statement'), ('switch_statement_value', 'Switch statement value'), ('switch_statement_property', 'Switch statement property'), ('switch_statement_property_value', 'Switch statement property and value'), ('remove_statement_by_id', 'Remove statement by id'), ('remove_statement_by_value', 'Remove statement by value'), ('remove_qualifier', 'Remove qualifier'), ('remove_reference', 'Remove reference'), ('set_sitelink', 'Set sitelink'), ('set_label', 'Set label'), ('set_description', 'Set description'), ('remove_sitelink', 'Remove sitelink'), ('remove_label', 'Remove label'), ('remove_description', 'Remove description'), ('add_alias', 'Add alias'), ('remove_alias', 'Remove alias')], null=True),
),
]
27 changes: 26 additions & 1 deletion src/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,10 @@ class Operation(models.TextChoices):
"switch_statement_property",
pgettext_lazy("batchcommand-py-operation-switch-statement-property", "Switch statement property"),
)
SWITCH_STATEMENT_PROPERTY_AND_VALUE = (
"switch_statement_property_value",
pgettext_lazy("batchcommand-py-operation-switch-statement-property-and-value", "Switch statement property and value"),
)
REMOVE_STATEMENT_BY_ID = (
"remove_statement_by_id",
pgettext_lazy(
Expand Down Expand Up @@ -1366,7 +1370,11 @@ def is_add_statement(self):
return self.is_add() and self.what == "STATEMENT"

def is_switch(self):
return self.is_switch_value() or self.is_switch_property()
return self.operation in (
self.Operation.SWITCH_STATEMENT_VALUE,
self.Operation.SWITCH_STATEMENT_PROPERTY,
self.Operation.SWITCH_STATEMENT_PROPERTY_AND_VALUE,
)

def is_switch_value(self):
return self.operation == self.Operation.SWITCH_STATEMENT_VALUE
Expand Down Expand Up @@ -1523,6 +1531,7 @@ def operation_is_combinable(self):
self.Operation.CREATE_PROPERTY,
self.Operation.SWITCH_STATEMENT_VALUE,
self.Operation.SWITCH_STATEMENT_PROPERTY,
self.Operation.SWITCH_STATEMENT_PROPERTY_AND_VALUE,
self.Operation.REMOVE_STATEMENT_BY_VALUE,
self.Operation.REMOVE_QUALIFIER,
self.Operation.REMOVE_REFERENCE,
Expand Down Expand Up @@ -1670,6 +1679,8 @@ def update_entity_json(self, entity: dict):
self._switch_statement_value(entity)
elif self.operation == self.Operation.SWITCH_STATEMENT_PROPERTY:
self._switch_statement_property(entity)
elif self.operation == self.Operation.SWITCH_STATEMENT_PROPERTY_AND_VALUE:
self._switch_statement_property_and_value(entity)
elif self.operation in (self.Operation.ADD_ALIAS, self.Operation.REMOVE_ALIAS):
self._update_entity_aliases(entity)
elif self.operation in (
Expand Down Expand Up @@ -1807,6 +1818,20 @@ def _switch_statement_property(self, entity: dict):
entity["statements"][new_prop].append(statement)
logger.debug("post switch proeprty: ", entity)

def _switch_statement_property_and_value(self, entity: dict):
"""
Switches a statement property and value
"""
statement = self._remove_entity_statement(entity)
if "id" in statement:
statement.pop("id") # id is read-only and defined by wikibase
new_prop = self.json["property_switch"]
statement["property"] = {"id": new_prop}
statement["value"] = self.statement_api_value_switch
entity["statements"].setdefault(new_prop, [])
entity["statements"][new_prop].append(statement)
logger.debug("post switch proeprty: ", entity)

def _update_entity_aliases(self, entity: dict):
"""
Update the entity's aliases, adding or removing.
Expand Down
31 changes: 31 additions & 0 deletions src/core/parsers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,34 @@ def parse_switch_property(self, elements):
data["operation"] = BatchCommand.Operation.SWITCH_STATEMENT_PROPERTY
return data

def parse_switch_property_and_value(self, elements):
llen = len(elements)
if llen != 6:
raise ParserException("SWITCH_PROPERTY_AND_VALUE command must be QID|PID|value|new_PID|new_value")
elements.pop(0)

# qid|pid|value|new_pid|new_value
property_switch = elements.pop(3)
if not self.is_valid_property_id(property_switch):
raise ParserException(f"Invalid property '{property_switch}'")

# since value is only parsed inside `parse_statement`,
# we do this workaround to get it
# qid|pid|value|new_value
new = list(elements)
new.pop(2)
value_switch = self.parse_statement(new, new[0].upper())["value"]

# qid|pid|value
elements.pop(3)
data = self.parse_statement(elements, elements[0].upper())
data["action"] = "switch"
data["what"] = "property"
data["property_switch"] = property_switch
data["value_switch"] = value_switch
data["operation"] = BatchCommand.Operation.SWITCH_STATEMENT_PROPERTY_AND_VALUE
return data

def parse_statement(self, elements, first_command):
llen = len(elements)
if llen < 3:
Expand Down Expand Up @@ -335,6 +363,9 @@ def parse_command(self, raw_command):
elif first_command == "SWITCH_PROPERTY":
logger.debug(f"parsing switch property: {elements}")
data = self.parse_switch_property(elements)
elif first_command == "SWITCH_PROPERTY_AND_VALUE":
logger.debug(f"parsing switch property and value: {elements}")
data = self.parse_switch_property_and_value(elements)
else:
logger.debug(f"parsing statement: {elements}/{first_command}")
data = self.parse_statement(elements, first_command)
Expand Down
68 changes: 68 additions & 0 deletions src/core/tests/test_batch_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,3 +1000,71 @@ def test_property_switch_data_type(self, mocker):
self.assertEqual(commands[1].status, BatchCommand.STATUS_DONE)
self.assertIsNone(commands[1].message)
self.assertIsNone(commands[1].error)

@requests_mock.Mocker()
def test_property_and_value_switch_data_type(self, mocker):
self.api_mocker.is_autoconfirmed(mocker)
self.api_mocker.property_data_type(mocker, "P5", "wikibase-item")
self.api_mocker.property_data_type(mocker, "P6", "wikibase-item")
self.api_mocker.property_data_type(mocker, "P7", "string")
self.api_mocker.wikidata_property_data_types(mocker)
self.api_mocker.item(
mocker,
"Q1",
{
"statements": {
"P5": [
{
"id": "Q1$abcdefgh-uijkl",
"value": {
"type": "value",
"content": "Q12",
},
"qualifiers": [],
"references": [],
"property": {"id": "P5", "data_type": "wikibase-item"},
},
],
},
},
)
self.api_mocker.patch_item_successful(mocker, "Q1", {})
raw = """
SWITCH_PROPERTY_AND_VALUE|Q1|P5|Q12|P7|Q13
SWITCH_PROPERTY_AND_VALUE|Q1|P5|Q12|P6|Q13
SWITCH_PROPERTY_AND_VALUE|Q1|P5|Q12|P6|"https://kernel.org/"
SWITCH_PROPERTY_AND_VALUE|Q1|P5|Q12|P6|+2000-01-01T00:00:00Z/11
SWITCH_PROPERTY_AND_VALUE|Q1|P5|Q12|P7|+2000-01-01T00:00:00Z/11
"""
batch = self.parse(raw)
commands = batch.commands()
batch.run()
self.assertEqual(batch.status, Batch.STATUS_DONE)
self.assertEqual(commands[0].status, BatchCommand.STATUS_ERROR)
self.assertIsNone(commands[0].error) # TODO: we should have an error type, no?
self.assertEqual(
commands[0].message,
"Invalid value type for the property P7: 'wikibase-entityid' was provided but it needs 'string'.",
)
self.assertEqual(commands[1].status, BatchCommand.STATUS_DONE)
self.assertIsNone(commands[1].message)
self.assertIsNone(commands[1].error)
self.assertEqual(batch.status, Batch.STATUS_DONE)
self.assertEqual(commands[2].status, BatchCommand.STATUS_ERROR)
self.assertIsNone(commands[2].error) # TODO: we should have an error type, no?
self.assertEqual(
commands[2].message,
"Invalid value type for the property P5: 'string' was provided but it needs 'wikibase-entityid'.",
)
self.assertEqual(commands[3].status, BatchCommand.STATUS_ERROR)
self.assertIsNone(commands[3].error)
self.assertEqual(
commands[3].message,
"Invalid value type for the property P5: 'time' was provided but it needs 'wikibase-entityid'.",
)
self.assertEqual(commands[4].status, BatchCommand.STATUS_ERROR)
self.assertIsNone(commands[4].error)
self.assertEqual(
commands[4].message,
"Invalid value type for the property P5: 'time' was provided but it needs 'wikibase-entityid'.",
)
51 changes: 51 additions & 0 deletions src/core/tests/test_entity_patching.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,54 @@ def test_switch_property(self):
entity["statements"]["P99"][0]["references"],
self.INITIAL["statements"]["P65"][0]["references"]
)

def test_switch_property_and_value(self):
text = """
SWITCH_PROPERTY_AND_VALUE|Q12345678|P1|42|P99|1337
SWITCH_PROPERTY_AND_VALUE|Q12345678|P65|1111|P99|1337
SWITCH_PROPERTY_AND_VALUE|Q12345678|P65|42|P99|1337
"""
batch = self.parse(text)
entity = copy.deepcopy(self.INITIAL)
self.assertStmtnCount(entity, "P65", 1)
self.assertStmtnCount(entity, "P99", 0)
self.assertEqual(
entity["statements"]["P65"][0]["value"]["content"]["amount"], "+42"
)
self.assertQualCount(entity, "P65", 2)
self.assertRefCount(entity, "P65", 0)
# -----
switch = batch.commands()[0]
with self.assertRaises(NoStatementsForThatProperty):
switch.update_entity_json(entity)
# -----
switch = batch.commands()[1]
with self.assertRaises(NoStatementsWithThatValue):
switch.update_entity_json(entity)
# -----
switch = batch.commands()[2]
switch.update_entity_json(entity)
self.assertStmtnCount(entity, "P65", 0)
self.assertStmtnCount(entity, "P99", 1)
self.assertQualCount(entity, "P99", 2)
self.assertRefCount(entity, "P99", 0)
self.assertEqual(
entity["statements"]["P99"][0]["property"]["id"],
"P99",
)
self.assertIsNone(entity["statements"]["P99"][0].get("id"))
self.assertEqual(
entity["statements"]["P99"][0]["rank"],
self.INITIAL["statements"]["P65"][0]["rank"],
)
self.assertEqual(
entity["statements"]["P99"][0]["value"]["content"]["amount"], "+1337"
)
self.assertEqual(
entity["statements"]["P99"][0]["qualifiers"],
self.INITIAL["statements"]["P65"][0]["qualifiers"]
)
self.assertEqual(
entity["statements"]["P99"][0]["references"],
self.INITIAL["statements"]["P65"][0]["references"]
)
2 changes: 1 addition & 1 deletion translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,5 +215,5 @@
"batch-button-rerun": "Rerun",
"batch-button-download-report": "Download report",
"login-token-expired": "Your Wikimedia authentication has expired. This is completely normal and it happens for security reasons. Please, log in again.",
"batchcommand-py-operation-switch-statement-property": "Switch statement property"
"batchcommand-py-operation-switch-statement-property-and-value": "Switch statement property and value"
}
3 changes: 2 additions & 1 deletion translations/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"batchcommand-py-operation-create-statement": "Individual command operation type displayed in command list",
"batchcommand-py-operation-switch-statement-value": "Individual command operation type displayed in command list",
"batchcommand-py-operation-switch-statement-property": "Individual command operation type displayed in command list",
"batchcommand-py-operation-switch-statement-property-value": "Individual command operation type displayed in command list",
"batchcommand-py-operation-remove-statement-by-id": "Individual command operation type displayed in command list",
"batchcommand-py-operation-remove-statement-by-value": "Individual command operation type displayed in command list",
"batchcommand-py-operation-remove-qualifier": "Individual command operation type displayed in command list",
Expand Down Expand Up @@ -201,4 +202,4 @@
"new-batch-file-upload": "Form title of file upload",
"new-batch-file-upload-targeting": "File upload explanation",
"login-token-expired": "Shown when the user is forcefully logged out and needs to log in again."
}
}
Loading