Interface is wired up for Approving/Rejecting posts

This commit is contained in:
Robin Ward
2015-04-14 14:21:02 -04:00
parent 96d2c5069b
commit 0c233e4e25
20 changed files with 273 additions and 90 deletions

View File

@@ -6,6 +6,16 @@ export function Result(payload, responseJson) {
this.target = null;
}
const ajax = Discourse.ajax;
// We use this to make sure 404s are caught
function rethrow(error) {
if (error.status === 404) {
throw "404: " + error.responseText;
}
throw(error);
}
export default Ember.Object.extend({
pathFor(store, type, findArgs) {
let path = "/" + Ember.String.underscore(store.pluralize(type));
@@ -31,17 +41,18 @@ export default Ember.Object.extend({
},
findAll(store, type) {
return Discourse.ajax(this.pathFor(store, type));
return ajax(this.pathFor(store, type)).catch(rethrow);
},
find(store, type, findArgs) {
return Discourse.ajax(this.pathFor(store, type, findArgs));
return ajax(this.pathFor(store, type, findArgs)).catch(rethrow);
},
update(store, type, id, attrs) {
const data = {};
data[Ember.String.underscore(type)] = attrs;
return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
return ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
return new Result(json[type], json);
});
},
@@ -50,13 +61,13 @@ export default Ember.Object.extend({
const data = {};
const typeField = Ember.String.underscore(type);
data[typeField] = attrs;
return Discourse.ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
return ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
return new Result(json[typeField], json);
});
},
destroyRecord(store, type, record) {
return Discourse.ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' });
return ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' });
}
});

View File

@@ -1,16 +1,16 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
function updateState(state) {
return function(post) {
post.update({ state }).then(() => {
this.get('model').removeObject(post);
}).catch(popupAjaxError);
};
}
export default Ember.Controller.extend({
actions: {
approve(post) {
post.update({ state: 'approved' }).then(() => {
this.get('model').removeObject(post);
});
},
reject(post) {
post.update({ state: 'rejected' }).then(() => {
this.get('model').removeObject(post);
});
}
approve: updateState('approved'),
reject: updateState('rejected')
}
});

View File

@@ -1,30 +1,38 @@
function extractError(error) {
if (error instanceof Error) {
Ember.Logger.error(error.stack);
}
if (typeof error === "string") {
Ember.Logger.error(error);
}
let parsedError;
if (error.responseText) {
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
} catch(ex) {
// in case the JSON doesn't parse
Ember.Logger.error(ex.stack);
}
}
return parsedError || I18n.t('generic_error');
}
export function throwAjaxError(undoCallback) {
return function(error) {
if (error instanceof Error) {
Ember.Logger.error(error.stack);
}
if (typeof error === "string") {
Ember.Logger.error(error);
}
// If we provided an `undo` callback
if (undoCallback) { undoCallback(error); }
let parsedError;
if (error.responseText) {
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
} catch(ex) {
// in case the JSON doesn't parse
Ember.Logger.error(ex.stack);
}
}
throw parsedError || I18n.t('generic_error');
throw extractError(error);
};
}
export function popupAjaxError(err) {
bootbox.alert(extractError(err));
}

View File

@@ -3,24 +3,30 @@ import Presence from 'discourse/mixins/presence';
const RestModel = Ember.Object.extend(Presence, {
isNew: Ember.computed.equal('__state', 'new'),
isCreated: Ember.computed.equal('__state', 'created'),
isSaving: false,
afterUpdate: Ember.K,
update(props) {
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
props = props || this.updateProperties();
const type = this.get('__type'),
store = this.get('store');
const self = this;
self.set('isSaving', true);
return store.update(type, this.get('id'), props).then(function(res) {
self.setProperties(self.__munge(res.payload || res.responseJson));
self.afterUpdate(res);
return res;
});
}).finally(() => this.set('isSaving', false));
},
_saveNew(props) {
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
props = props || this.createProperties();
const type = this.get('__type'),
@@ -28,6 +34,7 @@ const RestModel = Ember.Object.extend(Presence, {
adapter = store.adapterFor(type);
const self = this;
self.set('isSaving', true);
return adapter.createRecord(store, type, props).then(function(res) {
if (!res) { throw "Received no data back from createRecord"; }
@@ -40,7 +47,7 @@ const RestModel = Ember.Object.extend(Presence, {
res.target = self;
return res;
});
}).finally(() => this.set('isSaving', false));
},
createProperties() {

View File

@@ -36,7 +36,7 @@ export default Ember.Object.extend({
if (typeof findArgs === "object") {
return self._resultSet(type, result);
} else {
return self._hydrate(type, result[Ember.String.underscore(type)]);
return self._hydrate(type, result[Ember.String.underscore(type)], result);
}
});
},
@@ -48,7 +48,7 @@ export default Ember.Object.extend({
const typeName = Ember.String.underscore(self.pluralize(type)),
totalRows = result["total_rows_" + typeName] || result.get('totalRows'),
loadMoreUrl = result["load_more_" + typeName],
content = result[typeName].map(obj => self._hydrate(type, obj));
content = result[typeName].map(obj => self._hydrate(type, obj, result));
resultSet.setProperties({ totalRows, loadMoreUrl });
resultSet.get('content').pushObjects(content);
@@ -86,7 +86,7 @@ export default Ember.Object.extend({
_resultSet(type, result) {
const typeName = Ember.String.underscore(this.pluralize(type)),
content = result[typeName].map(obj => this._hydrate(type, obj)),
content = result[typeName].map(obj => this._hydrate(type, obj, result)),
totalRows = result["total_rows_" + typeName] || content.length,
loadMoreUrl = result["load_more_" + typeName];
@@ -111,10 +111,39 @@ export default Ember.Object.extend({
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
},
_hydrate(type, obj) {
_hydrateEmbedded(obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
if (m) {
const subType = m[1];
const collection = root[self.pluralize(subType)];
if (collection) {
const found = collection.findProperty('id', obj[k]);
if (found) {
const hydrated = self._hydrate(subType, found, root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
}
}
}
}
});
},
_hydrate(type, obj, root) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
root = root || obj;
// Experimental: If serialized with a certain option we'll wire up embedded objects
// automatically.
if (root.__rest_serializer === "1") {
this._hydrateEmbedded(obj, root);
}
_identityMap[type] = _identityMap[type] || {};
const existing = _identityMap[type][obj.id];

View File

@@ -5,4 +5,3 @@ export default DiscourseRoute.extend({
return this.store.find('queuedPost', {status: 'new'});
}
});

View File

@@ -1,6 +1,9 @@
// This route is used for retrieving a topic based on params
export default Discourse.Route.extend({
// Avoid default model hook
model: function() { return; },
setupController: function(controller, params) {
params = params || {};
params.track_visit = true;

View File

@@ -1,28 +1,44 @@
<div class='container'>
<div class='queued-posts'>
{{#each post in model}}
<div class='queued-post'>
{{#if post.title}}
<h4 class='title'>{{post.title}}</h4>
{{/if}}
<div class='poster'>
{{avatar post.user imageSize="large"}}
</div>
<div class='cooked'>
<div class='names'>
<span class='username'>{{post.user.username}}</span>
<div class='queued-post'>
<div class='poster'>
{{avatar post.user imageSize="large"}}
</div>
<div class='cooked'>
<div class='names'>
<span class='username'>{{post.user.username}}</span>
</div>
<div class='clearfix'></div>
<span class='post-title'>
{{i18n "queue.topic"}}
{{#if post.topic}}
{{topic-link post.topic}}
{{else}}
{{post.post_options.title}}
{{/if}}
</span>
{{{cook-text post.raw}}}
<div class='queue-controls'>
{{d-button action="approve"
actionParam=post
disabled=post.isSaving
label="queue.approve"
icon="check"
class="btn-primary approve"}}
{{d-button action="reject"
actionParam=post
disabled=post.isSaving
label="queue.reject"
icon="times"
class="btn-warning reject"}}
</div>
</div>
<div class='clearfix'></div>
{{{cook-text post.raw}}}
<div class='queue-controls'>
{{d-button action="approve" actionParam=post label="queue.approve" icon="check" class="btn-primary approve"}}
{{d-button action="reject" actionParam=post label="queue.reject" icon="times" class="btn-warning reject"}}
</div>
</div>
<div class='clearfix'></div>
</div>
{{else}}
<p>{{i18n "queue.none"}}</p>
{{/each}}

View File

@@ -10,8 +10,10 @@
width: $topic-body-width;
float: left;
}
h4.title {
margin-bottom: 1em;
.post-title {
color: darken(scale-color-diff(), 50%);
font-weight: bold;
}
border-bottom: 1px solid darken(scale-color-diff(), 10%);

View File

@@ -219,6 +219,7 @@ class ApplicationController < ActionController::Base
def render_json_dump(obj, opts=nil)
opts ||= {}
obj['__rest_serializer'] = "1" if opts[:rest_serializer]
render json: MultiJson.dump(obj), status: opts[:status] || 200
end

View File

@@ -8,12 +8,20 @@ class QueuedPostsController < ApplicationController
state = QueuedPost.states[(params[:state] || 'new').to_sym]
state ||= QueuedPost.states[:new]
@queued_posts = QueuedPost.where(state: state)
render_serialized(@queued_posts, QueuedPostSerializer, root: :queued_posts)
@queued_posts = QueuedPost.where(state: state).includes(:topic, :user)
render_serialized(@queued_posts, QueuedPostSerializer, root: :queued_posts, rest_serializer: true)
end
def update
qp = QueuedPost.where(id: params[:id]).first
state = params[:queued_post][:state]
if state == 'approved'
qp.approve!(current_user)
elsif state == 'rejected'
qp.reject!(current_user)
end
render_serialized(qp, QueuedPostSerializer, root: :queued_posts)
end

View File

@@ -1,4 +1,5 @@
class QueuedPostSerializer < ApplicationSerializer
attributes :id,
:queue,
:user_id,
@@ -11,4 +12,6 @@ class QueuedPostSerializer < ApplicationSerializer
:created_at
has_one :user, serializer: BasicUserSerializer, embed: :object
has_one :topic, serializer: BasicTopicSerializer
end