fix for admin api

This commit is contained in:
Athou
2013-04-18 12:50:44 +02:00
parent 9bd825d786
commit af109ccf5c
30 changed files with 1000 additions and 861 deletions

View File

@@ -0,0 +1,21 @@
package com.commafeed.backend.services;
import javax.ejb.Stateless;
import javax.inject.Inject;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.User;
@Stateless
public class FeedEntryService {
@Inject
FeedEntryStatusDAO feedEntryStatusDAO;
public void markEntry(User user, Long entryId, boolean read) {
FeedEntryStatus status = feedEntryStatusDAO.findById(user, entryId);
status.setRead(read);
feedEntryStatusDAO.update(status);
}
}

View File

@@ -7,6 +7,8 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@@ -16,39 +18,61 @@ import com.wordnik.swagger.annotations.ApiProperty;
@ApiClass("Entry details")
public class Entry implements Serializable {
public static Entry build(FeedEntryStatus status) {
Entry entry = new Entry();
FeedEntry feedEntry = status.getEntry();
entry.setId(String.valueOf(status.getId()));
entry.setTitle(feedEntry.getContent().getTitle());
entry.setContent(feedEntry.getContent().getContent());
entry.setEnclosureUrl(status.getEntry().getContent().getEnclosureUrl());
entry.setEnclosureType(status.getEntry().getContent()
.getEnclosureType());
entry.setDate(feedEntry.getUpdated());
entry.setUrl(feedEntry.getUrl());
entry.setRead(status.isRead());
entry.setFeedName(status.getSubscription().getTitle());
entry.setFeedId(String.valueOf(status.getSubscription().getId()));
entry.setFeedUrl(status.getSubscription().getFeed().getLink());
return entry;
}
@ApiProperty("entry id")
private String id;
@ApiProperty("entry title")
private String title;
@ApiProperty("entry content")
private String content;
@ApiProperty("entry enclosure url, if any")
private String enclosureUrl;
@ApiProperty("entry enclosure mime type, if any")
private String enclosureType;
@ApiProperty("entry publication date")
private Date date;
@ApiProperty("feed id")
private String feedId;
@ApiProperty("feed name")
private String feedName;
@ApiProperty("feed url")
private String feedUrl;
@ApiProperty("entry url")
private String url;
@ApiProperty("read sttaus")
private boolean read;
@ApiProperty("starred status")
private boolean starred;

View File

@@ -1,41 +0,0 @@
package com.commafeed.frontend.model;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class MarkRequest implements Serializable {
private String type;
private String id;
private boolean read;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,40 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Mark Request")
public class CollapseRequest implements Serializable {
@ApiProperty(value = "category id", required = true)
private Long id;
@ApiProperty(value = "collapse", required = true)
private boolean collapse;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public boolean isCollapse() {
return collapse;
}
public void setCollapse(boolean collapse) {
this.collapse = collapse;
}
}

View File

@@ -0,0 +1,29 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass
public class IDRequest implements Serializable {
@ApiProperty
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,51 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Mark Request")
public class MarkRequest implements Serializable {
@ApiProperty(value = "id", required = true)
private String id;
@ApiProperty(value = "mark as read or unread")
private boolean read;
@ApiProperty(value = "only entries older than this, prevent marking an entry that was not retrieved", required = false)
private Long olderThan;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
public Long getOlderThan() {
return olderThan;
}
public void setOlderThan(Long olderThan) {
this.olderThan = olderThan;
}
}

View File

@@ -1,4 +1,4 @@
package com.commafeed.frontend.model;
package com.commafeed.frontend.model.request;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

View File

@@ -1,4 +1,4 @@
package com.commafeed.frontend.model;
package com.commafeed.frontend.model.request;
import java.io.Serializable;

View File

@@ -0,0 +1,40 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Rename Request")
public class RenameRequest implements Serializable {
@ApiProperty(value = "id", required = true)
private Long id;
@ApiProperty(value = "mark as read or unread")
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -1,4 +1,4 @@
package com.commafeed.frontend.model;
package com.commafeed.frontend.model.request;
import java.io.Serializable;
@@ -13,7 +13,7 @@ import com.wordnik.swagger.annotations.ApiProperty;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ApiClass("Subscription request")
public class SubscriptionRequest implements Serializable {
public class SubscribeRequest implements Serializable {
@ApiProperty(value = "url of the feed", required = true)
private String url;

View File

@@ -26,7 +26,7 @@ import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.UserService;
import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.model.RegistrationRequest;
import com.commafeed.frontend.model.request.RegistrationRequest;
import com.commafeed.frontend.pages.GoogleImportRedirectPage;
import com.commafeed.frontend.utils.ModelFactory.MF;

View File

@@ -0,0 +1,14 @@
package com.commafeed.frontend.rest;
public class Enums {
public enum Type {
category, feed, entry;
}
public enum ReadType {
all, unread;
}
}

View File

@@ -7,10 +7,10 @@ import javax.ws.rs.core.Application;
import com.commafeed.frontend.rest.resources.AdminREST;
import com.commafeed.frontend.rest.resources.ApiDocumentationREST;
import com.commafeed.frontend.rest.resources.EntriesREST;
import com.commafeed.frontend.rest.resources.SessionREST;
import com.commafeed.frontend.rest.resources.SettingsREST;
import com.commafeed.frontend.rest.resources.SubscriptionsREST;
import com.commafeed.frontend.rest.resources.CategoryREST;
import com.commafeed.frontend.rest.resources.EntryREST;
import com.commafeed.frontend.rest.resources.FeedREST;
import com.commafeed.frontend.rest.resources.UserREST;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.google.common.collect.Sets;
import com.wordnik.swagger.jaxrs.JaxrsApiReader;
@@ -27,10 +27,10 @@ public class RESTApplication extends Application {
Set<Class<?>> set = Sets.newHashSet();
set.add(JacksonJsonProvider.class);
set.add(EntriesREST.class);
set.add(SubscriptionsREST.class);
set.add(SettingsREST.class);
set.add(SessionREST.class);
set.add(EntryREST.class);
set.add(FeedREST.class);
set.add(CategoryREST.class);
set.add(UserREST.class);
set.add(AdminREST.class);
set.add(ApiDocumentationREST.class);

View File

@@ -37,6 +37,7 @@ import com.commafeed.backend.feeds.OPMLImporter;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService;
@@ -46,7 +47,7 @@ import com.commafeed.frontend.SecurityCheck;
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AbstractREST {
public abstract class AbstractREST {
@Context
HttpServletRequest request;
@@ -73,6 +74,9 @@ public class AbstractREST {
@Inject
FeedEntryStatusDAO feedEntryStatusDAO;
@Inject
FeedEntryService feedEntryService;
@Inject
UserDAO userDAO;

View File

@@ -13,6 +13,7 @@ import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.request.MarkRequest;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.core.Documentation;
@@ -31,6 +32,8 @@ public abstract class AbstractResourceREST extends AbstractREST {
@Context HttpHeaders headers, @Context UriInfo uriInfo) {
TypeUtil.addAllowablePackage(Entries.class.getPackage().getName());
TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName());
String apiVersion = ApiDocumentationREST.API_VERSION;
String swaggerVersion = SwaggerSpec.version();
String basePath = ApiDocumentationREST

View File

@@ -7,7 +7,7 @@ import java.util.Set;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@@ -21,6 +21,7 @@ import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.IDRequest;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -35,7 +36,7 @@ public class AdminREST extends AbstractResourceREST {
@Path("/user/save")
@POST
@ApiOperation(value = "Manually save or update a user", notes = "Manually save or update a user. If the id is not specified, a new user will be created")
@ApiOperation(value = "Save or update a user", notes = "Save or update a user. If the id is not specified, a new user will be created")
public Response save(@ApiParam(required = true) UserModel userModel) {
Preconditions.checkNotNull(userModel);
Preconditions.checkNotNull(userModel.getName());
@@ -92,11 +93,11 @@ public class AdminREST extends AbstractResourceREST {
}
@Path("/user/get")
@Path("/user/get/{id}")
@GET
@ApiOperation(value = "Get user information", notes = "Get user information", responseClass = "com.commafeed.frontend.model.UserModel")
public UserModel getUser(
@ApiParam(value = "user id", required = true) @QueryParam("id") Long id) {
@ApiParam(value = "user id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id);
User user = userDAO.findById(id);
UserModel userModel = new UserModel();
@@ -137,11 +138,11 @@ public class AdminREST extends AbstractResourceREST {
@Path("/user/delete")
@POST
@ApiOperation(value = "Delete a user", notes = "Delete a user, and all his subscriptions")
public Response delete(
@ApiParam(value = "user id", required = true) @QueryParam("id") Long id) {
Preconditions.checkNotNull(id);
public Response delete(@ApiParam(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
User user = userDAO.findById(id);
User user = userDAO.findById(req.getId());
if (user == null) {
return Response.status(Status.NOT_FOUND).build();
}
@@ -159,24 +160,24 @@ public class AdminREST extends AbstractResourceREST {
return Response.ok().build();
}
@Path("/settings/get")
@Path("/settings")
@GET
@ApiOperation(value = "Retrieve application settings", notes = "Retrieve application settings", responseClass = "com.commafeed.backend.model.ApplicationSettings")
public ApplicationSettings getSettings() {
return applicationSettingsService.get();
}
@Path("/settings/save")
@Path("/settings")
@POST
@ApiOperation(value = "Save application settings", notes = "Save application settings")
public void saveSettings(@ApiParam(required = true) ApplicationSettings settings) {
public void saveSettings(
@ApiParam(required = true) ApplicationSettings settings) {
Preconditions.checkNotNull(settings);
applicationSettingsService.save(settings);
}
@Path("/metrics/get")
@Path("/metrics")
@GET
public int[] getMetrics() {
return new int[] { metricsBean.getFeedsRefreshedLastMinute(),

View File

@@ -7,6 +7,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.request.MarkRequest;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.core.Documentation;
@@ -25,6 +26,7 @@ public class ApiDocumentationREST extends AbstractREST {
public Response getAllApis(@Context Application app) {
TypeUtil.addAllowablePackage(Entries.class.getPackage().getName());
TypeUtil.addAllowablePackage(MarkRequest.class.getPackage().getName());
Documentation doc = new Documentation();
for (Class<?> resource : app.getClasses()) {

View File

@@ -0,0 +1,257 @@
package com.commafeed.frontend.rest.resources;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.CollapseRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.RenameRequest;
import com.commafeed.frontend.rest.Enums.ReadType;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/category")
@Api(value = "/category", description = "Operations about user categories")
public class CategoryREST extends AbstractResourceREST {
public static final String ALL = "all";
@Path("/entries")
@GET
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", responseClass = "com.commafeed.frontend.model.Entries")
public Entries getCategoryEntries(
@ApiParam(value = "id of the category, or 'all'", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @QueryParam("readType") ReadType readType,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType);
Entries entries = new Entries();
boolean unreadOnly = readType == ReadType.unread;
if (ALL.equals(id)) {
entries.setName("All");
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO.findAll(
getUser(), unreadOnly, offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(Entry.build(status));
}
} else {
FeedCategory feedCategory = feedCategoryDAO.findById(getUser(),
Long.valueOf(id));
if (feedCategory != null) {
List<FeedCategory> childrenCategories = feedCategoryDAO
.findAllChildrenCategories(getUser(), feedCategory);
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO
.findByCategories(childrenCategories, getUser(),
unreadOnly, offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(Entry.build(status));
}
entries.setName(feedCategory.getName());
}
}
entries.setTimestamp(Calendar.getInstance().getTimeInMillis());
return entries;
}
@Path("/mark")
@POST
@ApiOperation(value = "Mark category entries", notes = "Mark feed entries of this category as read")
public Response markCategoryEntries(
@ApiParam(value = "category id, or 'all'", required = true) MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Date olderThan = req.getOlderThan() == null ? null : new Date(
req.getOlderThan());
if (ALL.equals(req.getId())) {
feedEntryStatusDAO.markAllEntries(getUser(), olderThan);
} else {
List<FeedCategory> categories = feedCategoryDAO
.findAllChildrenCategories(
getUser(),
feedCategoryDAO.findById(getUser(),
Long.valueOf(req.getId())));
feedEntryStatusDAO.markCategoryEntries(getUser(), categories,
olderThan);
}
return Response.ok(Status.OK).build();
}
@Path("/add")
@POST
@ApiOperation(value = "Add a category", notes = "Add a new feed category")
public Response addCategory(
@ApiParam(value = "new name", required = true) @QueryParam("name") String name,
@ApiParam(value = "parent category id, if any") @QueryParam("parentId") String parentId) {
Preconditions.checkNotNull(name);
FeedCategory cat = new FeedCategory();
cat.setName(name);
cat.setUser(getUser());
if (parentId != null && !ALL.equals(parentId)) {
FeedCategory parent = new FeedCategory();
parent.setId(Long.valueOf(parentId));
cat.setParent(parent);
}
feedCategoryDAO.save(cat);
return Response.ok().build();
}
@POST
@Path("/delete")
@ApiOperation(value = "Delete a category", notes = "Delete an existing feed category")
public Response deleteCategory(@ApiParam(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
FeedCategory cat = feedCategoryDAO.findById(getUser(), req.getId());
if (cat != null) {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(
getUser(), cat);
for (FeedSubscription sub : subs) {
sub.setCategory(null);
}
feedSubscriptionDAO.update(subs);
feedCategoryDAO.delete(cat);
return Response.ok().build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
}
@POST
@Path("/rename")
@ApiOperation(value = "Rename a category", notes = "Rename an existing feed category")
public Response renameCategory(@ApiParam(required = true) RenameRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Preconditions.checkArgument(StringUtils.isNotBlank(req.getName()));
FeedCategory category = feedCategoryDAO
.findById(getUser(), req.getId());
category.setName(req.getName());
feedCategoryDAO.update(category);
return Response.ok(Status.OK).build();
}
@POST
@Path("/collapse")
@ApiOperation(value = "Collapse a category", notes = "Save collapsed or expanded status for a category")
public Response collapse(@ApiParam(required = true) CollapseRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
FeedCategory category = feedCategoryDAO.findById(getUser(),
Long.valueOf(req.getId()));
category.setCollapsed(req.isCollapse());
feedCategoryDAO.update(category);
return Response.ok(Status.OK).build();
}
@GET
@Path("/get")
@ApiOperation(value = "Get feed categories", notes = "Get all categories and subscriptions of the user", responseClass = "com.commafeed.frontend.model.Category")
public Category getSubscriptions() {
List<FeedCategory> categories = feedCategoryDAO.findAll(getUser());
List<FeedSubscription> subscriptions = feedSubscriptionDAO
.findAll(getUser());
Map<Long, Long> unreadCount = feedEntryStatusDAO
.getUnreadCount(getUser());
Category root = buildCategory(null, categories, subscriptions,
unreadCount);
root.setId("all");
root.setName("All");
return root;
}
private Category buildCategory(Long id, List<FeedCategory> categories,
List<FeedSubscription> subscriptions, Map<Long, Long> unreadCount) {
Category category = new Category();
category.setId(String.valueOf(id));
category.setExpanded(true);
for (FeedCategory c : categories) {
if ((id == null && c.getParent() == null)
|| (c.getParent() != null && ObjectUtils.equals(c
.getParent().getId(), id))) {
Category child = buildCategory(c.getId(), categories,
subscriptions, unreadCount);
child.setId(String.valueOf(c.getId()));
child.setName(c.getName());
child.setExpanded(!c.isCollapsed());
category.getChildren().add(child);
}
}
Collections.sort(category.getChildren(), new Comparator<Category>() {
@Override
public int compare(Category o1, Category o2) {
return ObjectUtils.compare(o1.getName(), o2.getName());
}
});
for (FeedSubscription subscription : subscriptions) {
if ((id == null && subscription.getCategory() == null)
|| (subscription.getCategory() != null && ObjectUtils
.equals(subscription.getCategory().getId(), id))) {
Subscription sub = new Subscription();
sub.setId(subscription.getId());
sub.setName(subscription.getTitle());
sub.setMessage(subscription.getFeed().getMessage());
sub.setErrorCount(subscription.getFeed().getErrorCount());
sub.setFeedUrl(subscription.getFeed().getLink());
Long size = unreadCount.get(subscription.getId());
sub.setUnread(size == null ? 0 : size);
category.getFeeds().add(sub);
}
}
Collections.sort(category.getFeeds(), new Comparator<Subscription>() {
@Override
public int compare(Subscription o1, Subscription o2) {
return ObjectUtils.compare(o1.getName(), o2.getName());
}
});
return category;
}
}

View File

@@ -1,231 +0,0 @@
package com.commafeed.frontend.rest.resources;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/entries/")
@Api(value = "/entries", description = "Operations about feed entries")
public class EntriesREST extends AbstractResourceREST {
public static final String ALL = "all";
public enum Type {
category, feed, entry;
}
public enum ReadType {
all, unread;
}
@Path("/feed/get")
@GET
@ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", responseClass = "com.commafeed.frontend.model.Entries")
public Entries getFeedEntries(
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @QueryParam("readType") ReadType readType,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType);
Entries entries = new Entries();
boolean unreadOnly = readType == ReadType.unread;
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
Long.valueOf(id));
if (subscription != null) {
entries.setName(subscription.getTitle());
entries.setMessage(subscription.getFeed().getMessage());
entries.setErrorCount(subscription.getFeed().getErrorCount());
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO
.findByFeed(subscription.getFeed(), getUser(), unreadOnly,
offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(buildEntry(status));
}
}
entries.setTimestamp(Calendar.getInstance().getTimeInMillis());
return entries;
}
@Path("/category/get")
@GET
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", responseClass = "com.commafeed.frontend.model.Entries")
public Entries getCategoryEntries(
@ApiParam(value = "id of the category, or 'all'", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @QueryParam("readType") ReadType readType,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType);
Entries entries = new Entries();
boolean unreadOnly = readType == ReadType.unread;
if (ALL.equals(id)) {
entries.setName("All");
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO.findAll(
getUser(), unreadOnly, offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(buildEntry(status));
}
} else {
FeedCategory feedCategory = feedCategoryDAO.findById(getUser(),
Long.valueOf(id));
if (feedCategory != null) {
List<FeedCategory> childrenCategories = feedCategoryDAO
.findAllChildrenCategories(getUser(), feedCategory);
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO
.findByCategories(childrenCategories, getUser(),
unreadOnly, offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(buildEntry(status));
}
entries.setName(feedCategory.getName());
}
}
entries.setTimestamp(Calendar.getInstance().getTimeInMillis());
return entries;
}
private Entry buildEntry(FeedEntryStatus status) {
Entry entry = new Entry();
FeedEntry feedEntry = status.getEntry();
entry.setId(String.valueOf(status.getId()));
entry.setTitle(feedEntry.getContent().getTitle());
entry.setContent(feedEntry.getContent().getContent());
entry.setEnclosureUrl(status.getEntry().getContent().getEnclosureUrl());
entry.setEnclosureType(status.getEntry().getContent()
.getEnclosureType());
entry.setDate(feedEntry.getUpdated());
entry.setUrl(feedEntry.getUrl());
entry.setRead(status.isRead());
entry.setFeedName(status.getSubscription().getTitle());
entry.setFeedId(String.valueOf(status.getSubscription().getId()));
entry.setFeedUrl(status.getSubscription().getFeed().getLink());
return entry;
}
@Path("/entry/mark")
@POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
public Response markFeedEntry(
@ApiParam(value = "entry id", required = true) @QueryParam("id") String id,
@ApiParam(value = "read status", required = true) @QueryParam("read") boolean read) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(read);
FeedEntryStatus status = feedEntryStatusDAO.findById(getUser(),
Long.valueOf(id));
status.setRead(read);
feedEntryStatusDAO.update(status);
return Response.ok(Status.OK).build();
}
@Path("/feed/mark")
@POST
@ApiOperation(value = "Mark feed entries", notes = "Mark feed entries as read")
public Response markFeedEntries(
@ApiParam(value = "feed id", required = true) @QueryParam("id") String id,
@ApiParam(value = "only entries older than this, prevent marking an entry that was not retrieved") @QueryParam("olderThan") Long olderThanTimestamp) {
Preconditions.checkNotNull(id);
Date olderThan = olderThanTimestamp == null ? null : new Date(
olderThanTimestamp);
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
Long.valueOf(id));
feedEntryStatusDAO.markFeedEntries(getUser(), subscription.getFeed(),
olderThan);
return Response.ok(Status.OK).build();
}
@Path("/category/mark")
@POST
@ApiOperation(value = "Mark category entries", notes = "Mark feed entries as read")
public Response markCategoryEntries(
@ApiParam(value = "category id, or 'all'", required = true) @QueryParam("id") String id,
@ApiParam(value = "only entries older than this, prevent marking an entry that was not retrieved") @QueryParam("olderThan") Long olderThanTimestamp) {
Preconditions.checkNotNull(id);
Date olderThan = olderThanTimestamp == null ? null : new Date(
olderThanTimestamp);
if (ALL.equals(id)) {
feedEntryStatusDAO.markAllEntries(getUser(), olderThan);
} else {
List<FeedCategory> categories = feedCategoryDAO
.findAllChildrenCategories(
getUser(),
feedCategoryDAO.findById(getUser(),
Long.valueOf(id)));
feedEntryStatusDAO.markCategoryEntries(getUser(), categories,
olderThan);
}
return Response.ok(Status.OK).build();
}
@Path("/search")
@GET
@ApiOperation(value = "Search for entries", notes = "Look through title and content of entries by keywords", responseClass = "com.commafeed.frontend.model.Entries")
public Entries searchEntries(
@ApiParam(value = "keywords separated by spaces, 3 characters minimum", required = true) @QueryParam("keywords") String keywords,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit) {
keywords = StringUtils.trimToEmpty(keywords);
Preconditions.checkArgument(StringUtils.length(keywords) >= 3);
Entries entries = new Entries();
List<Entry> list = Lists.newArrayList();
List<FeedEntryStatus> entriesStatus = feedEntryStatusDAO
.findByKeywords(getUser(), keywords, offset, limit, true);
for (FeedEntryStatus status : entriesStatus) {
list.add(buildEntry(status));
}
entries.setName("Search for : " + keywords);
entries.getEntries().addAll(list);
return entries;
}
}

View File

@@ -0,0 +1,67 @@
package com.commafeed.frontend.rest.resources;
import java.util.List;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.request.MarkRequest;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/entry")
@Api(value = "/entry", description = "Operations about feed entries")
public class EntryREST extends AbstractResourceREST {
@Path("/mark")
@POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
public Response markFeedEntry(
@ApiParam(value = "Mark Request", required = true) MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
feedEntryService.markEntry(getUser(), Long.valueOf(req.getId()),
req.isRead());
return Response.ok(Status.OK).build();
}
@Path("/search")
@GET
@ApiOperation(value = "Search for entries", notes = "Look through title and content of entries by keywords", responseClass = "com.commafeed.frontend.model.Entries")
public Entries searchEntries(
@ApiParam(value = "keywords separated by spaces, 3 characters minimum", required = true) @QueryParam("keywords") String keywords,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit) {
keywords = StringUtils.trimToEmpty(keywords);
Preconditions.checkArgument(StringUtils.length(keywords) >= 3);
Entries entries = new Entries();
List<Entry> list = Lists.newArrayList();
List<FeedEntryStatus> entriesStatus = feedEntryStatusDAO
.findByKeywords(getUser(), keywords, offset, limit, true);
for (FeedEntryStatus status : entriesStatus) {
list.add(Entry.build(status));
}
entries.setName("Search for : " + keywords);
entries.getEntries().addAll(list);
return entries;
}
}

View File

@@ -0,0 +1,205 @@
package com.commafeed.frontend.rest.resources;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.RenameRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.rest.Enums.ReadType;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/feed")
@Api(value = "/feed", description = "Operations about feeds")
public class FeedREST extends AbstractResourceREST {
@Path("/entries")
@GET
@ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", responseClass = "com.commafeed.frontend.model.Entries")
public Entries getFeedEntries(
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @QueryParam("readType") ReadType readType,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging") @DefaultValue("-1") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType);
Entries entries = new Entries();
boolean unreadOnly = readType == ReadType.unread;
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
Long.valueOf(id));
if (subscription != null) {
entries.setName(subscription.getTitle());
entries.setMessage(subscription.getFeed().getMessage());
entries.setErrorCount(subscription.getFeed().getErrorCount());
List<FeedEntryStatus> unreadEntries = feedEntryStatusDAO
.findByFeed(subscription.getFeed(), getUser(), unreadOnly,
offset, limit, order, true);
for (FeedEntryStatus status : unreadEntries) {
entries.getEntries().add(Entry.build(status));
}
}
entries.setTimestamp(Calendar.getInstance().getTimeInMillis());
return entries;
}
@GET
@Path("/fetch")
@ApiOperation(value = "Fetch a feed", notes = "Fetch a feed by its url", responseClass = "com.commafeed.backend.model.Feed")
public Feed fetchFeed(
@ApiParam(value = "the feed's url", required = true) @QueryParam("url") String url) {
Preconditions.checkNotNull(url);
url = StringUtils.trimToEmpty(url);
url = prependHttp(url);
Feed feed = null;
try {
feed = feedFetcher.fetch(url, true, null, null);
} catch (Exception e) {
throw new WebApplicationException(e, Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
}
return feed;
}
@Path("/mark")
@POST
@ApiOperation(value = "Mark feed entries", notes = "Mark feed entries as read (unread is not supported)")
public Response markFeedEntries(
@ApiParam(value = "Mark request") MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Date olderThan = req.getOlderThan() == null ? null : new Date(
req.getOlderThan());
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
Long.valueOf(req.getId()));
feedEntryStatusDAO.markFeedEntries(getUser(), subscription.getFeed(),
olderThan);
return Response.ok(Status.OK).build();
}
@POST
@Path("/subscribe")
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed")
public Response subscribe(
@ApiParam(value = "subscription request", required = true) SubscribeRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getTitle());
Preconditions.checkNotNull(req.getUrl());
String url = prependHttp(req.getUrl());
url = fetchFeed(url).getUrl();
FeedCategory category = CategoryREST.ALL.equals(req.getCategoryId()) ? null
: feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
Feed fetchedFeed = fetchFeed(url);
feedSubscriptionService.subscribe(getUser(), fetchedFeed.getUrl(),
req.getTitle(), category);
return Response.ok(Status.OK).build();
}
private String prependHttp(String url) {
if (!url.startsWith("http")) {
url = "http://" + url;
}
return url;
}
@POST
@Path("/unsubscribe")
@ApiOperation(value = "Unsubscribe to a feed", notes = "Unsubscribe to a feed")
public Response unsubscribe(
@ApiParam(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
FeedSubscription sub = feedSubscriptionDAO.findById(getUser(),
req.getId());
if (sub != null) {
feedSubscriptionDAO.delete(sub);
return Response.ok(Status.OK).build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
}
@POST
@Path("/rename")
@ApiOperation(value = "Rename a subscription", notes = "Rename a feed subscription")
public Response rename(
@ApiParam(value = "subscription id", required = true) RenameRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getName());
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
req.getId());
subscription.setTitle(req.getName());
feedSubscriptionDAO.update(subscription);
return Response.ok(Status.OK).build();
}
@POST
@Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@ApiOperation(value = "OPML Import", notes = "Import an OPML file, posted as a FORM with the 'file' name")
public Response importOpml() {
try {
FileItemFactory factory = new DiskFileItemFactory(1000000, null);
ServletFileUpload upload = new ServletFileUpload(factory);
for (FileItem item : upload.parseRequest(request)) {
if ("file".equals(item.getFieldName())) {
opmlImporter.importOpml(getUser(),
IOUtils.toString(item.getInputStream(), "UTF-8"));
break;
}
}
} catch (Exception e) {
throw new WebApplicationException(Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
}
return Response.ok(Status.OK).build();
}
}

View File

@@ -1,56 +0,0 @@
package com.commafeed.frontend.rest.resources;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.frontend.model.ProfileModificationRequest;
import com.commafeed.frontend.model.UserModel;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/session")
@Api(value = "/session", description = "Operations about user profile")
public class SessionREST extends AbstractResourceREST {
@Path("/get")
@GET
@ApiOperation(value = "Retrieve user's profile", responseClass = "com.commafeed.frontend.model.UserModel")
public UserModel get() {
User user = getUser();
UserModel userModel = new UserModel();
userModel.setId(user.getId());
userModel.setName(user.getName());
userModel.setEmail(user.getEmail());
userModel.setEnabled(!user.isDisabled());
for (UserRole role : userRoleDAO.findAll(user)) {
if (role.getRole() == Role.ADMIN) {
userModel.setAdmin(true);
}
}
return userModel;
}
@Path("/save")
@POST
@ApiOperation(value = "Save user's profile")
public Response save(
@ApiParam(required = true) ProfileModificationRequest request) {
User user = getUser();
user.setEmail(request.getEmail());
if (StringUtils.isNotBlank(request.getPassword())) {
byte[] password = encryptionService.getEncryptedPassword(
request.getPassword(), user.getSalt());
user.setPassword(password);
}
userDAO.update(user);
return Response.ok().build();
}
}

View File

@@ -1,277 +0,0 @@
package com.commafeed.frontend.rest.resources;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.SubscriptionRequest;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/subscriptions")
@Api(value = "/subscriptions", description = "Operations about user feed subscriptions")
public class SubscriptionsREST extends AbstractResourceREST {
@GET
@Path("/feed/fetch")
@ApiOperation(value = "Fetch a feed", notes = "Fetch a feed by its url", responseClass = "com.commafeed.backend.model.Feed")
public Feed fetchFeed(
@ApiParam(value = "the feed's url", required = true) @QueryParam("url") String url) {
Preconditions.checkNotNull(url);
url = StringUtils.trimToEmpty(url);
url = prependHttp(url);
Feed feed = null;
try {
feed = feedFetcher.fetch(url, true, null, null);
} catch (Exception e) {
throw new WebApplicationException(e, Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
}
return feed;
}
@POST
@Path("/feed/subscribe")
@ApiOperation(value = "Subscribe to a feed", notes = "Subscribe to a feed")
public Response subscribe(
@ApiParam(value = "subscription request", required = true) SubscriptionRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getTitle());
Preconditions.checkNotNull(req.getUrl());
String url = prependHttp(req.getUrl());
url = fetchFeed(url).getUrl();
FeedCategory category = EntriesREST.ALL.equals(req.getCategoryId()) ? null
: feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
Feed fetchedFeed = fetchFeed(url);
feedSubscriptionService.subscribe(getUser(), fetchedFeed.getUrl(),
req.getTitle(), category);
return Response.ok(Status.OK).build();
}
private String prependHttp(String url) {
if (!url.startsWith("http")) {
url = "http://" + url;
}
return url;
}
@POST
@Path("/feed/unsubscribe")
@ApiOperation(value = "Unsubscribe to a feed", notes = "Unsubscribe to a feed")
public Response unsubscribe(
@ApiParam(value = "subscription id", required = true) @QueryParam("id") Long subscriptionId) {
FeedSubscription sub = feedSubscriptionDAO.findById(getUser(),
subscriptionId);
if (sub != null) {
feedSubscriptionDAO.delete(sub);
return Response.ok(Status.OK).build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
}
@POST
@Path("/feed/rename")
@ApiOperation(value = "Rename a subscription", notes = "Rename a feed subscription")
public Response rename(
@ApiParam(value = "subscription id", required = true) @QueryParam("id") Long id,
@ApiParam(value = "new name", required = true) @QueryParam("name") String name) {
FeedSubscription subscription = feedSubscriptionDAO.findById(getUser(),
id);
subscription.setTitle(name);
feedSubscriptionDAO.update(subscription);
return Response.ok(Status.OK).build();
}
@Path("/category/add")
@POST
@ApiOperation(value = "Add a category", notes = "Add a new feed category")
public Response addCategory(
@ApiParam(value = "new name", required = true) @QueryParam("name") String name,
@ApiParam(value = "parent category id, if any") @QueryParam("parentId") String parentId) {
Preconditions.checkNotNull(name);
FeedCategory cat = new FeedCategory();
cat.setName(name);
cat.setUser(getUser());
if (parentId != null && !EntriesREST.ALL.equals(parentId)) {
FeedCategory parent = new FeedCategory();
parent.setId(Long.valueOf(parentId));
cat.setParent(parent);
}
feedCategoryDAO.save(cat);
return Response.ok().build();
}
@POST
@Path("/category/delete")
@ApiOperation(value = "Delete a category", notes = "Delete an existing feed category")
public Response deleteCategory(
@ApiParam(value = "category id", required = true) @QueryParam("id") Long id) {
FeedCategory cat = feedCategoryDAO.findById(getUser(), id);
if (cat != null) {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(
getUser(), cat);
for (FeedSubscription sub : subs) {
sub.setCategory(null);
}
feedSubscriptionDAO.update(subs);
feedCategoryDAO.delete(cat);
return Response.ok().build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
}
@POST
@Path("/category/rename")
@ApiOperation(value = "Rename a category", notes = "Rename an existing feed category")
public Response renameCategory(
@ApiParam(value = "category id", required = true) @QueryParam("id") Long id,
@ApiParam(value = "new name", required = true) @QueryParam("name") String name) {
FeedCategory category = feedCategoryDAO.findById(getUser(), id);
category.setName(name);
feedCategoryDAO.update(category);
return Response.ok(Status.OK).build();
}
@POST
@Path("/category/collapse")
@ApiOperation(value = "Collapse a category", notes = "Save collapsed or expanded status for a category")
public Response collapse(
@ApiParam(value = "category id", required = true) @QueryParam("id") String id,
@ApiParam(value = "true if collapsed", required = true) @QueryParam("collapse") boolean collapse) {
Preconditions.checkNotNull(id);
if (!EntriesREST.ALL.equals(id)) {
FeedCategory category = feedCategoryDAO.findById(getUser(),
Long.valueOf(id));
category.setCollapsed(collapse);
feedCategoryDAO.update(category);
}
return Response.ok(Status.OK).build();
}
@POST
@Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@ApiOperation(value = "OPML Import", notes = "Import an OPML file, posted as a FORM with the 'file' name")
public Response importOpml() {
try {
FileItemFactory factory = new DiskFileItemFactory(1000000, null);
ServletFileUpload upload = new ServletFileUpload(factory);
for (FileItem item : upload.parseRequest(request)) {
if ("file".equals(item.getFieldName())) {
opmlImporter.importOpml(getUser(),
IOUtils.toString(item.getInputStream(), "UTF-8"));
break;
}
}
} catch (Exception e) {
throw new WebApplicationException(Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
}
return Response.ok(Status.OK).build();
}
@GET
@Path("/get")
@ApiOperation(value = "Get feed subscriptions", notes = "Get all subscriptions of the user", responseClass = "com.commafeed.frontend.model.Category")
public Category getSubscriptions() {
List<FeedCategory> categories = feedCategoryDAO.findAll(getUser());
List<FeedSubscription> subscriptions = feedSubscriptionDAO
.findAll(getUser());
Map<Long, Long> unreadCount = feedEntryStatusDAO
.getUnreadCount(getUser());
Category root = buildCategory(null, categories, subscriptions,
unreadCount);
root.setId("all");
root.setName("All");
return root;
}
private Category buildCategory(Long id, List<FeedCategory> categories,
List<FeedSubscription> subscriptions, Map<Long, Long> unreadCount) {
Category category = new Category();
category.setId(String.valueOf(id));
category.setExpanded(true);
for (FeedCategory c : categories) {
if ((id == null && c.getParent() == null)
|| (c.getParent() != null && ObjectUtils.equals(c
.getParent().getId(), id))) {
Category child = buildCategory(c.getId(), categories,
subscriptions, unreadCount);
child.setId(String.valueOf(c.getId()));
child.setName(c.getName());
child.setExpanded(!c.isCollapsed());
category.getChildren().add(child);
}
}
Collections.sort(category.getChildren(), new Comparator<Category>() {
@Override
public int compare(Category o1, Category o2) {
return ObjectUtils.compare(o1.getName(), o2.getName());
}
});
for (FeedSubscription subscription : subscriptions) {
if ((id == null && subscription.getCategory() == null)
|| (subscription.getCategory() != null && ObjectUtils
.equals(subscription.getCategory().getId(), id))) {
Subscription sub = new Subscription();
sub.setId(subscription.getId());
sub.setName(subscription.getTitle());
sub.setMessage(subscription.getFeed().getMessage());
sub.setErrorCount(subscription.getFeed().getErrorCount());
sub.setFeedUrl(subscription.getFeed().getLink());
Long size = unreadCount.get(subscription.getId());
sub.setUnread(size == null ? 0 : size);
category.getFeeds().add(sub);
}
}
Collections.sort(category.getFeeds(), new Comparator<Subscription>() {
@Override
public int compare(Subscription o1, Subscription o2) {
return ObjectUtils.compare(o1.getName(), o2.getName());
}
});
return category;
}
}

View File

@@ -6,23 +6,30 @@ import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
@Path("/settings")
@Api(value = "/settings", description = "Operations about user settings")
public class SettingsREST extends AbstractResourceREST {
@Path("/user")
@Api(value = "/user", description = "Operations about the user")
public class UserREST extends AbstractResourceREST {
@Path("/get")
@Path("/settings")
@GET
@ApiOperation(value = "Retrieve user settings", notes = "Retrieve user settings", responseClass = "com.commafeed.frontend.model.Settings")
public Settings get() {
public Settings getSettings() {
Settings s = new Settings();
UserSettings settings = userSettingsDAO.findByUser(getUser());
if (settings != null) {
@@ -38,10 +45,10 @@ public class SettingsREST extends AbstractResourceREST {
return s;
}
@Path("/save")
@Path("/settings")
@POST
@ApiOperation(value = "Save user settings", notes = "Save user settings")
public Response save(@ApiParam Settings settings) {
public Response saveSettings(@ApiParam Settings settings) {
Preconditions.checkNotNull(settings);
UserSettings s = userSettingsDAO.findByUser(getUser());
@@ -57,4 +64,38 @@ public class SettingsREST extends AbstractResourceREST {
return Response.ok(Status.OK).build();
}
@Path("/profile")
@GET
@ApiOperation(value = "Retrieve user's profile", responseClass = "com.commafeed.frontend.model.UserModel")
public UserModel get() {
User user = getUser();
UserModel userModel = new UserModel();
userModel.setId(user.getId());
userModel.setName(user.getName());
userModel.setEmail(user.getEmail());
userModel.setEnabled(!user.isDisabled());
for (UserRole role : userRoleDAO.findAll(user)) {
if (role.getRole() == Role.ADMIN) {
userModel.setAdmin(true);
}
}
return userModel;
}
@Path("/profile")
@POST
@ApiOperation(value = "Save user's profile")
public Response save(
@ApiParam(required = true) ProfileModificationRequest request) {
User user = getUser();
user.setEmail(request.getEmail());
if (StringUtils.isNotBlank(request.getPassword())) {
byte[] password = encryptionService.getEncryptedPassword(
request.getPassword(), user.getSalt());
user.setPassword(password);
}
userDAO.update(user);
return Response.ok().build();
}
}

View File

@@ -30,7 +30,7 @@
apiKey:"",
dom_id:"swagger-ui-container",
supportHeaderParams: false,
supportedSubmitMethods: ['get', 'post', 'put'],
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function(swaggerApi, swaggerUi){
if(console) {
console.log("Loaded SwaggerUI")

View File

@@ -16,7 +16,8 @@ module.run(function($rootScope) {
});
});
module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
module.controller('SubscribeCtrl', function($scope, FeedService,
CategoryService) {
$scope.opts = {
backdropFade : true,
@@ -27,7 +28,7 @@ module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
$scope.isOpenImport = false;
$scope.sub = {};
$scope.SubscriptionService = SubscriptionService;
$scope.CategoryService = CategoryService;
$scope.open = function() {
$scope.sub = {};
@@ -42,7 +43,7 @@ module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
var msg = 'Loading...';
if ($scope.sub.url && (!$scope.sub.title || $scope.sub.title == msg)) {
$scope.sub.title = msg;
SubscriptionService.fetch({
FeedService.fetch({
url : $scope.sub.url
}, function(data) {
$scope.sub.title = data.title;
@@ -52,7 +53,9 @@ module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
};
$scope.save = function() {
SubscriptionService.subscribe($scope.sub);
FeedService.subscribe($scope.sub, function() {
CategoryService.init();
});
$scope.close();
};
@@ -65,7 +68,7 @@ module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
};
$scope.uploadComplete = function(contents, completed) {
SubscriptionService.init();
CategoryService.init();
$scope.closeImport();
};
@@ -81,13 +84,13 @@ module.controller('SubscribeCtrl', function($scope, SubscriptionService) {
};
$scope.saveCategory = function() {
SubscriptionService.addCategory($scope.cat);
CategoryService.add($scope.cat);
$scope.closeCategory();
};
});
module.controller('CategoryTreeCtrl', function($scope, $timeout, $stateParams,
$window, $location, $state, $route, SubscriptionService) {
$window, $location, $state, $route, CategoryService) {
$scope.selectedType = $stateParams._type;
$scope.selectedId = $stateParams._id;
@@ -98,12 +101,12 @@ module.controller('CategoryTreeCtrl', function($scope, $timeout, $stateParams,
});
$timeout(function refreshTree() {
SubscriptionService.init(function() {
CategoryService.init(function() {
$timeout(refreshTree, 15000);
});
}, 15000);
$scope.SubscriptionService = SubscriptionService;
$scope.CategoryService = CategoryService;
$scope.unreadCount = function(category) {
var count = 0;
@@ -123,7 +126,7 @@ module.controller('CategoryTreeCtrl', function($scope, $timeout, $stateParams,
};
var rootUnreadCount = function() {
return $scope.unreadCount($scope.SubscriptionService.subscriptions);
return $scope.unreadCount($scope.CategoryService.subscriptions);
};
$scope.$watch(rootUnreadCount, function(value) {
@@ -192,20 +195,19 @@ module.controller('CategoryTreeCtrl', function($scope, $timeout, $stateParams,
};
$scope.$on('mark', function(event, args) {
mark($scope.SubscriptionService.subscriptions, args.entry);
mark($scope.CategoryService.subscriptions, args.entry);
});
});
module.controller('ToolbarCtrl',
function($scope, $http, $state, $stateParams, $route, $location,
SettingsService, EntryService, SubscriptionService,
SessionService) {
SettingsService, EntryService, ProfileService) {
function totalActiveAjaxRequests() {
return ($http.pendingRequests.length + $.active);
}
$scope.session = SessionService.get();
$scope.session = ProfileService.get();
$scope.loading = true;
$scope.$watch(totalActiveAjaxRequests, function() {
@@ -271,7 +273,7 @@ module.controller('ToolbarCtrl',
});
module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
$window, EntryService, SettingsService, SubscriptionService) {
$window, EntryService, SettingsService, FeedService, CategoryService) {
$scope.selectedType = $stateParams._type;
$scope.selectedId = $stateParams._id;
@@ -327,8 +329,9 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
$scope.hasMore = data.entries.length == limit;
};
if (!$scope.keywords) {
EntryService.get({
type : $scope.selectedType,
var service = $scope.selectedType == 'feed' ? FeedService
: CategoryService;
service.entries({
id : $scope.selectedId,
readType : $scope.settingsService.settings.readingMode,
order : $scope.settingsService.settings.readingOrder,
@@ -351,7 +354,6 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
entry : entry
});
EntryService.mark({
type : 'entry',
id : entry.id,
read : read
});
@@ -442,13 +444,14 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
});
$scope.$on('markAll', function(event, args) {
EntryService.mark({
type : $scope.selectedType,
var service = $scope.selectedType == 'feed' ? FeedService
: CategoryService;
service.mark({
id : $scope.selectedId,
olderThan : $scope.timestamp,
read : true
}, function() {
SubscriptionService.init(function() {
CategoryService.init(function() {
$scope.$emit('emitReload');
});
});
@@ -559,8 +562,8 @@ module.controller('SettingsCtrl', function($scope, $location, SettingsService) {
};
});
module.controller('ProfileCtrl', function($scope, $location, SessionService) {
$scope.user = SessionService.get();
module.controller('ProfileCtrl', function($scope, $location, ProfileService) {
$scope.user = ProfileService.get();
$scope.cancel = function() {
$location.path('/');
@@ -574,7 +577,7 @@ module.controller('ProfileCtrl', function($scope, $location, SessionService) {
password : $scope.user.password
};
SessionService.save(o, function() {
ProfileService.save(o, function() {
$location.path('/');
});

View File

@@ -86,7 +86,7 @@ module.directive('category', function($compile) {
restrict : 'E',
replace : true,
templateUrl : 'directives/category.html',
controller : function($scope, $dialog, SubscriptionService,
controller : function($scope, $dialog, FeedService, CategoryService,
SettingsService) {
$scope.settingsService = SettingsService;
$scope.unsubscribe = function(subscription) {
@@ -104,8 +104,12 @@ module.directive('category', function($compile) {
$dialog.messageBox(title, msg, btns).open().then(
function(result) {
if (result == 'ok') {
SubscriptionService
.unsubscribe(subscription.id);
var data = {
id : subscription.id
};
FeedService.unsubscribe(data, function() {
CategoryService.init();
});
}
});
};
@@ -114,7 +118,7 @@ module.directive('category', function($compile) {
var name = window.prompt('Rename feed : ', feed.name);
if (name && name != feed.name) {
feed.name = name;
SubscriptionService.rename({
FeedService.rename({
id : feed.id,
name : name
});
@@ -125,7 +129,7 @@ module.directive('category', function($compile) {
var name = window.prompt('Rename category: ', category.name);
if (name && name != category.name) {
category.name = name;
SubscriptionService.renameCategory({
CategoryService.rename({
id : category.id,
name : name
});
@@ -147,10 +151,10 @@ module.directive('category', function($compile) {
$dialog.messageBox(title, msg, btns).open().then(
function(result) {
if (result == 'ok') {
SubscriptionService.deleteCategory({
CategoryService.remove({
id : category.id
}, function() {
SubscriptionService.init();
CategoryService.init();
});
}
});
@@ -158,7 +162,10 @@ module.directive('category', function($compile) {
$scope.toggleCategory = function(category) {
category.expanded = !category.expanded;
SubscriptionService.collapse({
if (category.id == 'all') {
return;
}
CategoryService.collapse({
id : category.id,
collapse : !category.expanded
});

View File

@@ -1,24 +1,78 @@
var module = angular.module('commafeed.services', [ 'ngResource' ]);
module.factory('SessionService', function($resource) {
var actions = {
get : {
method : 'GET',
params : {
_method : 'get'
}
},
save : {
method : 'POST',
params : {
_method : 'save'
}
}
};
return $resource('rest/session/:_method', {}, actions);
module.factory('ProfileService', function($resource) {
return $resource('rest/user/profile/');
});
module.factory('SubscriptionService', function($resource, $http) {
module.factory('SettingsService', function($resource) {
var res = $resource('rest/user/settings');
var s = {};
s.settings = {};
s.save = function(callback) {
res.save(s.settings, function(data) {
if (callback) {
callback(data);
}
});
};
s.init = function(callback) {
res.get(function(data) {
s.settings = data;
if (callback) {
callback(data);
}
});
};
s.init();
return s;
});
module.factory('FeedService', function($resource, $http) {
var actions = {
entries : {
method : 'GET',
params : {
_method : 'entries'
}
},
fetch : {
method : 'GET',
params : {
_method : 'fetch'
}
},
mark : {
method : 'POST',
params : {
_method : 'mark'
}
},
subscribe : {
method : 'POST',
params : {
_method : 'subscribe'
}
},
unsubscribe : {
method : 'POST',
params : {
_method : 'unsubscribe'
}
},
rename : {
method : 'POST',
params : {
_method : 'rename'
}
}
};
var res = $resource('rest/feed/:_method', {}, actions);
return res;
});
module.factory('CategoryService', function($resource, $http) {
var flatten = function(category, parentName, array) {
if (!array)
@@ -39,131 +93,72 @@ module.factory('SubscriptionService', function($resource, $http) {
return array;
};
var actions = {
fetch : {
get : {
method : 'GET',
params : {
_type : 'feed',
_method : 'fetch'
_method : 'get'
}
},
subscribe : {
method : 'POST',
entries : {
method : 'GET',
params : {
_type : 'feed',
_method : 'subscribe'
_method : 'entries'
}
},
unsubscribe : {
mark : {
method : 'POST',
params : {
_type : 'feed',
_method : 'unsubscribe'
_method : 'mark'
}
},
add : {
method : 'POST',
params : {
_method : 'add'
}
},
remove : {
method : 'POST',
params : {
_method : 'delete'
}
},
rename : {
method : 'POST',
params : {
_type : 'feed',
_method : 'rename'
}
},
collapse : {
method : 'POST',
params : {
_type : 'category',
_method : 'collapse'
}
},
addCategory : {
method : 'POST',
params : {
_type : 'category',
_method : 'add'
}
},
deleteCategory : {
method : 'POST',
params : {
_type : 'category',
_method : 'delete'
}
},
renameCategory : {
method : 'POST',
params : {
_type : 'category',
_method : 'rename'
}
}
};
var s = {};
s.get = $resource('rest/subscriptions/get').get;
s.subscriptions = {};
s.flatCategories = {};
var res = $resource('rest/category/:_method', {}, actions);
res.subscriptions = {};
res.flatCategories = {};
var res = $resource('rest/subscriptions/:_type/:_method', {}, actions);
s.init = function(callback) {
s.get(function(data) {
s.subscriptions = data;
s.flatCategories = flatten(s.subscriptions);
if (callback)
callback(data);
});
};
s.fetch = res.fetch;
s.rename = res.rename;
s.subscribe = function(sub, callback) {
res.subscribe(sub, function(data) {
s.init();
res.init = function(callback) {
res.get(function(data) {
res.subscriptions = data;
res.flatCategories = flatten(data);
if (callback)
callback(data);
});
};
var removeSubscription = function(node, subId) {
if (node.children) {
$.each(node.children, function(k, v) {
removeSubscription(v, subId);
});
}
if (node.feeds) {
var foundAtIndex = -1;
$.each(node.feeds, function(k, v) {
if (v.id == subId) {
foundAtIndex = k;
}
});
if (foundAtIndex > -1) {
node.feeds.splice(foundAtIndex, 1);
}
}
};
s.unsubscribe = function(id) {
removeSubscription(s.subscriptions, id);
res.unsubscribe({
id : id
});
};
s.addCategory = function(cat, callback) {
res.addCategory(cat, function(data) {
s.init();
if (callback)
callback(data);
});
};
s.deleteCategory = res.deleteCategory;
s.collapse = res.collapse;
s.init();
return s;
res.init();
return res;
});
module.factory('EntryService', function($resource, $http) {
var actions = {
get : {
search : {
method : 'GET',
params : {
_method : 'get'
_method : 'search'
}
},
mark : {
@@ -173,80 +168,20 @@ module.factory('EntryService', function($resource, $http) {
}
}
};
var res = $resource('rest/entries/:type/:_method', {}, actions);
res.search = $resource('rest/entries/search', {}, actions).get;
var res = $resource('rest/entry/:_method', {}, actions);
return res;
});
module.factory('SettingsService', function($resource) {
var s = {};
s.settings = {};
s.save = function(callback) {
$resource('rest/settings/save').save(s.settings, function(data) {
if (callback) {
callback(data);
}
});
};
s.init = function(callback) {
$resource('rest/settings/get').get(function(data) {
s.settings = data;
if (callback) {
callback(data);
}
});
};
s.init();
return s;
});
module.factory('AdminUsersService', function($resource) {
var actions = {
get : {
method : 'GET',
params : {
_method : 'get'
}
},
getAll : {
method : 'GET',
params : {
_method : 'getAll'
},
isArray : true
},
save : {
method : 'POST',
params : {
_method : 'save'
}
},
remove : {
method : 'POST',
params : {
_method : 'delete'
}
}
};
var res = $resource('rest/admin/user/:_method', {}, actions);
var res = {};
res.get = $resource('rest/admin/user/get/:id').get;
res.getAll = $resource('rest/admin/user/getAll').query;
res.save = $resource('rest/admin/user/save').save;
res.remove = $resource('rest/admin/user/delete').save;
return res;
});
module.factory('AdminSettingsService', function($resource) {
var actions = {
get : {
method : 'GET',
params : {
_method : 'get'
}
},
save : {
method : 'POST',
params : {
_method : 'save'
}
}
};
var res = $resource('rest/admin/settings/:_method', {}, actions);
var res = $resource('rest/admin/settings/');
return res;
});

View File

@@ -34,7 +34,7 @@
<label class="control-label">Category</label>
<div class="controls">
<select name="category" ng-model="sub.categoryId" class="input-block-level" required>
<option ng-repeat="cat in SubscriptionService.flatCategories" value="{{cat.id}}">{{cat.name}}</option>
<option ng-repeat="cat in CategoryService.flatCategories" value="{{cat.id}}">{{cat.name}}</option>
</select>
<span class="help-block" ng-show="!subscribeForm.category.$valid">Required</span>
</div>
@@ -51,7 +51,7 @@
<button type="button" class="close" ng-click="closeImport()">&times;</button>
<h4>Import</h4>
</div>
<form ng-upload class="form-horizontal" action="rest/subscriptions/import">
<form ng-upload class="form-horizontal" action="rest/feed/import">
<div class="modal-body">
<div class="control-group">
<div>Let me import your feeds from your
@@ -93,7 +93,7 @@
<label class="control-label">Parent</label>
<div class="controls">
<select name="category" ng-model="cat.parentId" class="input-block-level" required>
<option ng-repeat="cat in SubscriptionService.flatCategories" value="{{cat.id}}">{{cat.name}}</option>
<option ng-repeat="cat in CategoryService.flatCategories" value="{{cat.id}}">{{cat.name}}</option>
</select>
<span class="help-block" ng-show="!categoryForm.category.$valid">Required</span>
</div>

View File

@@ -1,6 +1,6 @@
<div class="css-treeview" ng-controller="CategoryTreeCtrl">
<ul>
<category node="SubscriptionService.subscriptions"
<category node="CategoryService.subscriptions"
feed-click="feedClicked(id)" category-click="categoryClicked(id)"
selected-type="selectedType" selected-id="selectedId"
format-category-name="formatCategoryName(category)"