Refactor: Separate concerns.
This commit is contained in:
39
zaaReloaded2/Importer/IImporter.cs
Executable file
39
zaaReloaded2/Importer/IImporter.cs
Executable file
@ -0,0 +1,39 @@
|
||||
/* IImporter.cs
|
||||
* part of zaaReloaded2
|
||||
*
|
||||
* Copyright 2015 Daniel Kraus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using zaaReloaded2.LabModel;
|
||||
|
||||
namespace zaaReloaded2.Importer
|
||||
{
|
||||
public interface IImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Laboratory resulting from the data import.
|
||||
/// </summary>
|
||||
Laboratory Laboratory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Imports laboratory data contained in a string.
|
||||
/// </summary>
|
||||
/// <param name="text">String with laboratory data.</param>
|
||||
void Import(string text);
|
||||
}
|
||||
}
|
203
zaaReloaded2/Importer/ZaaImporter/LaurisItem.cs
Executable file
203
zaaReloaded2/Importer/ZaaImporter/LaurisItem.cs
Executable file
@ -0,0 +1,203 @@
|
||||
/* LabItem.cs
|
||||
* part of zaaReloaded2
|
||||
*
|
||||
* Copyright 2015 Daniel Kraus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using zaaReloaded2.LabModel;
|
||||
|
||||
namespace zaaReloaded2.Importer.ZaaImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single laboratory item (e.g., sodium or creatinine).
|
||||
/// </summary>
|
||||
public class LaurisItem : LabItem
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The original Lauris string from which this lab item was created.
|
||||
/// </summary>
|
||||
public string LaurisText { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original name of this item as known by Lauris
|
||||
/// </summary>
|
||||
public string OriginalName { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty LabItem object.
|
||||
/// </summary>
|
||||
public LaurisItem() : base() { }
|
||||
|
||||
public LaurisItem(string laurisString)
|
||||
: this()
|
||||
{
|
||||
LaurisText = laurisString;
|
||||
ParseLauris();
|
||||
DetectMaterial();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a LabItem object from a given Lauris output, using
|
||||
/// a <see cref="ParameterDictionary"/> too look up additional
|
||||
/// properties (canonical name, material type, whether or not
|
||||
/// to always print the reference interval).
|
||||
/// </summary>
|
||||
/// <param name="laurisString">Lauris output to parse.</param>
|
||||
/// <param name="parameterDictionary">ParameterDictionary that is used
|
||||
/// to look up the canonical name, material type, and whether or
|
||||
/// not to always print the reference interval</param>
|
||||
public LaurisItem(string laurisString,
|
||||
Dictionaries.ParameterDictionary parameterDictionary,
|
||||
Dictionaries.UnitDictionary unitDictionary)
|
||||
: this(laurisString)
|
||||
{
|
||||
if (parameterDictionary != null)
|
||||
{
|
||||
Name = parameterDictionary.GetCanonicalName(OriginalName);
|
||||
AlwaysPrintLimits = parameterDictionary.GetForceReferenceDisplay(Name);
|
||||
}
|
||||
if (unitDictionary != null)
|
||||
{
|
||||
Unit = unitDictionary.TranslateLaurisUnit(Unit);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses the original Lauris string contained in
|
||||
/// <see cref="LaurisText"/>.
|
||||
/// </summary>
|
||||
void ParseLauris()
|
||||
{
|
||||
// Examples of Lauris output strings:
|
||||
// "Natrium: 139 [135 - 145] mmol/l"
|
||||
// "HDL - Cholesterin: 45 [>= 35] mg/dl"
|
||||
// "GOT (ASAT): 303.0 [<= 50] U/l; "
|
||||
// "Niedermol. Heparin (Anti-Xa): 0.99 U/ml;"
|
||||
// "HBs-Antigen: neg. ;"
|
||||
// "Erythrozyten (U): + [negativ]"
|
||||
Match match;
|
||||
Regex numericalRegex = new Regex(
|
||||
@"(?<name>[^:]+):\s*(?<value>[\d.]+)\s*(?<limits>\[[^\]]+])?\s*(?<unit>[^;]+)?");
|
||||
Regex categoricalRegex = new Regex(
|
||||
@"(?<name>[^:]+):\s*(?<value>[^[;]+)\s*(\[(?<normal>[^\]]+)])?");
|
||||
if (numericalRegex.IsMatch(LaurisText))
|
||||
{
|
||||
match = numericalRegex.Match(LaurisText);
|
||||
ParseLimits(match);
|
||||
}
|
||||
else
|
||||
{
|
||||
match = categoricalRegex.Match(LaurisText);
|
||||
Normal = match.Groups["normal"].Value.Trim();
|
||||
}
|
||||
if (match != null)
|
||||
{
|
||||
OriginalName = match.Groups["name"].Value.Trim();
|
||||
Name = OriginalName;
|
||||
Value = match.Groups["value"].Value.Trim();
|
||||
Unit = match.Groups["unit"].Value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string containing value limits. The string must be like
|
||||
/// "[3.5 - 5]", "[>= 50]", or "[<= 100]".
|
||||
/// </summary>
|
||||
/// <param name="match">Match object that should contain a group "limits".</param>
|
||||
void ParseLimits(Match match)
|
||||
{
|
||||
if (match.Groups["limits"].Success)
|
||||
{
|
||||
Regex limitRegex = new Regex(@"\[(?<limit1>[\d.]+)?\s*(?<operator>\S+)\s*(?<limit2>[\d.]+)?]");
|
||||
Match limitMatch = limitRegex.Match(match.Groups["limits"].Value);
|
||||
if (limitMatch.Groups["limit1"].Success && limitMatch.Groups["limit2"].Success)
|
||||
{
|
||||
// Use InvariantCulture because Lauris always outputs dots as decimal separator
|
||||
LowerLimit = Double.Parse(limitMatch.Groups["limit1"].Value,
|
||||
CultureInfo.InvariantCulture);
|
||||
UpperLimit = Double.Parse(limitMatch.Groups["limit2"].Value,
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (limitMatch.Groups["operator"].Value.Trim())
|
||||
{
|
||||
case "<=":
|
||||
UpperLimit = Double.Parse(limitMatch.Groups["limit2"].Value,
|
||||
CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case ">=":
|
||||
LowerLimit = Double.Parse(limitMatch.Groups["limit2"].Value,
|
||||
CultureInfo.InvariantCulture);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
String.Format("Unknown operator in {0}",
|
||||
match.Groups["limits"].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyses the Lauris name for a material abbreviation.
|
||||
/// If the parameter does not refer to blood (serum, whole
|
||||
/// blood, etc.), Lauris appends an abbreviation in parentheses
|
||||
/// to the parameter name.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// Gesamt-Eiweiss (SU), Albumin (SU)/die, Gesamt-Eiweiss (PU)
|
||||
/// </example>
|
||||
void DetectMaterial()
|
||||
{
|
||||
// The material is encoded in the original name of the item
|
||||
// that was produced by Lauris (eg. "Natrium (PU)" for spot
|
||||
// urine).
|
||||
Match m = _materialRegex.Match(OriginalName);
|
||||
if (m.Success)
|
||||
{
|
||||
switch (m.Groups["material"].Value.ToUpper())
|
||||
{
|
||||
case "SU":
|
||||
Material = LabModel.Material.SU;
|
||||
break;
|
||||
case "PU":
|
||||
Material = LabModel.Material.U;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
static readonly Regex _materialRegex = new Regex(@"\((?<material>(SU|PU))\)");
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
131
zaaReloaded2/Importer/ZaaImporter/LaurisParagraph.cs
Executable file
131
zaaReloaded2/Importer/ZaaImporter/LaurisParagraph.cs
Executable file
@ -0,0 +1,131 @@
|
||||
/* LaurisParagraph.cs
|
||||
* part of zaaReloaded2
|
||||
*
|
||||
* Copyright 2015 Daniel Kraus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using zaaReloaded2.Dictionaries;
|
||||
using zaaReloaded2.LabModel;
|
||||
|
||||
namespace zaaReloaded2.Importer.ZaaImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses an entire Lauris paragraph (such as "Klinische Chemie: ...")
|
||||
/// and creates a list of <see cref="LabItem"/>s.
|
||||
/// </summary>
|
||||
public class LaurisParagraph
|
||||
{
|
||||
#region Public properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="LabItem"/>s found in this paragraph.
|
||||
/// </summary>
|
||||
public IItemDictionary Items { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the caption that was extracted from the <see cref="OriginalParagraph"/>,
|
||||
/// e.g. "Klin. Chemie" in "Klin. Chemie: Natrium ...".
|
||||
/// </summary>
|
||||
public string Caption { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original paragraph that this object was constructed from.
|
||||
/// </summary>
|
||||
public string OriginalParagraph { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is true if the <see cref="OriginalParagraph"/> matches the expected
|
||||
/// format and contains <see cref="LabItem"/>s.
|
||||
/// </summary>
|
||||
public bool IsLaurisParagraph { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public LaurisParagraph(string paragraph)
|
||||
{
|
||||
OriginalParagraph = paragraph;
|
||||
Parse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="LaurisParagraph"/> object from a given
|
||||
/// Lauris paragraph, using a <paramref name="parameterDictionary"/>
|
||||
/// and a <paramref name="unitDictionary"/> to translate the individual
|
||||
/// items' properties.
|
||||
/// </summary>
|
||||
/// <param name="paragraph">lauris paragraph to parse.</param>
|
||||
/// <param name="parameterDictionary">ParameterDictionary that contains
|
||||
/// canonical names and material types.</param>
|
||||
/// <param name="unitDictionary">Unit dictionary that contains canonical
|
||||
/// unit names.</param>
|
||||
public LaurisParagraph(string paragraph,
|
||||
Dictionaries.ParameterDictionary parameterDictionary,
|
||||
Dictionaries.UnitDictionary unitDictionary)
|
||||
{
|
||||
OriginalParagraph = paragraph;
|
||||
_parameterDictionary = parameterDictionary;
|
||||
_unitDictionary = unitDictionary;
|
||||
Parse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a Lauris paragraph.
|
||||
/// </summary>
|
||||
void Parse()
|
||||
{
|
||||
Match m = _expectedFormat.Match(OriginalParagraph);
|
||||
if (m.Success)
|
||||
{
|
||||
Items = new ItemDictionary();
|
||||
if (m.Groups["caption"].Success)
|
||||
{
|
||||
Caption = m.Groups["caption"].Value.Trim(new char[] {' ', ':'});
|
||||
}
|
||||
|
||||
foreach (Capture itemCapture in m.Groups["items"].Captures)
|
||||
{
|
||||
LaurisItem i = new LaurisItem(itemCapture.Value, _parameterDictionary, _unitDictionary);
|
||||
Items.Add(i.QualifiedName, i);
|
||||
}
|
||||
IsLaurisParagraph = Items.Count > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsLaurisParagraph = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
static readonly Regex _expectedFormat = new Regex(@"(?<caption>[^:]+:\s*)?(?<items>[^:]+:\s*[^;]+;)*");
|
||||
Dictionaries.ParameterDictionary _parameterDictionary;
|
||||
Dictionaries.UnitDictionary _unitDictionary;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
179
zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs
Executable file
179
zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs
Executable file
@ -0,0 +1,179 @@
|
||||
/* LaurisTimePoint.cs
|
||||
* part of zaaReloaded2
|
||||
*
|
||||
* Copyright 2015 Daniel Kraus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using zaaReloaded2.Dictionaries;
|
||||
using zaaReloaded2.LabModel;
|
||||
|
||||
namespace zaaReloaded2.Importer.ZaaImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds all laboratory items for a given time point.
|
||||
/// </summary>
|
||||
class LaurisTimePoint : TimePoint
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets an array of paragraphs in this LaurisText.
|
||||
/// </summary>
|
||||
public string[] Paragraphs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is true if the LaurisText has time stamp in the first
|
||||
/// paragraph and <see cref="LabItem"/>s in the others.
|
||||
/// </summary>
|
||||
public bool IsValidTimePoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the original Lauris text for this timepoint.
|
||||
/// </summary>
|
||||
public string LaurisText
|
||||
{
|
||||
[DebuggerStepThrough]
|
||||
get
|
||||
{
|
||||
return String.Join(Environment.NewLine, Paragraphs);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!String.IsNullOrEmpty(value))
|
||||
{
|
||||
Paragraphs = value.Split(
|
||||
new string[] { Environment.NewLine },
|
||||
StringSplitOptions.None);
|
||||
ParseParagraphs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public LaurisTimePoint() { }
|
||||
|
||||
public LaurisTimePoint(string laurisTest)
|
||||
: this()
|
||||
{
|
||||
_parameterDictionary = null;
|
||||
_unitDictionary = null;
|
||||
LaurisText = laurisTest;
|
||||
}
|
||||
|
||||
public LaurisTimePoint(
|
||||
string laurisTest,
|
||||
ParameterDictionary parameterDictionary,
|
||||
UnitDictionary unitDictionary)
|
||||
: this()
|
||||
{
|
||||
_parameterDictionary = parameterDictionary;
|
||||
_unitDictionary = unitDictionary;
|
||||
LaurisText = laurisTest;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes each Lauris paragraph in this time point, sets the date
|
||||
/// and time, and collects LabItem data.
|
||||
/// </summary>
|
||||
/// <returns>True if the LaurisText has time stamp in the first paragraphs
|
||||
/// and contains <see cref="LabItem"/>s in the others.</returns>
|
||||
bool ParseParagraphs()
|
||||
{
|
||||
Items = new ItemDictionary();
|
||||
if (Paragraphs.Length > 0)
|
||||
{
|
||||
if (!ParseTimeStamp()) return false;
|
||||
LaurisParagraph lp;
|
||||
if (IsValidTimePoint)
|
||||
{
|
||||
for (int i = 1; i < Paragraphs.Length; i++)
|
||||
{
|
||||
lp = new LaurisParagraph(
|
||||
Paragraphs[i],
|
||||
_parameterDictionary,
|
||||
_unitDictionary);
|
||||
if (lp.IsLaurisParagraph)
|
||||
{
|
||||
Items.Merge(lp.Items);
|
||||
}
|
||||
}
|
||||
}
|
||||
IsValidTimePoint = Items.Count > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes the date and time information that is expected to be
|
||||
/// in the first paragraph.
|
||||
/// </summary>
|
||||
/// <returns>True if the LaurisText contains a time stamp in the
|
||||
/// first paragraph.</returns>
|
||||
bool ParseTimeStamp()
|
||||
{
|
||||
if (Paragraphs.Length == 0)
|
||||
throw new InvalidOperationException("The time point has no paragraphs.");
|
||||
|
||||
Match m = _dateStampRegex.Match(Paragraphs[0]);
|
||||
bool success = false;
|
||||
if (m.Success)
|
||||
{
|
||||
DateTime dt;
|
||||
success = DateTime.TryParseExact(
|
||||
m.Groups["datetime"].Value,
|
||||
"dd.MM.yyyy HH:mm",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AllowWhiteSpaces,
|
||||
out dt);
|
||||
TimeStamp = dt;
|
||||
}
|
||||
IsValidTimePoint = success;
|
||||
return success;
|
||||
}
|
||||
|
||||
void AddItems(IItemDictionary items)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
|
||||
/// <summary>
|
||||
/// A regular expression that matches the time stamp in the first
|
||||
/// paragraph of a LaurisText.
|
||||
/// </summary>
|
||||
static readonly Regex _dateStampRegex = new Regex(
|
||||
@"^\s*\[?\s*(?<datetime>\d\d\.\d\d\.\d\d\d\d\s+\d\d:\d\d)");
|
||||
ParameterDictionary _parameterDictionary;
|
||||
UnitDictionary _unitDictionary;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
54
zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs
Executable file
54
zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs
Executable file
@ -0,0 +1,54 @@
|
||||
/* ZaaImporter.cs
|
||||
* part of zaaReloaded2
|
||||
*
|
||||
* Copyright 2015 Daniel Kraus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using zaaReloaded2.LabModel;
|
||||
|
||||
namespace zaaReloaded2.Importer.ZaaImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports laboratory items by parsing the Lauris data from a
|
||||
/// physician's letter.
|
||||
/// </summary>
|
||||
public class ZaaImporter : IImporter
|
||||
{
|
||||
#region IImporter implementation
|
||||
|
||||
public Laboratory Laboratory
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public void Import(string text)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user