Add regression test for sending group notices via xmlrpc groups connector.
parent
ddd38a3dea
commit
71918eeab4
|
@ -48,9 +48,57 @@ namespace OpenSim.Data
|
||||||
public ulong everyonePowers;
|
public ulong everyonePowers;
|
||||||
public ulong ownersPowers;
|
public ulong ownersPowers;
|
||||||
|
|
||||||
|
public Dictionary<UUID, XGroupMember> members = new Dictionary<UUID, XGroupMember>();
|
||||||
|
public Dictionary<UUID, XGroupNotice> notices = new Dictionary<UUID, XGroupNotice>();
|
||||||
|
|
||||||
public XGroup Clone()
|
public XGroup Clone()
|
||||||
{
|
{
|
||||||
return (XGroup)MemberwiseClone();
|
XGroup clone = (XGroup)MemberwiseClone();
|
||||||
|
clone.members = new Dictionary<UUID, XGroupMember>();
|
||||||
|
clone.notices = new Dictionary<UUID, XGroupNotice>();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<UUID, XGroupMember> kvp in members)
|
||||||
|
clone.members[kvp.Key] = kvp.Value.Clone();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<UUID, XGroupNotice> kvp in notices)
|
||||||
|
clone.notices[kvp.Key] = kvp.Value.Clone();
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class XGroupMember
|
||||||
|
{
|
||||||
|
public UUID agentID;
|
||||||
|
public UUID groupID;
|
||||||
|
public UUID roleID;
|
||||||
|
public bool acceptNotices = true;
|
||||||
|
public bool listInProfile = true;
|
||||||
|
|
||||||
|
public XGroupMember Clone()
|
||||||
|
{
|
||||||
|
return (XGroupMember)MemberwiseClone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class XGroupNotice
|
||||||
|
{
|
||||||
|
public UUID groupID;
|
||||||
|
public UUID noticeID;
|
||||||
|
public uint timestamp;
|
||||||
|
public string fromName;
|
||||||
|
public string subject;
|
||||||
|
public string message;
|
||||||
|
public byte[] binaryBucket;
|
||||||
|
public bool hasAttachment;
|
||||||
|
public int assetType;
|
||||||
|
|
||||||
|
public XGroupNotice Clone()
|
||||||
|
{
|
||||||
|
XGroupNotice clone = (XGroupNotice)MemberwiseClone();
|
||||||
|
clone.binaryBucket = (byte[])binaryBucket.Clone();
|
||||||
|
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,14 +106,13 @@ namespace OpenSim.Data
|
||||||
/// Early stub interface for groups data, not final.
|
/// Early stub interface for groups data, not final.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Currently in-use only for regression test purposes. Needs to be filled out over time.
|
/// Currently in-use only for regression test purposes.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IXGroupData
|
public interface IXGroupData
|
||||||
{
|
{
|
||||||
bool StoreGroup(XGroup group);
|
bool StoreGroup(XGroup group);
|
||||||
XGroup[] GetGroups(string field, string val);
|
XGroup GetGroup(UUID groupID);
|
||||||
XGroup[] GetGroups(string[] fields, string[] vals);
|
Dictionary<UUID, XGroup> GetGroups();
|
||||||
bool DeleteGroups(string field, string val);
|
bool DeleteGroup(UUID groupID);
|
||||||
bool DeleteGroups(string[] fields, string[] vals);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -38,7 +38,7 @@ using OpenSim.Data;
|
||||||
|
|
||||||
namespace OpenSim.Data.Null
|
namespace OpenSim.Data.Null
|
||||||
{
|
{
|
||||||
public class NullXGroupData : NullGenericDataHandler, IXGroupData
|
public class NullXGroupData : IXGroupData
|
||||||
{
|
{
|
||||||
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||||
|
|
||||||
|
@ -56,35 +56,31 @@ namespace OpenSim.Data.Null
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public XGroup[] GetGroups(string field, string val)
|
public XGroup GetGroup(UUID groupID)
|
||||||
{
|
{
|
||||||
return GetGroups(new string[] { field }, new string[] { val });
|
XGroup group = null;
|
||||||
|
|
||||||
|
lock (m_groups)
|
||||||
|
m_groups.TryGetValue(groupID, out group);
|
||||||
|
|
||||||
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
public XGroup[] GetGroups(string[] fields, string[] vals)
|
public Dictionary<UUID, XGroup> GetGroups()
|
||||||
|
{
|
||||||
|
Dictionary<UUID, XGroup> groupsClone = new Dictionary<UUID, XGroup>();
|
||||||
|
|
||||||
|
lock (m_groups)
|
||||||
|
foreach (XGroup group in m_groups.Values)
|
||||||
|
groupsClone[group.groupID] = group.Clone();
|
||||||
|
|
||||||
|
return groupsClone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DeleteGroup(UUID groupID)
|
||||||
{
|
{
|
||||||
lock (m_groups)
|
lock (m_groups)
|
||||||
{
|
return m_groups.Remove(groupID);
|
||||||
List<XGroup> origGroups = Get<XGroup>(fields, vals, m_groups.Values.ToList());
|
|
||||||
|
|
||||||
return origGroups.Select(g => g.Clone()).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DeleteGroups(string field, string val)
|
|
||||||
{
|
|
||||||
return DeleteGroups(new string[] { field }, new string[] { val });
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DeleteGroups(string[] fields, string[] vals)
|
|
||||||
{
|
|
||||||
lock (m_groups)
|
|
||||||
{
|
|
||||||
XGroup[] groupsToDelete = GetGroups(fields, vals);
|
|
||||||
Array.ForEach(groupsToDelete, g => m_groups.Remove(g.groupID));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -497,6 +497,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.XmlRpcGroups
|
||||||
OnNewGroupNotice(GroupID, NoticeID);
|
OnNewGroupNotice(GroupID, NoticeID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** We would insert call code here ***/
|
||||||
// Send notice out to everyone that wants notices
|
// Send notice out to everyone that wants notices
|
||||||
foreach (GroupMembersData member in m_groupData.GetGroupMembers(GetRequestingAgentID(remoteClient), GroupID))
|
foreach (GroupMembersData member in m_groupData.GetGroupMembers(GetRequestingAgentID(remoteClient), GroupID))
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Nini.Config;
|
using Nini.Config;
|
||||||
|
@ -118,5 +119,59 @@ namespace OpenSim.Region.OptionalModules.Avatar.XmlRpcGroups.Tests
|
||||||
|
|
||||||
// TODO: More checking of more actual event data.
|
// TODO: More checking of more actual event data.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSendGroupNotice()
|
||||||
|
{
|
||||||
|
TestHelpers.InMethod();
|
||||||
|
// TestHelpers.EnableLogging();
|
||||||
|
|
||||||
|
TestScene scene = new SceneHelpers().SetupScene();
|
||||||
|
IConfigSource configSource = new IniConfigSource();
|
||||||
|
IConfig config = configSource.AddConfig("Groups");
|
||||||
|
config.Set("Enabled", true);
|
||||||
|
config.Set("Module", "GroupsModule");
|
||||||
|
config.Set("DebugEnabled", true);
|
||||||
|
|
||||||
|
GroupsModule gm = new GroupsModule();
|
||||||
|
|
||||||
|
SceneHelpers.SetupSceneModules(scene, configSource, gm, new MockGroupsServicesConnector());
|
||||||
|
|
||||||
|
UUID userId = TestHelpers.ParseTail(0x1);
|
||||||
|
string subjectText = "newman";
|
||||||
|
string messageText = "Hello";
|
||||||
|
string combinedSubjectMessage = string.Format("{0}|{1}", subjectText, messageText);
|
||||||
|
|
||||||
|
ScenePresence sp = SceneHelpers.AddScenePresence(scene, TestHelpers.ParseTail(0x1));
|
||||||
|
TestClient tc = (TestClient)sp.ControllingClient;
|
||||||
|
|
||||||
|
UUID groupID = gm.CreateGroup(tc, "group1", null, true, UUID.Zero, 0, true, true, true);
|
||||||
|
gm.JoinGroupRequest(tc, groupID);
|
||||||
|
|
||||||
|
// Create a second user who doesn't want to receive notices
|
||||||
|
ScenePresence sp2 = SceneHelpers.AddScenePresence(scene, TestHelpers.ParseTail(0x2));
|
||||||
|
TestClient tc2 = (TestClient)sp2.ControllingClient;
|
||||||
|
gm.JoinGroupRequest(tc2, groupID);
|
||||||
|
gm.SetGroupAcceptNotices(tc2, groupID, false, true);
|
||||||
|
|
||||||
|
List<GridInstantMessage> spReceivedMessages = new List<GridInstantMessage>();
|
||||||
|
tc.OnReceivedInstantMessage += im => spReceivedMessages.Add(im);
|
||||||
|
|
||||||
|
List<GridInstantMessage> sp2ReceivedMessages = new List<GridInstantMessage>();
|
||||||
|
tc2.OnReceivedInstantMessage += im => sp2ReceivedMessages.Add(im);
|
||||||
|
|
||||||
|
GridInstantMessage noticeIm = new GridInstantMessage();
|
||||||
|
noticeIm.fromAgentID = userId.Guid;
|
||||||
|
noticeIm.toAgentID = groupID.Guid;
|
||||||
|
noticeIm.message = combinedSubjectMessage;
|
||||||
|
noticeIm.dialog = (byte)InstantMessageDialog.GroupNotice;
|
||||||
|
|
||||||
|
tc.HandleImprovedInstantMessage(noticeIm);
|
||||||
|
|
||||||
|
Assert.That(spReceivedMessages.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(spReceivedMessages[0].message, Is.EqualTo(combinedSubjectMessage));
|
||||||
|
|
||||||
|
Assert.That(sp2ReceivedMessages.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -138,33 +138,28 @@ namespace OpenSim.Tests.Common.Mock
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private XGroup GetXGroup(UUID groupID, string name)
|
||||||
|
{
|
||||||
|
XGroup group = m_data.GetGroup(groupID);
|
||||||
|
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
m_log.DebugFormat("[MOCK GROUPS SERVICES CONNECTOR]: No group found with ID {0}", groupID);
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
public GroupRecord GetGroupRecord(UUID requestingAgentID, UUID groupID, string groupName)
|
public GroupRecord GetGroupRecord(UUID requestingAgentID, UUID groupID, string groupName)
|
||||||
{
|
{
|
||||||
m_log.DebugFormat(
|
m_log.DebugFormat(
|
||||||
"[MOCK GROUPS SERVICES CONNECTOR]: Processing GetGroupRecord() for groupID {0}, name {1}",
|
"[MOCK GROUPS SERVICES CONNECTOR]: Processing GetGroupRecord() for groupID {0}, name {1}",
|
||||||
groupID, groupName);
|
groupID, groupName);
|
||||||
|
|
||||||
XGroup[] groups;
|
XGroup xg = GetXGroup(groupID, groupName);
|
||||||
string field, val;
|
|
||||||
|
|
||||||
if (groupID != UUID.Zero)
|
if (xg == null)
|
||||||
{
|
|
||||||
field = "groupID";
|
|
||||||
val = groupID.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
field = "name";
|
|
||||||
val = groupName;
|
|
||||||
}
|
|
||||||
|
|
||||||
groups = m_data.GetGroups(field, val);
|
|
||||||
|
|
||||||
if (groups.Length == 0)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
XGroup xg = groups[0];
|
|
||||||
|
|
||||||
GroupRecord gr = new GroupRecord()
|
GroupRecord gr = new GroupRecord()
|
||||||
{
|
{
|
||||||
GroupID = xg.groupID,
|
GroupID = xg.groupID,
|
||||||
|
@ -196,8 +191,25 @@ namespace OpenSim.Tests.Common.Mock
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetAgentGroupInfo(UUID requestingAgentID, UUID AgentID, UUID GroupID, bool AcceptNotices, bool ListInProfile)
|
public void SetAgentGroupInfo(UUID requestingAgentID, UUID agentID, UUID groupID, bool acceptNotices, bool listInProfile)
|
||||||
{
|
{
|
||||||
|
m_log.DebugFormat(
|
||||||
|
"[MOCK GROUPS SERVICES CONNECTOR]: SetAgentGroupInfo, requestingAgentID {0}, agentID {1}, groupID {2}, acceptNotices {3}, listInProfile {4}",
|
||||||
|
requestingAgentID, agentID, groupID, acceptNotices, listInProfile);
|
||||||
|
|
||||||
|
XGroup group = GetXGroup(groupID, null);
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
XGroupMember xgm = null;
|
||||||
|
if (!group.members.TryGetValue(agentID, out xgm))
|
||||||
|
return;
|
||||||
|
|
||||||
|
xgm.acceptNotices = acceptNotices;
|
||||||
|
xgm.listInProfile = listInProfile;
|
||||||
|
|
||||||
|
m_data.StoreGroup(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddAgentToGroupInvite(UUID requestingAgentID, UUID inviteID, UUID groupID, UUID roleID, UUID agentID)
|
public void AddAgentToGroupInvite(UUID requestingAgentID, UUID inviteID, UUID groupID, UUID roleID, UUID agentID)
|
||||||
|
@ -213,8 +225,27 @@ namespace OpenSim.Tests.Common.Mock
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddAgentToGroup(UUID requestingAgentID, UUID AgentID, UUID GroupID, UUID RoleID)
|
public void AddAgentToGroup(UUID requestingAgentID, UUID agentID, UUID groupID, UUID roleID)
|
||||||
{
|
{
|
||||||
|
m_log.DebugFormat(
|
||||||
|
"[MOCK GROUPS SERVICES CONNECTOR]: AddAgentToGroup, requestingAgentID {0}, agentID {1}, groupID {2}, roleID {3}",
|
||||||
|
requestingAgentID, agentID, groupID, roleID);
|
||||||
|
|
||||||
|
XGroup group = GetXGroup(groupID, null);
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
XGroupMember groupMember = new XGroupMember()
|
||||||
|
{
|
||||||
|
agentID = agentID,
|
||||||
|
groupID = groupID,
|
||||||
|
roleID = roleID
|
||||||
|
};
|
||||||
|
|
||||||
|
group.members[agentID] = groupMember;
|
||||||
|
|
||||||
|
m_data.StoreGroup(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveAgentFromGroup(UUID requestingAgentID, UUID AgentID, UUID GroupID)
|
public void RemoveAgentFromGroup(UUID requestingAgentID, UUID AgentID, UUID GroupID)
|
||||||
|
@ -259,9 +290,31 @@ namespace OpenSim.Tests.Common.Mock
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<GroupMembersData> GetGroupMembers(UUID requestingAgentID, UUID GroupID)
|
public List<GroupMembersData> GetGroupMembers(UUID requestingAgentID, UUID groupID)
|
||||||
{
|
{
|
||||||
return null;
|
m_log.DebugFormat(
|
||||||
|
"[MOCK GROUPS SERVICES CONNECTOR]: GetGroupMembers, requestingAgentID {0}, groupID {1}",
|
||||||
|
requestingAgentID, groupID);
|
||||||
|
|
||||||
|
List<GroupMembersData> groupMembers = new List<GroupMembersData>();
|
||||||
|
|
||||||
|
XGroup group = GetXGroup(groupID, null);
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
return groupMembers;
|
||||||
|
|
||||||
|
foreach (XGroupMember xgm in group.members.Values)
|
||||||
|
{
|
||||||
|
GroupMembersData gmd = new GroupMembersData();
|
||||||
|
gmd.AgentID = xgm.agentID;
|
||||||
|
gmd.IsOwner = group.founderID == gmd.AgentID;
|
||||||
|
gmd.AcceptNotices = xgm.acceptNotices;
|
||||||
|
gmd.ListInProfile = xgm.listInProfile;
|
||||||
|
|
||||||
|
groupMembers.Add(gmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<GroupRoleMembersData> GetGroupRoleMembers(UUID requestingAgentID, UUID GroupID)
|
public List<GroupRoleMembersData> GetGroupRoleMembers(UUID requestingAgentID, UUID GroupID)
|
||||||
|
@ -269,18 +322,71 @@ namespace OpenSim.Tests.Common.Mock
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<GroupNoticeData> GetGroupNotices(UUID requestingAgentID, UUID GroupID)
|
public List<GroupNoticeData> GetGroupNotices(UUID requestingAgentID, UUID groupID)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupNoticeInfo GetGroupNotice(UUID requestingAgentID, UUID noticeID)
|
public GroupNoticeInfo GetGroupNotice(UUID requestingAgentID, UUID noticeID)
|
||||||
{
|
{
|
||||||
|
m_log.DebugFormat(
|
||||||
|
"[MOCK GROUPS SERVICES CONNECTOR]: GetGroupNotices, requestingAgentID {0}, noticeID {1}",
|
||||||
|
requestingAgentID, noticeID);
|
||||||
|
|
||||||
|
// Yes, not an efficient way to do it.
|
||||||
|
Dictionary<UUID, XGroup> groups = m_data.GetGroups();
|
||||||
|
|
||||||
|
foreach (XGroup group in groups.Values)
|
||||||
|
{
|
||||||
|
if (group.notices.ContainsKey(noticeID))
|
||||||
|
{
|
||||||
|
XGroupNotice n = group.notices[noticeID];
|
||||||
|
|
||||||
|
GroupNoticeInfo gni = new GroupNoticeInfo();
|
||||||
|
gni.GroupID = n.groupID;
|
||||||
|
gni.Message = n.message;
|
||||||
|
gni.BinaryBucket = n.binaryBucket;
|
||||||
|
gni.noticeData.NoticeID = n.noticeID;
|
||||||
|
gni.noticeData.Timestamp = n.timestamp;
|
||||||
|
gni.noticeData.FromName = n.fromName;
|
||||||
|
gni.noticeData.Subject = n.subject;
|
||||||
|
gni.noticeData.HasAttachment = n.hasAttachment;
|
||||||
|
gni.noticeData.AssetType = (byte)n.assetType;
|
||||||
|
|
||||||
|
return gni;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddGroupNotice(UUID requestingAgentID, UUID groupID, UUID noticeID, string fromName, string subject, string message, byte[] binaryBucket)
|
public void AddGroupNotice(UUID requestingAgentID, UUID groupID, UUID noticeID, string fromName, string subject, string message, byte[] binaryBucket)
|
||||||
{
|
{
|
||||||
|
m_log.DebugFormat(
|
||||||
|
"[MOCK GROUPS SERVICES CONNECTOR]: AddGroupNotice, requestingAgentID {0}, groupID {1}, noticeID {2}, fromName {3}, subject {4}, message {5}, binaryBucket.Length {6}",
|
||||||
|
requestingAgentID, groupID, noticeID, fromName, subject, message, binaryBucket.Length);
|
||||||
|
|
||||||
|
XGroup group = GetXGroup(groupID, null);
|
||||||
|
|
||||||
|
if (group == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
XGroupNotice groupNotice = new XGroupNotice()
|
||||||
|
{
|
||||||
|
groupID = groupID,
|
||||||
|
noticeID = noticeID,
|
||||||
|
fromName = fromName,
|
||||||
|
subject = subject,
|
||||||
|
message = message,
|
||||||
|
timestamp = (uint)Util.UnixTimeSinceEpoch(),
|
||||||
|
hasAttachment = false,
|
||||||
|
assetType = 0,
|
||||||
|
binaryBucket = binaryBucket
|
||||||
|
};
|
||||||
|
|
||||||
|
group.notices[noticeID] = groupNotice;
|
||||||
|
|
||||||
|
m_data.StoreGroup(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetAgentGroupChatSessions(UUID agentID)
|
public void ResetAgentGroupChatSessions(UUID agentID)
|
||||||
|
|
|
@ -279,8 +279,9 @@
|
||||||
|
|
||||||
<ReferencePath>../../bin/</ReferencePath>
|
<ReferencePath>../../bin/</ReferencePath>
|
||||||
<Reference name="System"/>
|
<Reference name="System"/>
|
||||||
<Reference name="System.Xml"/>
|
<Reference name="System.Core"/>
|
||||||
<Reference name="System.Data"/>
|
<Reference name="System.Data"/>
|
||||||
|
<Reference name="System.Xml"/>
|
||||||
<Reference name="XMLRPC" path="../../bin/"/>
|
<Reference name="XMLRPC" path="../../bin/"/>
|
||||||
<Reference name="OpenMetaverse" path="../../bin/"/>
|
<Reference name="OpenMetaverse" path="../../bin/"/>
|
||||||
<Reference name="OpenMetaverse.StructuredData" path="../../bin/"/>
|
<Reference name="OpenMetaverse.StructuredData" path="../../bin/"/>
|
||||||
|
|
Loading…
Reference in New Issue