partiallly implemnted support for multi attachments
git-svn-id: http://svn.research-infrastructures.eu/public/d4science/gcube/trunk/portal/social-networking-library@122236 82a268e6-3cf1-43bd-a215-b396298e98cf
This commit is contained in:
parent
75a8352ba5
commit
630a26d821
Binary file not shown.
|
@ -192,6 +192,15 @@ public class CassandraClusterConnection {
|
|||
* define Likes CF with FeedId as secondary index
|
||||
*/
|
||||
ColumnFamilyDefinition cfDefLikes = getStaticCFDef(DBCassandraAstyanaxImpl.LIKES, "Feedid");
|
||||
/**
|
||||
* define Invites CF with SenderUserId as secondary index
|
||||
*/
|
||||
ColumnFamilyDefinition cfDefInvites = getStaticCFDef(DBCassandraAstyanaxImpl.INVITES, "SenderUserId");
|
||||
/**
|
||||
* define Attachments CF with FeedId as secondary index
|
||||
*/
|
||||
ColumnFamilyDefinition cfDefAttachments = getStaticCFDef(DBCassandraAstyanaxImpl.ATTACHMENTS, "Feedid");
|
||||
|
||||
|
||||
//get dynamic column families, act as auxiliary indexes
|
||||
ColumnFamilyDefinition cfDefConn = getDynamicCFDef(DBCassandraAstyanaxImpl.CONNECTIONS);
|
||||
|
@ -214,6 +223,8 @@ public class CassandraClusterConnection {
|
|||
.addColumnFamily(cfDefFeeds)
|
||||
.addColumnFamily(cfDefComments)
|
||||
.addColumnFamily(cfDefLikes)
|
||||
.addColumnFamily(cfDefInvites)
|
||||
.addColumnFamily(cfDefAttachments)
|
||||
.addColumnFamily(cfDefConn)
|
||||
.addColumnFamily(cfDefPendingConn)
|
||||
.addColumnFamily(cfDefVRETimeline)
|
||||
|
|
|
@ -9,11 +9,13 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.mail.internet.AddressException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
import org.apache.commons.lang.NullArgumentException;
|
||||
import org.gcube.portal.databook.shared.Attachment;
|
||||
import org.gcube.portal.databook.shared.Comment;
|
||||
import org.gcube.portal.databook.shared.Feed;
|
||||
import org.gcube.portal.databook.shared.FeedType;
|
||||
|
@ -83,6 +85,8 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
public static final String HASHTAGGED_FEEDS = "HashtaggedFeeds"; // contains hashtags per type associated with vre and feed
|
||||
public static final String VRE_INVITES = "VREInvites"; //contains the emails that were invited per VRE
|
||||
public static final String EMAIL_INVITES = "EMAILInvites"; //contains the list of invitation per email
|
||||
public static final String ATTACHMENTS = "Attachments"; //contains the list of all the attachments in a feed
|
||||
public static final String FEED_ATTACHMENTS = "FeedAttachments"; //contains the list of all the attachments for a given feed (dynamic CF)
|
||||
|
||||
|
||||
private static ColumnFamily<String, String> cf_Connections = new ColumnFamily<String, String>(
|
||||
|
@ -162,7 +166,10 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
StringSerializer.get(), // Key Serializer
|
||||
StringSerializer.get()); // Column Serializer
|
||||
|
||||
|
||||
protected static ColumnFamily<String, String> cf_Attachments = new ColumnFamily<String, String>(
|
||||
ATTACHMENTS, // Column Family Name
|
||||
StringSerializer.get(), // Key Serializer
|
||||
StringSerializer.get()); // Column Serializer
|
||||
|
||||
/**
|
||||
* connection instance
|
||||
|
@ -331,7 +338,8 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
.putColumn("LinkTitle", feed.getLinkTitle(), null)
|
||||
.putColumn("LinkDescription", feed.getLinkDescription(), null)
|
||||
.putColumn("LinkHost", feed.getLinkHost(), null)
|
||||
.putColumn("IsApplicationFeed", feed.isApplicationFeed(), null);
|
||||
.putColumn("IsApplicationFeed", feed.isApplicationFeed(), null)
|
||||
.putColumn("multiFileUpload", feed.isMultiFileUpload(), null);
|
||||
return m;
|
||||
}
|
||||
/**
|
||||
|
@ -353,6 +361,26 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
}
|
||||
return execute(m);
|
||||
}
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean saveUserFeed(Feed feed, List<Attachment> attachments) {
|
||||
if (attachments != null && !attachments.isEmpty())
|
||||
feed.setMultiFileUpload(true);
|
||||
boolean saveFeedResult = saveUserFeed(feed);
|
||||
if (saveFeedResult) {
|
||||
String feedKey = feed.getKey();
|
||||
for (Attachment attachment : attachments) {
|
||||
boolean attachSaveResult = saveAttachmentEntry(feedKey, attachment);
|
||||
if (!attachSaveResult)
|
||||
_log.warn("Some of the attachments failed to me saved: " + attachment.getName());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
@ -438,6 +466,7 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
toReturn.setLinkDescription(columns.getColumnByName("LinkDescription").getStringValue());
|
||||
toReturn.setLinkHost(columns.getColumnByName("LinkHost").getStringValue());
|
||||
toReturn.setApplicationFeed(columns.getColumnByName("IsApplicationFeed").getBooleanValue());
|
||||
toReturn.setMultiFileUpload(columns.getColumnByName("multiFileUpload").getBooleanValue());
|
||||
|
||||
} catch (ConnectionException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -452,7 +481,7 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
public List<Feed> getRecentFeedsByUserAndDate(String userid, long timeInMillis) throws IllegalArgumentException {
|
||||
Date now = new Date();
|
||||
if (timeInMillis > now.getTime())
|
||||
throw new IllegalArgumentException("the timeInMillis must be before today");
|
||||
throw new IllegalArgumentException("the timeInMillis must be before today");
|
||||
|
||||
OperationResult<Rows<String, String>> result = null;
|
||||
try {
|
||||
|
@ -1651,8 +1680,8 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
*
|
||||
*/
|
||||
/**
|
||||
* common part to save a feed
|
||||
* @param feed
|
||||
* common part to save a invite
|
||||
* @param invite
|
||||
* @return the partial mutation batch instance
|
||||
*/
|
||||
private MutationBatch initSaveInvite(Invite invite) {
|
||||
|
@ -1660,7 +1689,7 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
throw new NullArgumentException("Invite instance is null");
|
||||
// Inserting data
|
||||
MutationBatch m = conn.getKeyspace().prepareMutationBatch();
|
||||
//an entry in the feed CF
|
||||
//an entry in the invite CF
|
||||
m.withRow(cf_Invites, invite.getKey().toString())
|
||||
.putColumn("SenderUserId", invite.getSenderUserId(), null)
|
||||
.putColumn("Vreid", invite.getVreid(), null)
|
||||
|
@ -1837,6 +1866,31 @@ public final class DBCassandraAstyanaxImpl implements DatabookStore {
|
|||
********************** Helper methods ***********************
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @param feedId the feedId to which the attachment is attached
|
||||
* @param toSave the instance to save
|
||||
* @return true if the attachemnt entry is saved in the Attachments CF
|
||||
*/
|
||||
private boolean saveAttachmentEntry(String feedId, Attachment toSave) {
|
||||
// Inserting data
|
||||
MutationBatch m = conn.getKeyspace().prepareMutationBatch();
|
||||
//an entry in the Attachment CF
|
||||
String attachmentID = UUID.randomUUID().toString();
|
||||
m.withRow(cf_Attachments, attachmentID)
|
||||
.putColumn("feedId", feedId, null)
|
||||
.putColumn("uri", toSave.getUri(), null)
|
||||
.putColumn("name", toSave.getName(), null)
|
||||
.putColumn("description",toSave.getDescription(), null)
|
||||
.putColumn("thumbnailURL",toSave.getThumbnailURL(), null);
|
||||
try {
|
||||
m.execute();
|
||||
} catch (ConnectionException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* simply return an enum representing the privacy level
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
package org.gcube.portal.databook.server;
|
||||
|
||||
import java.util.List;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.gcube.portal.databook.shared.Attachment;
|
||||
import org.gcube.portal.databook.shared.Comment;
|
||||
import org.gcube.portal.databook.shared.Feed;
|
||||
import org.gcube.portal.databook.shared.FeedType;
|
||||
import org.gcube.portal.databook.shared.PrivacyLevel;
|
||||
import org.gcube.portal.databook.shared.ex.ColumnNameNotFoundException;
|
||||
import org.gcube.portal.databook.shared.ex.FeedIDNotFoundException;
|
||||
import org.gcube.portal.databook.shared.ex.FeedTypeNotFoundException;
|
||||
|
@ -12,6 +20,11 @@ import org.junit.AfterClass;
|
|||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
|
||||
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
|
||||
import com.netflix.astyanax.model.ColumnFamily;
|
||||
import com.netflix.astyanax.serializers.StringSerializer;
|
||||
|
||||
public class DatabookCassandraTest {
|
||||
private static DBCassandraAstyanaxImpl store;
|
||||
|
||||
|
@ -26,61 +39,52 @@ public class DatabookCassandraTest {
|
|||
System.out.println("End");
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testFeedNumberPerUser() {
|
||||
// String userid = "massimiliano.assante";
|
||||
//
|
||||
// List<Feed> feeds = null;
|
||||
// int numComment = 0;
|
||||
// long init = System.currentTimeMillis();
|
||||
// try {
|
||||
// feeds = store.getAllFeedsByUser(userid);
|
||||
//
|
||||
// for (Feed feed : feeds) {
|
||||
// List<Comment> comments = store.getAllCommentByFeed(feed.getKey());
|
||||
//
|
||||
//
|
||||
// for (Comment comment : comments) {
|
||||
// numComment ++;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// } catch (PrivacyLevelTypeNotFoundException | FeedTypeNotFoundException
|
||||
// | ColumnNameNotFoundException | FeedIDNotFoundException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// System.err.println(e.toString());
|
||||
// }
|
||||
// long end = System.currentTimeMillis();
|
||||
// System.err.println("retrieved " + feeds.size() + " and " + numComment + " in " + (end - init) + "ms");
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void testFeedNumberPerUser() {
|
||||
String userid = "massimiliano.assante";
|
||||
|
||||
List<Feed> feeds = null;
|
||||
int numComment = 0;
|
||||
long init = System.currentTimeMillis();
|
||||
try {
|
||||
feeds = store.getAllFeedsByUser(userid);
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
List<Comment> comments = store.getAllCommentByFeed(feed.getKey());
|
||||
public void testAttachments() {
|
||||
Attachment a1 = new Attachment("www1", "gattino1", "description1", "http://cdn.tuttozampe.com/wp-content/uploads/2010/09/ipoglicemia-gatto.jpg");
|
||||
Attachment a2 = new Attachment("www2", "name2", "description2", "http://www.gcomegatto.it/wp-content/uploads/2015/01/09gatto.jpg");
|
||||
Attachment a3 = new Attachment("www3", "name3", "description3", "http://cdn.tuttozampe.com/wp-content/uploads/2010/09/ipoglicemia-gatto.jpg");
|
||||
List<Attachment> toPass = new ArrayList<Attachment>();
|
||||
toPass.add(a1);
|
||||
toPass.add(a2);
|
||||
|
||||
|
||||
for (Comment comment : comments) {
|
||||
numComment ++;
|
||||
}
|
||||
}
|
||||
Feed feed = new Feed(UUID.randomUUID().toString(), FeedType.TWEET, "massimiliano.assante", new Date(), "/gcube/devsec/devVRE",
|
||||
"http://www.gcomegatto.it/wp-content/uploads/2015/01/09gatto.jpg", "http://www.gcomegatto.it/wp-content/uploads/2015/01/09gatto.jpg", "This feed has attachments (gattini) ", PrivacyLevel.SINGLE_VRE,
|
||||
"Massimiliano Assante", "massimiliano.assante@isti.cnr.it", "http://cdn.tuttozampe.com/wp-content/uploads/2010/09/ipoglicemia-gatto.jpg", "Gattino", "linkDesc", "http://www.gcomegatto.it/wp-content/uploads/2015/01/09gatto.jpg", false);
|
||||
feed.setMultiFileUpload(true);
|
||||
assertTrue(store.saveUserFeed(feed, toPass));
|
||||
|
||||
} catch (PrivacyLevelTypeNotFoundException | FeedTypeNotFoundException
|
||||
| ColumnNameNotFoundException | FeedIDNotFoundException e) {
|
||||
// TODO Auto-generated catch block
|
||||
System.err.println(e.toString());
|
||||
}
|
||||
long end = System.currentTimeMillis();
|
||||
System.err.println("retrieved " + feeds.size() + " and " + numComment + " in " + (end - init) + "ms");
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testInvites() {
|
||||
// String controlCode = UUID.randomUUID().toString();
|
||||
// String vreid = "/gcube/devsec/devVRE";
|
||||
// String email = "ciccio@gmail.com";
|
||||
//
|
||||
// InviteStatus statusToLookfor = InviteStatus.ACCEPTED;
|
||||
//
|
||||
// Invite invite = new Invite(UUID.randomUUID().toString(), "andrea.rossi", vreid, email, controlCode, InviteStatus.PENDING, new Date(), "Andrea Rossi");
|
||||
// try {
|
||||
// InviteOperationResult result = store.saveInvite(invite);
|
||||
// System.out.println(result);
|
||||
// String inviteid = store.isExistingInvite(vreid, email);
|
||||
//// System.out.println(store.readInvite(inviteid));
|
||||
//// store.setInviteStatus(vreid, email, InviteStatus.ACCEPTED);
|
||||
//// System.out.println(store.readInvite(inviteid));
|
||||
// System.out.println("Looking for all invites in " + vreid + " with status " + statusToLookfor);
|
||||
// List<Invite> invites = store.getInvitedEmailsByVRE(vreid, statusToLookfor, InviteStatus.PENDING);
|
||||
// for (Invite invite2 : invites) {
|
||||
// System.out.println(invite2);
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// public void testHashTag() {
|
||||
// try {
|
||||
|
@ -116,26 +120,26 @@ public class DatabookCassandraTest {
|
|||
//
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * use exclusively to add a new (Static) CF to a keyspace
|
||||
// */
|
||||
// @Test
|
||||
// public void addInvitesStaticColumnFamilies() {
|
||||
// ColumnFamily<String, String> CF_INVITES = ColumnFamily.newColumnFamily(DBCassandraAstyanaxImpl.INVITES, StringSerializer.get(), StringSerializer.get());
|
||||
//
|
||||
// try {
|
||||
// new CassandraClusterConnection(false).getKeyspace().createColumnFamily(CF_INVITES, ImmutableMap.<String, Object>builder()
|
||||
// .put("default_validation_class", "UTF8Type")
|
||||
// .put("key_validation_class", "UTF8Type")
|
||||
// .put("comparator_type", "UTF8Type")
|
||||
// .build());
|
||||
// /**
|
||||
// * use exclusively to add a new (Static) CF to a keyspace
|
||||
// */
|
||||
// @Test
|
||||
// public void addAttachmentStaticColumnFamilies() {
|
||||
// ColumnFamily<String, String> CF_INVITES = ColumnFamily.newColumnFamily(DBCassandraAstyanaxImpl.ATTACHMENTS, StringSerializer.get(), StringSerializer.get());
|
||||
//
|
||||
// } catch (ConnectionException e) {
|
||||
// e.printStackTrace();
|
||||
// try {
|
||||
// new CassandraClusterConnection(false).getKeyspace().createColumnFamily(CF_INVITES, ImmutableMap.<String, Object>builder()
|
||||
// .put("default_validation_class", "UTF8Type")
|
||||
// .put("key_validation_class", "UTF8Type")
|
||||
// .put("comparator_type", "UTF8Type")
|
||||
// .build());
|
||||
//
|
||||
// } catch (ConnectionException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// System.out.println("addStaticColumnFamily END");
|
||||
// }
|
||||
// System.out.println("addInvitesStaticColumnFamily END");
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package org.gcube.portal.databook.server;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.mail.internet.AddressException;
|
||||
|
||||
import org.apache.commons.lang.NullArgumentException;
|
||||
import org.gcube.portal.databook.shared.Attachment;
|
||||
import org.gcube.portal.databook.shared.Comment;
|
||||
import org.gcube.portal.databook.shared.Feed;
|
||||
import org.gcube.portal.databook.shared.Invite;
|
||||
|
@ -65,6 +64,11 @@ public interface DatabookStore {
|
|||
* @return true if everything went fine
|
||||
*/
|
||||
boolean saveUserFeed(Feed feed);
|
||||
/**
|
||||
* save a Feed instance in the store having more than one attachment
|
||||
* @return true if everything went fine
|
||||
*/
|
||||
boolean saveUserFeed(Feed feed, List<Attachment> attachments);
|
||||
/**
|
||||
* delete a Feed from the store
|
||||
* @throws FeedIDNotFoundException
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package org.gcube.portal.databook.shared;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class Attachment implements Serializable {
|
||||
|
||||
private String uri;
|
||||
private String name;
|
||||
private String description;
|
||||
private String thumbnailURL;
|
||||
|
||||
/**
|
||||
* @param uri where you can download the file from
|
||||
* @param name the name of the attached file
|
||||
* @param description the description of the attached file
|
||||
* @param thumbnailURL the URL of the image representing the attached file
|
||||
*/
|
||||
|
||||
public Attachment(String uri, String name,
|
||||
String description, String thumbnailURL) {
|
||||
super();
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void setUri(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
public String getThumbnailURL() {
|
||||
return thumbnailURL;
|
||||
}
|
||||
|
||||
|
||||
public void setThumbnailURL(String thumbnailURL) {
|
||||
this.thumbnailURL = thumbnailURL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Attachment [uri=" + uri + ", name=" + name + ", description="
|
||||
+ description + ", thumbnailURL=" + thumbnailURL + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -29,6 +29,10 @@ public class Feed implements Serializable, Comparable<Feed> {
|
|||
private String linkDescription;
|
||||
private String linkHost;
|
||||
boolean applicationFeed;
|
||||
/**
|
||||
* this boolean indicates that the attachments to the post are > 1
|
||||
*/
|
||||
boolean multiFileUpload;
|
||||
/**
|
||||
* default constructor
|
||||
*/
|
||||
|
@ -123,7 +127,7 @@ public class Feed implements Serializable, Comparable<Feed> {
|
|||
public Feed(String key, FeedType type, String entityId, Date time,
|
||||
String vreid, String uri, String uriThumbnail, String description, PrivacyLevel privacy,
|
||||
String fullName, String email, String thumbnailURL, String commentsNo,
|
||||
String likesNo, String linkTitle, String linkDescription, String linkHost, boolean applicationFeed) {
|
||||
String likesNo, String linkTitle, String linkDescription, String linkHost, boolean applicationFeed, boolean multiFileUpload) {
|
||||
super();
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
|
@ -143,6 +147,7 @@ public class Feed implements Serializable, Comparable<Feed> {
|
|||
this.linkTitle = linkTitle;
|
||||
this.linkHost = linkHost;
|
||||
this.applicationFeed = applicationFeed;
|
||||
this.multiFileUpload = multiFileUpload;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
@ -293,6 +298,12 @@ public class Feed implements Serializable, Comparable<Feed> {
|
|||
public void setApplicationFeed(boolean applicationFeed) {
|
||||
this.applicationFeed = applicationFeed;
|
||||
}
|
||||
public boolean isMultiFileUpload() {
|
||||
return multiFileUpload;
|
||||
}
|
||||
public void setMultiFileUpload(boolean multiFileUpload) {
|
||||
this.multiFileUpload = multiFileUpload;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Feed [key=" + key + ", type=" + type + ", entityId=" + entityId
|
||||
|
@ -302,6 +313,9 @@ public class Feed implements Serializable, Comparable<Feed> {
|
|||
+ fullName + ", email=" + email + ", thumbnailURL="
|
||||
+ thumbnailURL + ", commentsNo=" + commentsNo + ", likesNo="
|
||||
+ likesNo + ", linkTitle=" + linkTitle + ", linkDescription="
|
||||
+ linkDescription + ", linkHost=" + linkHost + "]";
|
||||
+ linkDescription + ", linkHost=" + linkHost
|
||||
+ ", applicationFeed=" + applicationFeed
|
||||
+ ", multiFileUpload=" + multiFileUpload + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue