mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 17:39:06 +02:00
Renamed "devices" permission to "projects"
This commit is contained in:
parent
f2dfe12087
commit
7b6a906d98
19 changed files with 157 additions and 23 deletions
|
@ -93,9 +93,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
||||||
<<: *PART_CONTAINING
|
<<: *PART_CONTAINING
|
||||||
label: "perm.part.manufacturers"
|
label: "perm.part.manufacturers"
|
||||||
|
|
||||||
devices:
|
projects:
|
||||||
<<: *PART_CONTAINING
|
<<: *PART_CONTAINING
|
||||||
label: "perm.part.devices"
|
label: "perm.projects"
|
||||||
|
|
||||||
attachment_types:
|
attachment_types:
|
||||||
<<: *PART_CONTAINING
|
<<: *PART_CONTAINING
|
||||||
|
|
|
@ -80,7 +80,7 @@ class ProjectController extends AbstractController
|
||||||
if($project) {
|
if($project) {
|
||||||
$this->denyAccessUnlessGranted('edit', $project);
|
$this->denyAccessUnlessGranted('edit', $project);
|
||||||
} else {
|
} else {
|
||||||
$this->denyAccessUnlessGranted('@devices.edit');
|
$this->denyAccessUnlessGranted('@projects.edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder = $this->createFormBuilder();
|
$builder = $this->createFormBuilder();
|
||||||
|
|
|
@ -138,7 +138,7 @@ class TreeController extends AbstractController
|
||||||
*/
|
*/
|
||||||
public function deviceTree(?Project $device = null): JsonResponse
|
public function deviceTree(?Project $device = null): JsonResponse
|
||||||
{
|
{
|
||||||
if ($this->isGranted('@devices.read')) {
|
if ($this->isGranted('@projects.read')) {
|
||||||
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');
|
$tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices');
|
||||||
} else {
|
} else {
|
||||||
return new JsonResponse("Access denied", 403);
|
return new JsonResponse("Access denied", 403);
|
||||||
|
|
|
@ -72,7 +72,7 @@ class GroupFixtures extends Fixture
|
||||||
|
|
||||||
private function addDevicesPermissions(Group $group): void
|
private function addDevicesPermissions(Group $group): void
|
||||||
{
|
{
|
||||||
$this->permissionManager->setAllOperationsOfPermission($group, 'devices', true);
|
$this->permissionManager->setAllOperationsOfPermission($group, 'projects', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ trait ProjectTrait
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all devices which uses this part.
|
* Get all projects which uses this part.
|
||||||
*
|
*
|
||||||
* @return Project[] * all devices which uses this part as a one-dimensional array of Device objects
|
* @return Project[] * all devices which uses this part as a one-dimensional array of Device objects
|
||||||
* (empty array if there are no ones)
|
* (empty array if there are no ones)
|
||||||
|
|
|
@ -40,7 +40,7 @@ final class PermissionData implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* The current schema version of the permission data
|
* The current schema version of the permission data
|
||||||
*/
|
*/
|
||||||
public const CURRENT_SCHEMA_VERSION = 1;
|
public const CURRENT_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array This array contains the permission values for each permission
|
* @var array This array contains the permission values for each permission
|
||||||
|
@ -69,6 +69,56 @@ final class PermissionData implements \JsonSerializable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any of the operations of the given permission is defined (meaning it is either ALLOW or DENY)
|
||||||
|
* @param string $permission
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isAnyOperationOfPermissionSet(string $permission): bool
|
||||||
|
{
|
||||||
|
return !empty($this->data[$permission]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an associative array containing all defined (non-INHERIT) operations of the given permission.
|
||||||
|
* @param string $permission
|
||||||
|
* @return array An array in the form ["operation" => value], returns an empty array if no operations are defined
|
||||||
|
*/
|
||||||
|
public function getAllDefinedOperationsOfPermission(string $permission): array
|
||||||
|
{
|
||||||
|
if (empty($this->data[$permission])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data[$permission];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all operations of the given permission via the given array.
|
||||||
|
* The data is an array in the form [$operation => $value], all existing values will be overwritten/deleted.
|
||||||
|
* @param string $permission
|
||||||
|
* @param array $data
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setAllOperationsOfPermission(string $permission, array $data): self
|
||||||
|
{
|
||||||
|
$this->data[$permission] = $data;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a whole permission from the data including all operations (effectivly setting them to INHERIT)
|
||||||
|
* @param string $permission
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function removePermission(string $permission): self
|
||||||
|
{
|
||||||
|
unset($this->data[$permission]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a permission value is set for the given permission and operation (meaning there value is not inherit).
|
* Check if a permission value is set for the given permission and operation (meaning there value is not inherit).
|
||||||
* @param string $permission
|
* @param string $permission
|
||||||
|
|
|
@ -96,7 +96,7 @@ class ParameterVoter extends ExtendedVoter
|
||||||
} elseif ($subject instanceof CurrencyParameter) {
|
} elseif ($subject instanceof CurrencyParameter) {
|
||||||
$param = 'currencies';
|
$param = 'currencies';
|
||||||
} elseif ($subject instanceof ProjectParameter) {
|
} elseif ($subject instanceof ProjectParameter) {
|
||||||
$param = 'devices';
|
$param = 'projects';
|
||||||
} elseif ($subject instanceof FootprintParameter) {
|
} elseif ($subject instanceof FootprintParameter) {
|
||||||
$param = 'footprints';
|
$param = 'footprints';
|
||||||
} elseif ($subject instanceof GroupParameter) {
|
} elseif ($subject instanceof GroupParameter) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ class StructureVoter extends ExtendedVoter
|
||||||
protected const OBJ_PERM_MAP = [
|
protected const OBJ_PERM_MAP = [
|
||||||
AttachmentType::class => 'attachment_types',
|
AttachmentType::class => 'attachment_types',
|
||||||
Category::class => 'categories',
|
Category::class => 'categories',
|
||||||
Project::class => 'devices',
|
Project::class => 'projects',
|
||||||
Footprint::class => 'footprints',
|
Footprint::class => 'footprints',
|
||||||
Manufacturer::class => 'manufacturers',
|
Manufacturer::class => 'manufacturers',
|
||||||
Storelocation::class => 'storelocations',
|
Storelocation::class => 'storelocations',
|
||||||
|
|
|
@ -111,6 +111,7 @@ class PermissionPresetsHelper
|
||||||
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'currencies', PermissionData::ALLOW);
|
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'currencies', PermissionData::ALLOW);
|
||||||
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'measurement_units', PermissionData::ALLOW);
|
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'measurement_units', PermissionData::ALLOW);
|
||||||
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'suppliers', PermissionData::ALLOW);
|
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'suppliers', PermissionData::ALLOW);
|
||||||
|
$this->permissionResolver->setAllOperationsOfPermission($permHolder, 'projects', PermissionData::ALLOW);
|
||||||
|
|
||||||
//Attachments permissions
|
//Attachments permissions
|
||||||
$this->permissionResolver->setPermission($permHolder, 'attachments', 'show_private', PermissionData::ALLOW);
|
$this->permissionResolver->setPermission($permHolder, 'attachments', 'show_private', PermissionData::ALLOW);
|
||||||
|
@ -150,8 +151,8 @@ class PermissionPresetsHelper
|
||||||
$this->permissionResolver->setPermission($perm_holder, 'labels', 'edit_options', PermissionData::ALLOW);
|
$this->permissionResolver->setPermission($perm_holder, 'labels', 'edit_options', PermissionData::ALLOW);
|
||||||
$this->permissionResolver->setPermission($perm_holder, 'labels', 'read_profiles', PermissionData::ALLOW);
|
$this->permissionResolver->setPermission($perm_holder, 'labels', 'read_profiles', PermissionData::ALLOW);
|
||||||
|
|
||||||
//Set devices permissions
|
//Set projects permissions
|
||||||
$this->permissionResolver->setPermission($perm_holder, 'devices', 'read', PermissionData::ALLOW);
|
$this->permissionResolver->setPermission($perm_holder, 'projects', 'read', PermissionData::ALLOW);
|
||||||
|
|
||||||
return $perm_holder;
|
return $perm_holder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,4 +133,14 @@ class PermissionSchemaUpdater
|
||||||
$holder->getPermissions()->setPermissionValue('parts_stock', 'move', $new_value);
|
$holder->getPermissions()->setPermissionValue('parts_stock', 'move', $new_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function upgradeSchemaToVersion2(HasPermissionsInterface $holder): void
|
||||||
|
{
|
||||||
|
//If the projects permissions are not defined yet, rename devices permission to projects (just copy its data over)
|
||||||
|
if (!$holder->getPermissions()->isAnyOperationOfPermissionSet('projects')) {
|
||||||
|
$operations_value = $holder->getPermissions()->getAllDefinedOperationsOfPermission('devices');
|
||||||
|
$holder->getPermissions()->setAllOperationsOfPermission('projects', $operations_value);
|
||||||
|
$holder->getPermissions()->removePermission('devices');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -28,7 +28,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<a class="btn btn-success" {% if not is_granted('@devices.edit') %}disabled{% endif %}
|
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
|
||||||
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.requestUri}) }}">
|
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.requestUri}) }}">
|
||||||
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
|
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
|
||||||
{% trans %}part.info.add_part_to_project{% endtrans %}
|
{% trans %}part.info.add_part_to_project{% endtrans %}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
|
|
||||||
{{ dropdown.profile_dropdown('part', part.id) }}
|
{{ dropdown.profile_dropdown('part', part.id) }}
|
||||||
|
|
||||||
<a class="btn btn-success mt-2" {% if not is_granted('@devices.edit') %}disabled{% endif %}
|
<a class="btn btn-success mt-2" {% if not is_granted('@projects.edit') %}disabled{% endif %}
|
||||||
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.requestUri}) }}">
|
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.requestUri}) }}">
|
||||||
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
|
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
|
||||||
{% trans %}part.info.add_part_to_project{% endtrans %}
|
{% trans %}part.info.add_part_to_project{% endtrans %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }}
|
{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }}
|
||||||
|
|
||||||
<a class="btn btn-success" {% if not is_granted('@devices.edit') %}disabled{% endif %}
|
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
|
||||||
href="{{ path('project_add_parts', {"id": project.id, "_redirect": app.request.requestUri}) }}">
|
href="{{ path('project_add_parts', {"id": project.id, "_redirect": app.request.requestUri}) }}">
|
||||||
<i class="fa-solid fa-square-plus fa-fw"></i>
|
<i class="fa-solid fa-square-plus fa-fw"></i>
|
||||||
{% trans %}project.info.bom_add_parts{% endtrans %}
|
{% trans %}project.info.bom_add_parts{% endtrans %}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<option {% if not is_granted('@measurement_units.read') %}disabled{% endif %} value="change_unit" data-url="{{ path('select_measurement_unit') }}">{% trans %}part_list.action.action.change_unit{% endtrans %}</option>
|
<option {% if not is_granted('@measurement_units.read') %}disabled{% endif %} value="change_unit" data-url="{{ path('select_measurement_unit') }}">{% trans %}part_list.action.action.change_unit{% endtrans %}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="{% trans %}part_list.action.group.projects{% endtrans %}">
|
<optgroup label="{% trans %}part_list.action.group.projects{% endtrans %}">
|
||||||
<option {% if not is_granted('@devices.read') %}disabled{% endif %} value="add_to_project" data-url="{{ path('select_project')}}">{% trans %}part_list.action.projects.add_to_project{% endtrans %}</option>
|
<option {% if not is_granted('@projects.read') %}disabled{% endif %} value="add_to_project" data-url="{{ path('select_project')}}">{% trans %}part_list.action.projects.add_to_project{% endtrans %}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
|
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')],
|
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')],
|
||||||
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
|
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
|
||||||
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
|
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
|
||||||
['devices', path('tree_device_root'), 'project.labelp', is_granted('@devices.read')],
|
['devices', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
|
||||||
['tools', path('tree_tools'), 'tools.label', true],
|
['tools', path('tree_tools'), 'tools.label', true],
|
||||||
] %}
|
] %}
|
||||||
|
|
||||||
|
|
|
@ -158,4 +158,61 @@ class PermissionDataTest extends TestCase
|
||||||
$data->setSchemaVersion(12345);
|
$data->setSchemaVersion(12345);
|
||||||
$this->assertEquals(12345, $data->getSchemaVersion());
|
$this->assertEquals(12345, $data->getSchemaVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIsAnyOperationOfPermissionSet()
|
||||||
|
{
|
||||||
|
$data = new PermissionData();
|
||||||
|
|
||||||
|
//Initially no operation of any permission is set
|
||||||
|
$this->assertFalse($data->isAnyOperationOfPermissionSet('perm1'));
|
||||||
|
|
||||||
|
$data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW);
|
||||||
|
$this->assertTrue($data->isAnyOperationOfPermissionSet('perm1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllDefinedOperationsOfPermission()
|
||||||
|
{
|
||||||
|
$data = new PermissionData();
|
||||||
|
|
||||||
|
$this->assertEmpty($data->getAllDefinedOperationsOfPermission('perm1'));
|
||||||
|
|
||||||
|
$data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW);
|
||||||
|
$data->setPermissionValue('perm1', 'op2', PermissionData::DISALLOW);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'op1' => PermissionData::ALLOW, 'op2' => PermissionData::DISALLOW,
|
||||||
|
],
|
||||||
|
$data->getAllDefinedOperationsOfPermission('perm1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetAllOperationsOfPermission()
|
||||||
|
{
|
||||||
|
$data = new PermissionData();
|
||||||
|
|
||||||
|
$data->setAllOperationsOfPermission('perm1', [
|
||||||
|
'op1' => PermissionData::ALLOW,
|
||||||
|
'op2' => PermissionData::DISALLOW,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'op1' => PermissionData::ALLOW, 'op2' => PermissionData::DISALLOW,
|
||||||
|
],
|
||||||
|
$data->getAllDefinedOperationsOfPermission('perm1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRemovePermission()
|
||||||
|
{
|
||||||
|
$data = new PermissionData();
|
||||||
|
|
||||||
|
$data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW);
|
||||||
|
$data->setPermissionValue('perm1', 'op2', PermissionData::DISALLOW);
|
||||||
|
|
||||||
|
$this->assertTrue($data->isPermissionSet('perm1', 'op1'));
|
||||||
|
$this->assertTrue($data->isPermissionSet('perm1', 'op2'));
|
||||||
|
|
||||||
|
$data->removePermission('perm1');
|
||||||
|
|
||||||
|
$this->assertFalse($data->isPermissionSet('perm1', 'op1'));
|
||||||
|
$this->assertFalse($data->isPermissionSet('perm1', 'op2'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,4 +97,20 @@ class PermissionSchemaUpdaterTest extends WebTestCase
|
||||||
self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'add'));
|
self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'add'));
|
||||||
self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'withdraw'));
|
self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'withdraw'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUpgradeSchemaToVersion2()
|
||||||
|
{
|
||||||
|
$perm_data = new PermissionData();
|
||||||
|
$perm_data->setSchemaVersion(1);
|
||||||
|
$perm_data->setPermissionValue('devices', 'read', PermissionData::ALLOW);
|
||||||
|
$perm_data->setPermissionValue('devices', 'edit', PermissionData::INHERIT);
|
||||||
|
$perm_data->setPermissionValue('devices', 'delete', PermissionData::DISALLOW);
|
||||||
|
$user = new TestPermissionHolder($perm_data);
|
||||||
|
|
||||||
|
//After the upgrade all operations should be available under the name "projects" with the same values
|
||||||
|
self::assertTrue($this->service->upgradeSchema($user, 2));
|
||||||
|
self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('projects', 'read'));
|
||||||
|
self::assertEquals(PermissionData::INHERIT, $user->getPermissions()->getPermissionValue('projects', 'edit'));
|
||||||
|
self::assertEquals(PermissionData::DISALLOW, $user->getPermissions()->getPermissionValue('projects', 'delete'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8111,14 +8111,14 @@ Element 3</target>
|
||||||
<target>Hersteller</target>
|
<target>Hersteller</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
<unit id="5_k9ofl" name="perm.part.devices">
|
<unit id="5_k9ofl" name="perm.projects">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">obsolete</note>
|
<note priority="1">obsolete</note>
|
||||||
<note category="state" priority="1">obsolete</note>
|
<note category="state" priority="1">obsolete</note>
|
||||||
</notes>
|
</notes>
|
||||||
<segment state="translated">
|
<segment state="translated">
|
||||||
<source>perm.part.devices</source>
|
<source>perm.projects</source>
|
||||||
<target>Baugruppen</target>
|
<target>Projekte</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
<unit id="hgZrqP_" name="perm.part.attachment_types">
|
<unit id="hgZrqP_" name="perm.part.attachment_types">
|
||||||
|
|
|
@ -3284,7 +3284,7 @@ Sub elements will be moved upwards.]]></target>
|
||||||
</notes>
|
</notes>
|
||||||
<segment>
|
<segment>
|
||||||
<source>statistics.devices_count</source>
|
<source>statistics.devices_count</source>
|
||||||
<target>Number of devices</target>
|
<target>Number of projects</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
<unit id="8HKc.Yq" name="statistics.attachment_types_count">
|
<unit id="8HKc.Yq" name="statistics.attachment_types_count">
|
||||||
|
@ -8112,14 +8112,14 @@ Element 3</target>
|
||||||
<target>Manufacturers</target>
|
<target>Manufacturers</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
<unit id="5_k9ofl" name="perm.part.devices">
|
<unit id="5_k9ofl" name="perm.projects">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">obsolete</note>
|
<note priority="1">obsolete</note>
|
||||||
<note category="state" priority="1">obsolete</note>
|
<note category="state" priority="1">obsolete</note>
|
||||||
</notes>
|
</notes>
|
||||||
<segment>
|
<segment>
|
||||||
<source>perm.part.devices</source>
|
<source>perm.projects</source>
|
||||||
<target>Devices</target>
|
<target>Projects</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
<unit id="hgZrqP_" name="perm.part.attachment_types">
|
<unit id="hgZrqP_" name="perm.part.attachment_types">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue