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/0042_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-13 17:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0041_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'), ('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),
),
]
32 changes: 30 additions & 2 deletions src/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,10 @@ class Operation(models.TextChoices):
"switch_statement_value",
pgettext_lazy("batchcommand-py-operation-switch-statement-value", "Switch statement value"),
)
SWITCH_STATEMENT_PROPERTY = (
"switch_statement_property",
pgettext_lazy("batchcommand-py-operation-switch-statement-property", "Switch statement property"),
)
REMOVE_STATEMENT_BY_ID = (
"remove_statement_by_id",
pgettext_lazy(
Expand Down Expand Up @@ -1361,9 +1365,15 @@ def is_add(self):
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()

def is_switch_value(self):
return self.operation == self.Operation.SWITCH_STATEMENT_VALUE

def is_switch_property(self):
return self.operation == self.Operation.SWITCH_STATEMENT_PROPERTY

def is_add_label_description_alias(self):
return self.is_add() and self.what in ["DESCRIPTION", "LABEL", "ALIAS"]

Expand Down Expand Up @@ -1512,6 +1522,7 @@ def operation_is_combinable(self):
self.Operation.CREATE_STATEMENT,
self.Operation.CREATE_PROPERTY,
self.Operation.SWITCH_STATEMENT_VALUE,
self.Operation.SWITCH_STATEMENT_PROPERTY,
self.Operation.REMOVE_STATEMENT_BY_VALUE,
self.Operation.REMOVE_QUALIFIER,
self.Operation.REMOVE_REFERENCE,
Expand Down Expand Up @@ -1657,6 +1668,8 @@ def update_entity_json(self, entity: dict):
self._remove_entity_statement(entity)
elif self.operation == self.Operation.SWITCH_STATEMENT_VALUE:
self._switch_statement_value(entity)
elif self.operation == self.Operation.SWITCH_STATEMENT_PROPERTY:
self._switch_statement_property(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 @@ -1781,6 +1794,19 @@ def _switch_statement_value(self, entity: dict):
return
raise NoStatementsWithThatValue(self.entity_id, self.prop, self.statement_api_value)

def _switch_statement_property(self, entity: dict):
"""
Switches a statement property
"""
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}
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 Expand Up @@ -1926,6 +1952,8 @@ def property_and_value_types_to_verify(self):
to_verify.append((p["property"], p["value"]["type"]))
if self.json.get("value_switch"):
to_verify.append((self.prop, self.json["value_switch"]["type"]))
if self.json.get("property_switch"):
to_verify.append((self.json["property_switch"], self.value_type))
return to_verify

def should_verify_value_types(self):
Expand All @@ -1937,8 +1965,8 @@ def should_verify_value_types(self):
2) It needs if it is of the following types/actions:

- Statement addition
- Statement value switch
- Statement value or property switch
"""
is_not_verified_yet = not self.value_type_verified
is_needed_actions = self.is_add_statement() or self.is_switch_value()
is_needed_actions = self.is_add_statement() or self.is_switch()
return is_not_verified_yet and is_needed_actions
20 changes: 20 additions & 0 deletions src/core/parsers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ def parse_switch_value(self, elements):
data["operation"] = BatchCommand.Operation.SWITCH_STATEMENT_VALUE
return data

def parse_switch_property(self, elements):
llen = len(elements)
if llen != 5:
raise ParserException("SWITCH_PROPERTY command must be QID|PID|value|new_PID")
elements.pop(0)

property_switch = elements.pop(3)
if not self.is_valid_property_id(property_switch):
raise ParserException(f"Invalid property '{property_switch}'")

data = self.parse_statement(elements, elements[0].upper())
data["action"] = "switch"
data["what"] = "property"
data["property_switch"] = property_switch
data["operation"] = BatchCommand.Operation.SWITCH_STATEMENT_PROPERTY
return data

def parse_statement(self, elements, first_command):
llen = len(elements)
if llen < 3:
Expand Down Expand Up @@ -315,6 +332,9 @@ def parse_command(self, raw_command):
elif first_command == "SWITCH_VALUE":
logger.debug(f"parsing switch value: {elements}")
data = self.parse_switch_value(elements)
elif first_command == "SWITCH_PROPERTY":
logger.debug(f"parsing switch property: {elements}")
data = self.parse_switch_property(elements)
else:
logger.debug(f"parsing statement: {elements}/{first_command}")
data = self.parse_statement(elements, first_command)
Expand Down
2 changes: 1 addition & 1 deletion src/core/templatetags/quickstatements.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def datavalue_display(command, datavalue):

@register.simple_tag
def entity_display(command, entity_id):
return mark_safe(render_entity_datavalue(command, entity_id))
return mark_safe(render_entity_datavalue(command, entity_id)) if entity_id else ""


@register.simple_tag
Expand Down
46 changes: 46 additions & 0 deletions src/core/tests/test_batch_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,3 +954,49 @@ def test_value_switch_data_type(self, mocker):
self.assertEqual(commands[2].status, BatchCommand.STATUS_DONE)
self.assertIsNone(commands[2].message)
self.assertIsNone(commands[2].error)

@requests_mock.Mocker()
def test_property_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|Q1|P5|Q12|P7
SWITCH_PROPERTY|Q1|P5|Q12|P6
"""
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)
69 changes: 68 additions & 1 deletion src/core/tests/test_entity_patching.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def parse(self, text):
return batch

def assertStmtnCount(self, entity: dict, property_id: str, length: int):
self.assertEqual(len(entity["statements"][property_id]), length)
self.assertEqual(len(entity["statements"].get(property_id, [])), length)

def assertQualCount(self, entity: dict, property_id: str, length: int, i: int = 0):
quals = entity["statements"][property_id][i]["qualifiers"]
Expand Down Expand Up @@ -544,3 +544,70 @@ def test_switch_value(self):
entity["statements"]["P31"][0]["references"],
self.INITIAL["statements"]["P31"][0]["references"]
)

def test_switch_property(self):
text = """
SWITCH_PROPERTY|Q12345678|P1|42|P99 /* wrong property */
SWITCH_PROPERTY|Q12345678|P65|1111|P99 /* wrong value */
SWITCH_PROPERTY|Q12345678|P65|42|P99
SWITCH_VALUE|Q12345678|P99|42|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"],
self.INITIAL["statements"]["P65"][0]["value"],
)
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"]
)
# -----
switch = batch.commands()[3]
switch.update_entity_json(entity)
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"]
)
4 changes: 2 additions & 2 deletions src/web/templates/batch_commands.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@
{% endfor %}
</div>
{% endif %}
{% if command.is_switch_value %}
{% if command.is_switch %}
<span class="header"></span>
<div>
<span class="property"></span>
<span class="property">{% entity_display command command.json.property_switch %}</span>
<span class="value">{% datavalue_display command command.json.value_switch %}</span>
</div>
{% endif %}
Expand Down
4 changes: 3 additions & 1 deletion translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"batchcommand-py-operation-set-statement": "Set statement",
"batchcommand-py-operation-create-statement": "Create statement",
"batchcommand-py-operation-switch-statement-value": "Switch statement value",
"batchcommand-py-operation-switch-statement-property": "Switch statement property",
"batchcommand-py-operation-remove-statement-by-id": "Remove statement by id",
"batchcommand-py-operation-remove-statement-by-value": "Remove statement by value",
"batchcommand-py-operation-remove-qualifier": "Remove qualifier",
Expand Down Expand Up @@ -213,5 +214,6 @@
"batch-button-rerun-uncombined": "Rerun uncombined",
"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."
"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"
}
1 change: 1 addition & 0 deletions translations/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"batchcommand-py-operation-set-statement": "Individual command operation type displayed in command list",
"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-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
Loading