mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
Interface is wired up for Approving/Rejecting posts
This commit is contained in:
@@ -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' });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -5,4 +5,3 @@ export default DiscourseRoute.extend({
|
||||
return this.store.find('queuedPost', {status: 'new'});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user