mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
search for entries
This commit is contained in:
@@ -65,6 +65,29 @@ public class FeedEntryService extends GenericDAO<FeedEntry> {
|
|||||||
return query.getResultList();
|
return query.getResultList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FeedEntryWithStatus> getEntriesByKeywords(User user,
|
||||||
|
String keywords) {
|
||||||
|
return getEntriesByKeywords(user, keywords, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FeedEntryWithStatus> getEntriesByKeywords(User user,
|
||||||
|
String keywords, int offset, int limit) {
|
||||||
|
Query query = em.createNamedQuery("Entry.allByKeywords");
|
||||||
|
query.setParameter("userId", user.getId());
|
||||||
|
query.setParameter("user", user);
|
||||||
|
|
||||||
|
String joinedKeywords = StringUtils.join(
|
||||||
|
keywords.toLowerCase().split(" "), "%");
|
||||||
|
query.setParameter("keywords", "%" + joinedKeywords + "%");
|
||||||
|
if (offset > -1) {
|
||||||
|
query.setFirstResult(offset);
|
||||||
|
}
|
||||||
|
if (limit > -1) {
|
||||||
|
query.setMaxResults(limit);
|
||||||
|
}
|
||||||
|
return buildList(query.getResultList());
|
||||||
|
}
|
||||||
|
|
||||||
public List<FeedEntryWithStatus> getEntries(User user, boolean unreadOnly) {
|
public List<FeedEntryWithStatus> getEntries(User user, boolean unreadOnly) {
|
||||||
return getEntries(user, unreadOnly, -1, -1);
|
return getEntries(user, unreadOnly, -1, -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import javax.persistence.Table;
|
|||||||
import javax.persistence.Temporal;
|
import javax.persistence.Temporal;
|
||||||
import javax.persistence.TemporalType;
|
import javax.persistence.TemporalType;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.StringUtils;
|
|
||||||
import org.hibernate.annotations.Index;
|
import org.hibernate.annotations.Index;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -32,7 +31,8 @@ public class FeedEntry extends AbstractModel {
|
|||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Lob
|
@Lob
|
||||||
private byte[] content;
|
@Column(length = Integer.MAX_VALUE)
|
||||||
|
private String content;
|
||||||
|
|
||||||
@Column(length = 2048)
|
@Column(length = 2048)
|
||||||
private String url;
|
private String url;
|
||||||
@@ -61,11 +61,11 @@ public class FeedEntry extends AbstractModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getContent() {
|
public String getContent() {
|
||||||
return StringUtils.newStringUtf8(content);
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContent(String content) {
|
public void setContent(String content) {
|
||||||
this.content = StringUtils.getBytesUtf8(content);
|
this.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import javax.ws.rs.QueryParam;
|
|||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
@@ -223,4 +225,33 @@ public class EntriesREST extends AbstractREST {
|
|||||||
feedEntryStatusService.saveOrUpdate(status);
|
feedEntryStatusService.saveOrUpdate(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("search")
|
||||||
|
@GET
|
||||||
|
public Entries searchEntries(@QueryParam("keywords") String keywords) {
|
||||||
|
Preconditions.checkArgument(StringUtils.length(keywords) >= 3);
|
||||||
|
|
||||||
|
Entries entries = new Entries();
|
||||||
|
|
||||||
|
List<FeedSubscription> subs = feedSubscriptionService
|
||||||
|
.findAll(getUser());
|
||||||
|
Map<Long, FeedSubscription> subMapping = Maps.uniqueIndex(subs,
|
||||||
|
new Function<FeedSubscription, Long>() {
|
||||||
|
public Long apply(FeedSubscription sub) {
|
||||||
|
return sub.getFeed().getId();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Entry> list = Lists.newArrayList();
|
||||||
|
List<FeedEntryWithStatus> entriesWithStatus = feedEntryService
|
||||||
|
.getEntriesByKeywords(getUser(), keywords);
|
||||||
|
for (FeedEntryWithStatus feedEntry : entriesWithStatus) {
|
||||||
|
Long id = feedEntry.getEntry().getFeed().getId();
|
||||||
|
list.add(populateEntry(buildEntry(feedEntry), subMapping.get(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.setName("Search for : " + keywords);
|
||||||
|
entries.getEntries().addAll(list);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,8 @@
|
|||||||
<named-query name="Entry.allByCategories">
|
<named-query name="Entry.allByCategories">
|
||||||
<query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed and s2.category in (:categories) ) order by e.updated desc</query>
|
<query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed and s2.category in (:categories) ) order by e.updated desc</query>
|
||||||
</named-query>
|
</named-query>
|
||||||
|
|
||||||
|
<named-query name="Entry.allByKeywords">
|
||||||
|
<query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed) and lower(e.content) like :keywords order by e.updated desc</query>
|
||||||
|
</named-query>
|
||||||
</entity-mappings>
|
</entity-mappings>
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
<div>
|
<div>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<span ui-if="showButtons()">
|
||||||
|
<div class="btn-group read-mode" data-toggle="buttons-radio">
|
||||||
|
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">Unread</button>
|
||||||
|
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<button type="button" class="btn" ng-click="refresh()"><i class="icon-refresh"></i> Refresh</button>
|
||||||
<div class="btn-group read-mode" data-toggle="buttons-radio">
|
<button type="button" class="btn" ng-click="markAllAsRead()"><i class="icon-ok"></i> Mark all as read</button>
|
||||||
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">Unread</button>
|
|
||||||
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">All</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn" ng-click="refresh()"><i class="icon-refresh"></i> Refresh</button>
|
<div class="btn-group">
|
||||||
<button type="button" class="btn" ng-click="markAllAsRead()"><i class="icon-ok"></i> Mark all as read</button>
|
<a class="btn dropdown-toggle" data-toggle="dropdown" href="settings"><i class="icon-cog"></i> {{session.name}} <span class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu pull-right">
|
||||||
<div class="btn-group">
|
<li><a href="settings"><i class="icon-wrench"></i> Settings</a></li>
|
||||||
<a class="btn dropdown-toggle" data-toggle="dropdown" href="settings"><i class="icon-cog"></i> {{session.name}} <span class="caret"></span></a>
|
<li ng-show="session.admin"><a ng-click="toAdmin()"><i class="icon-edit"></i> Admin</a></li>
|
||||||
<ul class="dropdown-menu pull-right">
|
<li class="divider"></li>
|
||||||
<li><a href="settings"><i class="icon-wrench"></i> Settings</a></li>
|
<li><a href="logout"><i class="icon-user"></i> Logout</a></li>
|
||||||
<li ng-show="session.admin"><a ng-click="toAdmin()"><i class="icon-edit"></i> Admin</a></li>
|
</ul>
|
||||||
<li class="divider"></li>
|
</div>
|
||||||
<li><a href="logout"><i class="icon-user"></i> Logout</a></li>
|
</span>
|
||||||
</ul>
|
<form ng-submit="search()" class="input-append">
|
||||||
</div>
|
<input type="text" ng-model="keywords"></input>
|
||||||
|
<button class="btn" type="submit"><i class="icon-search"></i></button>
|
||||||
|
</form>
|
||||||
<div spinner shown="loading"></div>
|
<div spinner shown="loading"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +110,7 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
|
|||||||
|
|
||||||
$scope.selectedType = $stateParams._type;
|
$scope.selectedType = $stateParams._type;
|
||||||
$scope.selectedId = $stateParams._id;
|
$scope.selectedId = $stateParams._id;
|
||||||
|
$scope.keywords = $stateParams._keywords;
|
||||||
|
|
||||||
$scope.name = null;
|
$scope.name = null;
|
||||||
$scope.message = null;
|
$scope.message = null;
|
||||||
@@ -140,13 +141,8 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
|
|||||||
limit = $window.height() / 33;
|
limit = $window.height() / 33;
|
||||||
limit = parseInt(limit, 10) + 5;
|
limit = parseInt(limit, 10) + 5;
|
||||||
}
|
}
|
||||||
EntryService.get({
|
|
||||||
type : $scope.selectedType,
|
var callback = function(data) {
|
||||||
id : $scope.selectedId,
|
|
||||||
readType : $scope.settingsService.settings.readingMode,
|
|
||||||
offset : $scope.entries.length,
|
|
||||||
limit : limit
|
|
||||||
}, function(data) {
|
|
||||||
for ( var i = 0; i < data.entries.length; i++) {
|
for ( var i = 0; i < data.entries.length; i++) {
|
||||||
$scope.entries.push(data.entries[i]);
|
$scope.entries.push(data.entries[i]);
|
||||||
}
|
}
|
||||||
@@ -154,7 +150,22 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
|
|||||||
$scope.message = data.message;
|
$scope.message = data.message;
|
||||||
$scope.busy = false;
|
$scope.busy = false;
|
||||||
$scope.hasMore = data.entries.length == limit;
|
$scope.hasMore = data.entries.length == limit;
|
||||||
});
|
};
|
||||||
|
if (!$scope.keywords) {
|
||||||
|
EntryService.get({
|
||||||
|
type : $scope.selectedType,
|
||||||
|
id : $scope.selectedId,
|
||||||
|
readType : $scope.settingsService.settings.readingMode,
|
||||||
|
offset : $scope.entries.length,
|
||||||
|
limit : limit
|
||||||
|
}, callback);
|
||||||
|
} else {
|
||||||
|
EntryService.search({
|
||||||
|
keywords : $scope.keywords,
|
||||||
|
offset : $scope.entries.length,
|
||||||
|
limit : limit
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.mark = function(entry, read) {
|
$scope.mark = function(entry, read) {
|
||||||
@@ -282,8 +293,8 @@ module.controller('ManageUsersCtrl', function($scope, $state, $location,
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('ManageUserCtrl', function($scope, $state, $stateParams, $dialog,
|
module.controller('ManageUserCtrl', function($scope, $state, $stateParams,
|
||||||
AdminUsersService) {
|
$dialog, AdminUsersService) {
|
||||||
$scope.user = $stateParams._id ? AdminUsersService.get({
|
$scope.user = $stateParams._id ? AdminUsersService.get({
|
||||||
id : $stateParams._id
|
id : $stateParams._id
|
||||||
}) : {
|
}) : {
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ module.directive('category', function($compile) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.directive('toolbar', function($stateParams, $route, $location,
|
module.directive('toolbar', function($state, $stateParams, $route, $location,
|
||||||
SettingsService, EntryService, SubscriptionService, SessionService) {
|
SettingsService, EntryService, SubscriptionService, SessionService) {
|
||||||
return {
|
return {
|
||||||
scope : {},
|
scope : {},
|
||||||
@@ -208,6 +208,21 @@ module.directive('toolbar', function($stateParams, $route, $location,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.keywords = $stateParams._keywords;
|
||||||
|
$scope.search = function() {
|
||||||
|
if ($scope.keywords == $stateParams._keywords) {
|
||||||
|
$scope.refresh();
|
||||||
|
} else {
|
||||||
|
$state.transitionTo('feeds.search', {
|
||||||
|
_keywords : $scope.keywords
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$scope.showButtons = function() {
|
||||||
|
return !$stateParams._keywords;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.toAdmin = function() {
|
$scope.toAdmin = function() {
|
||||||
$location.path('admin');
|
$location.path('admin');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ app.config(function($routeProvider, $stateProvider, $urlRouterProvider) {
|
|||||||
templateUrl : 'templates/feeds.view.html',
|
templateUrl : 'templates/feeds.view.html',
|
||||||
controller : 'FeedListCtrl'
|
controller : 'FeedListCtrl'
|
||||||
});
|
});
|
||||||
|
$stateProvider.state('feeds.search', {
|
||||||
|
url : '/search/:_keywords',
|
||||||
|
templateUrl : 'templates/feeds.view.html',
|
||||||
|
controller : 'FeedListCtrl'
|
||||||
|
});
|
||||||
|
|
||||||
$stateProvider.state('admin', {
|
$stateProvider.state('admin', {
|
||||||
abstract : true,
|
abstract : true,
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ module.factory('EntryService', function($resource, $http) {
|
|||||||
params : {
|
params : {
|
||||||
_method : 'mark'
|
_method : 'mark'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
search : {
|
||||||
|
method : 'GET',
|
||||||
|
params : {
|
||||||
|
_method : 'search'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var res = $resource('rest/entries/:_method', {}, actions);
|
var res = $resource('rest/entries/:_method', {}, actions);
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<div ng-repeat="entry in entries" class="entry">
|
<div ng-repeat="entry in entries" class="entry">
|
||||||
<a scroll-to="isOpen && current == entry" scroll-to-offset="-58" href="{{entry.url}}" target="_blank" class="entry-heading" ng-click="entryClicked(entry, $event)"
|
<a scroll-to="isOpen && current == entry" scroll-to-offset="-58" href="{{entry.url}}" target="_blank" class="entry-heading" ng-click="entryClicked(entry, $event)"
|
||||||
ng-class="{open: current == entry, closed: current != entry}">
|
ng-class="{open: current == entry, closed: current != entry}">
|
||||||
<span ui-if="selectedType == 'category'" class="feed-name">{{entry.feedName}}</span>
|
<span ui-if="keywords || selectedType == 'category'" class="feed-name">{{entry.feedName}}</span>
|
||||||
<span class="entry-date">{{entry.date}}</span>
|
<span class="entry-date">{{entry.date}}</span>
|
||||||
<span class="entry-name" ng-class="{unread: entry.read == false, shrink: selectedType == 'category'}" ng-bind-html-unsafe="entry.title"></span>
|
<span class="entry-name" ng-class="{unread: entry.read == false, shrink: keywords || selectedType == 'category'}" ng-bind-html-unsafe="entry.title"></span>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<div class="entry-body" ui-if="isOpen && current == entry">
|
<div class="entry-body" ui-if="isOpen && current == entry">
|
||||||
|
|||||||
Reference in New Issue
Block a user