Skip to content

Commit fce9731

Browse files
Paul Johnsonronaldbarendse
andauthored
Cherry-pick: support import/export for doc type history cleanup policy (#11708)
* cherry-pick 13a51d3 (V8 History cleanup import/export) Support import/export for doc type history cleanup policy (#11660) * Support import/export for doc type history cleanup policy * Support unset/null history cleanup value * Resolve issue when api endpoints called without cleanup policy. noop isn't good enough as map fails for response. * null conditional vs null coalesce assignment * Don't overwrite existing policy if omitted in import XML * Update history cleanup warning and translations * Change history cleanup alert to infomational styling * Remove margin around history cleanup config Co-authored-by: Ronald Barendse <[email protected]> # Conflicts: # src/Umbraco.Core/Models/IContentType.cs # src/Umbraco.Core/Packaging/PackageDataInstallation.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs # src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs # src/Umbraco.Tests/Packaging/PackageDataInstallationTests.cs # src/Umbraco.Tests/Services/Importing/ImportResources.Designer.cs # src/Umbraco.Tests/Umbraco.Tests.csproj # src/Umbraco.Web.UI/umbraco/config/lang/en.xml # src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml # src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs # tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx # tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/Importing/SingleDocType-WithCleanupPolicy.xml * Remove namespace aliases * Update IContentTypeWithHistoryCleanup documentation Co-authored-by: Ronald Barendse <[email protected]>
1 parent 08f106a commit fce9731

File tree

13 files changed

+324
-72
lines changed

13 files changed

+324
-72
lines changed

src/Umbraco.Core/Models/IContentType.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace Umbraco.Cms.Core.Models
66
{
7-
[Obsolete("This will be merged into IContentType in Umbraco 10")]
7+
/// <summary>
8+
/// Defines a content type that contains a history cleanup policy.
9+
/// </summary>
10+
[Obsolete("This will be merged into IContentType in Umbraco 10.")]
811
public interface IContentTypeWithHistoryCleanup : IContentType
912
{
1013
/// <summary>
11-
/// Gets or Sets the history cleanup configuration
14+
/// Gets or sets the history cleanup configuration.
1215
/// </summary>
16+
/// <value>The history cleanup configuration.</value>
1317
HistoryCleanup HistoryCleanup { get; set; }
1418
}
1519

src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,20 +185,15 @@ private void Map(IContentType source, DocumentTypeDisplay target, MapperContext
185185
{
186186
target.HistoryCleanup = new HistoryCleanupViewModel
187187
{
188-
PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup.PreventCleanup,
189-
KeepAllVersionsNewerThanDays =
190-
sourceWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays,
191-
KeepLatestVersionPerDayForDays =
192-
sourceWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays,
193-
GlobalKeepAllVersionsNewerThanDays =
194-
_contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays,
195-
GlobalKeepLatestVersionPerDayForDays =
196-
_contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays,
188+
PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false,
189+
KeepAllVersionsNewerThanDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays,
190+
KeepLatestVersionPerDayForDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays,
191+
GlobalKeepAllVersionsNewerThanDays = _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays,
192+
GlobalKeepLatestVersionPerDayForDays = _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays,
197193
GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup
198194
};
199195
}
200196

201-
202197
target.AllowCultureVariant = source.VariesByCulture();
203198
target.AllowSegmentVariant = source.VariesBySegment();
204199
target.ContentApps = _commonMapper.GetContentApps(source);

src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,9 +814,47 @@ private T UpdateContentTypeFromXml<T>(
814814
UpdateContentTypesPropertyGroups(contentType, documentType.Element("Tabs"));
815815
UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties"));
816816

817+
if (contentType is IContentTypeWithHistoryCleanup withCleanup)
818+
{
819+
UpdateHistoryCleanupPolicy(withCleanup, documentType.Element("HistoryCleanupPolicy"));
820+
}
821+
817822
return contentType;
818823
}
819824

825+
private void UpdateHistoryCleanupPolicy(IContentTypeWithHistoryCleanup withCleanup, XElement element)
826+
{
827+
if (element == null)
828+
{
829+
return;
830+
}
831+
832+
withCleanup.HistoryCleanup ??= new Core.Models.ContentEditing.HistoryCleanup();
833+
834+
if (bool.TryParse(element.Attribute("preventCleanup")?.Value, out var preventCleanup))
835+
{
836+
withCleanup.HistoryCleanup.PreventCleanup = preventCleanup;
837+
}
838+
839+
if (int.TryParse(element.Attribute("keepAllVersionsNewerThanDays")?.Value, out var keepAll))
840+
{
841+
withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = keepAll;
842+
}
843+
else
844+
{
845+
withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = null;
846+
}
847+
848+
if (int.TryParse(element.Attribute("keepLatestVersionPerDayForDays")?.Value, out var keepLatest))
849+
{
850+
withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = keepLatest;
851+
}
852+
else
853+
{
854+
withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = null;
855+
}
856+
}
857+
820858
private void UpdateContentTypesAllowedTemplates(IContentType contentType, XElement allowedTemplatesElement, XElement defaultTemplateElement)
821859
{
822860
if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any())

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,17 @@ protected override void PersistUpdatedItem(IContentType entity)
297297

298298
private void PersistHistoryCleanup(IContentType entity)
299299
{
300+
// historyCleanup property is not mandatory for api endpoint, handle the case where it's not present.
301+
// DocumentTypeSave doesn't handle this for us like ContentType constructors do.
300302
if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup)
301303
{
302304
ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto()
303305
{
304306
ContentTypeId = entity.Id,
305307
Updated = DateTime.Now,
306-
PreventCleanup = entityWithHistoryCleanup.HistoryCleanup.PreventCleanup,
307-
KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays,
308-
KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays
308+
PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false,
309+
KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays,
310+
KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays
309311
};
310312
Database.InsertOrUpdate(dto);
311313
}

src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Net;
66
using System.Xml.Linq;
77
using Umbraco.Cms.Core.Models;
8+
using Umbraco.Cms.Core.Models.ContentEditing;
89
using Umbraco.Cms.Core.PropertyEditors;
910
using Umbraco.Cms.Core.Serialization;
1011
using Umbraco.Cms.Core.Strings;
@@ -494,6 +495,11 @@ public XElement Serialize(IContentType contentType)
494495
genericProperties,
495496
tabs);
496497

498+
if (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null)
499+
{
500+
xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
501+
}
502+
497503
var folderNames = string.Empty;
498504
var folderKeys = string.Empty;
499505
//don't add folders if this is a child doc type
@@ -564,6 +570,29 @@ private XElement SerializePropertyType(IPropertyType propertyType, IDataType def
564570
propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
565571
propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null);
566572

573+
private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy)
574+
{
575+
if (cleanupPolicy == null)
576+
{
577+
throw new ArgumentNullException(nameof(cleanupPolicy));
578+
}
579+
580+
var element = new XElement("HistoryCleanupPolicy",
581+
new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
582+
583+
if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
584+
{
585+
element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
586+
}
587+
588+
if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
589+
{
590+
element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
591+
}
592+
593+
return element;
594+
}
595+
567596
// exports an IContentBase (IContent, IMedia or IMember) as an XElement.
568597
private XElement SerializeContentBase(IContentBase contentBase, string urlValue, string nodeName, bool published)
569598
{

src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,19 @@ <h5><localize key="contentTypeEditor_historyCleanupHeading"></localize></h5>
101101
</div>
102102

103103
<div class="sub-view-column-right">
104-
<umb-box-content>
105-
<div class="umb-panel-group__details-status-text" ng-if="!model.historyCleanup.globalEnableCleanup">
106-
<p class="umb-panel-group__details-status-action"><localize key="contentTypeEditor_historyCleanupGloballyDisabled"></localize></p>
107-
</div>
108-
109-
<umb-control-group label="@contentTypeEditor_historyCleanupPreventCleanup">
110-
<umb-toggle checked="model.historyCleanup.preventCleanup" on-click="vm.toggleHistoryCleanupPreventCleanup()"></umb-toggle>
111-
</umb-control-group>
112-
113-
<umb-control-group label="@contentTypeEditor_historyCleanupKeepAllVersionsNewerThanDays">
114-
<input ng-readonly="model.historyCleanup.preventCleanup" placeholder="{{model.historyCleanup.globalKeepAllVersionsNewerThanDays}}" type="number" min="0" max="2147483647" ng-model="model.historyCleanup.keepAllVersionsNewerThanDays" />
115-
</umb-control-group>
116-
117-
<umb-control-group label="@contentTypeEditor_historyCleanupKeepLatestVersionPerDayForDays">
118-
<input ng-readonly="model.historyCleanup.preventCleanup" placeholder="{{model.historyCleanup.globalKeepLatestVersionPerDayForDays}}" type="number" min="0" max="2147483647" ng-model="model.historyCleanup.keepLatestVersionPerDayForDays" />
119-
</umb-control-group>
120-
</umb-box-content>
104+
<p ng-if="!model.historyCleanup.globalEnableCleanup" class="umb-alert umb-alert--info"><localize key="contentTypeEditor_historyCleanupGloballyDisabled"></localize></p>
105+
106+
<umb-control-group label="@contentTypeEditor_historyCleanupPreventCleanup">
107+
<umb-toggle checked="model.historyCleanup.preventCleanup" on-click="vm.toggleHistoryCleanupPreventCleanup()"></umb-toggle>
108+
</umb-control-group>
109+
110+
<umb-control-group label="@contentTypeEditor_historyCleanupKeepAllVersionsNewerThanDays">
111+
<input ng-readonly="model.historyCleanup.preventCleanup" placeholder="{{model.historyCleanup.globalKeepAllVersionsNewerThanDays}}" type="number" min="0" max="2147483647" ng-model="model.historyCleanup.keepAllVersionsNewerThanDays" />
112+
</umb-control-group>
113+
114+
<umb-control-group label="@contentTypeEditor_historyCleanupKeepLatestVersionPerDayForDays">
115+
<input ng-readonly="model.historyCleanup.preventCleanup" placeholder="{{model.historyCleanup.globalKeepLatestVersionPerDayForDays}}" type="number" min="0" max="2147483647" ng-model="model.historyCleanup.keepLatestVersionPerDayForDays" />
116+
</umb-control-group>
121117
</div>
122118

123119
</div>

src/Umbraco.Web.UI/umbraco/config/lang/en.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,11 +1833,11 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
18331833
</key>
18341834
<key alias="usingEditor">using this editor will get updated with the new settings.</key>
18351835
<key alias="historyCleanupHeading">History cleanup</key>
1836-
<key alias="historyCleanupDescription">Allow override the global settings for when history versions are removed.</key>
1836+
<key alias="historyCleanupDescription">Allow overriding the global history cleanup settings.</key>
18371837
<key alias="historyCleanupKeepAllVersionsNewerThanDays">Keep all versions newer than days</key>
18381838
<key alias="historyCleanupKeepLatestVersionPerDayForDays">Keep latest version per day for days</key>
18391839
<key alias="historyCleanupPreventCleanup">Prevent cleanup</key>
1840-
<key alias="historyCleanupGloballyDisabled">NOTE! The content version cleanup feature is disabled globally. These settings will not take effect until it is enabled.</key>
1840+
<key alias="historyCleanupGloballyDisabled">History cleanup is disabled globally, these settings will not take effect until it is enabled.</key>
18411841
</area>
18421842
<area alias="languages">
18431843
<key alias="addLanguage">Add language</key>

src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,11 +1897,11 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
18971897
</key>
18981898
<key alias="usingEditor">using this editor will get updated with the new settings.</key>
18991899
<key alias="historyCleanupHeading">History cleanup</key>
1900-
<key alias="historyCleanupDescription">Allow override the global settings for when history versions are removed.</key>
1900+
<key alias="historyCleanupDescription">Allow overriding the global history cleanup settings.</key>
19011901
<key alias="historyCleanupKeepAllVersionsNewerThanDays">Keep all versions newer than days</key>
19021902
<key alias="historyCleanupKeepLatestVersionPerDayForDays">Keep latest version per day for days</key>
19031903
<key alias="historyCleanupPreventCleanup">Prevent cleanup</key>
1904-
<key alias="historyCleanupGloballyDisabled">NOTE! The content version cleanup feature is disabled globally. These settings will not take effect until it is enabled.</key>
1904+
<key alias="historyCleanupGloballyDisabled">History cleanup is disabled globally, these settings will not take effect until it is enabled.</key>
19051905
</area>
19061906
<area alias="languages">
19071907
<key alias="addLanguage">Add language</key>

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/PackageDataInstallationTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,77 @@ public void Can_Import_Package_With_Compositions_Ordered()
766766
Assert.That(testContentType.ContentTypeCompositionExists("Seo"), Is.True);
767767
}
768768

769+
[Test]
770+
public void ImportDocumentType_NewTypeWithOmittedHistoryCleanupPolicy_InsertsDefaultPolicy()
771+
{
772+
// Arrange
773+
var withoutCleanupPolicy = XElement.Parse(ImportResources.SingleDocType);
774+
775+
// Act
776+
var contentTypes = PackageDataInstallation
777+
.ImportDocumentType(withoutCleanupPolicy, 0)
778+
.OfType<IContentTypeWithHistoryCleanup>();
779+
780+
// Assert
781+
Assert.Multiple(() =>
782+
{
783+
Assert.NotNull(contentTypes.Single().HistoryCleanup);
784+
Assert.IsFalse(contentTypes.Single().HistoryCleanup.PreventCleanup);
785+
});
786+
}
787+
788+
[Test]
789+
public void ImportDocumentType_WithHistoryCleanupPolicyElement_ImportsWithCorrectValues()
790+
{
791+
// Arrange
792+
var docTypeElement = XElement.Parse(ImportResources.SingleDocType_WithCleanupPolicy);
793+
794+
// Act
795+
var contentTypes = PackageDataInstallation
796+
.ImportDocumentType(docTypeElement, 0)
797+
.OfType<IContentTypeWithHistoryCleanup>();
798+
799+
// Assert
800+
Assert.Multiple(() =>
801+
{
802+
Assert.NotNull(contentTypes.Single().HistoryCleanup);
803+
Assert.IsTrue(contentTypes.Single().HistoryCleanup.PreventCleanup);
804+
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
805+
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
806+
});
807+
}
808+
809+
[Test]
810+
public void ImportDocumentType_ExistingTypeWithOmittedHistoryCleanupPolicy_DoesNotOverwriteDatabaseContent()
811+
{
812+
// Arrange
813+
var withoutCleanupPolicy = XElement.Parse(ImportResources.SingleDocType);
814+
var withCleanupPolicy = XElement.Parse(ImportResources.SingleDocType_WithCleanupPolicy);
815+
816+
// Act
817+
var contentTypes = PackageDataInstallation
818+
.ImportDocumentType(withCleanupPolicy, 0)
819+
.OfType<IContentTypeWithHistoryCleanup>();
820+
821+
var contentTypesUpdated = PackageDataInstallation
822+
.ImportDocumentType(withoutCleanupPolicy, 0)
823+
.OfType<IContentTypeWithHistoryCleanup>();
824+
825+
// Assert
826+
Assert.Multiple(() =>
827+
{
828+
Assert.NotNull(contentTypes.Single().HistoryCleanup);
829+
Assert.IsTrue(contentTypes.Single().HistoryCleanup.PreventCleanup);
830+
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
831+
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
832+
833+
Assert.NotNull(contentTypesUpdated.Single().HistoryCleanup);
834+
Assert.IsTrue(contentTypesUpdated.Single().HistoryCleanup.PreventCleanup);
835+
Assert.AreEqual(1, contentTypes.Single().HistoryCleanup.KeepAllVersionsNewerThanDays);
836+
Assert.AreEqual(2, contentTypes.Single().HistoryCleanup.KeepLatestVersionPerDayForDays);
837+
});
838+
}
839+
769840
private void AddLanguages()
770841
{
771842
var globalSettings = new GlobalSettings();

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityXmlSerializerTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using Microsoft.Extensions.Options;
2626
using Microsoft.Extensions.DependencyInjection;
2727
using Umbraco.Cms.Core.Media;
28+
using Umbraco.Cms.Core.Models.ContentEditing;
2829
using Umbraco.Cms.Core.Strings;
2930

3031
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
@@ -225,6 +226,58 @@ public void Can_Generate_Xml_Representation_Of_Media()
225226
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Bytes].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Bytes).Single().Value);
226227
Assert.AreEqual(media.Properties[Constants.Conventions.Media.Extension].GetValue().ToString(), element.Elements(Constants.Conventions.Media.Extension).Single().Value);
227228
}
229+
230+
[Test]
231+
public void Serialize_ForContentTypeWithHistoryCleanupPolicy_OutputsSerializedHistoryCleanupPolicy()
232+
{
233+
// Arrange
234+
var template = TemplateBuilder.CreateTextPageTemplate();
235+
FileService.SaveTemplate(template); // else, FK violation on contentType!
236+
237+
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
238+
239+
contentType.HistoryCleanup = new HistoryCleanup
240+
{
241+
PreventCleanup = true,
242+
KeepAllVersionsNewerThanDays = 1,
243+
KeepLatestVersionPerDayForDays = 2
244+
};
245+
246+
ContentTypeService.Save(contentType);
247+
248+
// Act
249+
var element = Serializer.Serialize(contentType);
250+
251+
// Assert
252+
Assert.Multiple(() =>
253+
{
254+
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("preventCleanup")!.Value, Is.EqualTo("true"));
255+
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("keepAllVersionsNewerThanDays")!.Value, Is.EqualTo("1"));
256+
Assert.That(element.Element("HistoryCleanupPolicy")!.Attribute("keepLatestVersionPerDayForDays")!.Value, Is.EqualTo("2"));
257+
});
258+
}
259+
260+
[Test]
261+
public void Serialize_ForContentTypeWithNullHistoryCleanupPolicy_DoesNotOutputSerializedDefaultPolicy()
262+
{
263+
// Arrange
264+
var template = TemplateBuilder.CreateTextPageTemplate();
265+
FileService.SaveTemplate(template); // else, FK violation on contentType!
266+
267+
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
268+
269+
contentType.HistoryCleanup = null;
270+
271+
ContentTypeService.Save(contentType);
272+
273+
var element = Serializer.Serialize(contentType);
274+
275+
// Assert
276+
Assert.Multiple(() =>
277+
{
278+
Assert.That(element.Element("HistoryCleanupPolicy"), Is.Null);
279+
});
280+
}
228281

229282
private void CreateDictionaryData()
230283
{

0 commit comments

Comments
 (0)