mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-04-07 04:24:35 -04:00
Merge branch 'master' into category-reorder
This commit is contained in:
commit
a5f906db69
81 changed files with 860 additions and 727 deletions
.travis.ymlCONTRIBUTING.mdGemfile.lockmain_include.js
app
assets
javascripts
discourse
components
avatar-uploader.js.es6edit-category-security.js.es6menu-panel.js.es6search-menu.js.es6search-text-field.js.es6who-liked.js.es6
controllers
helpers
initializers
lib
models
routes
templates
views
stylesheets
controllers
models
serializers
admin_post_serializer.rbbasic_post_serializer.rbbasic_user_serializer.rbcategory_serializer.rbpost_action_user_serializer.rbpost_serializer.rbtopic_post_count_serializer.rbuser_action_serializer.rbuser_name_serializer.rbuser_serializer.rb
views/list
config
lib
spec
test/javascripts/lib
|
@ -41,7 +41,7 @@ cache:
|
|||
|
||||
before_install:
|
||||
- gem install bundler
|
||||
- npm i -g eslint babel-eslint
|
||||
- npm i -g eslint@1.3.1 babel-eslint
|
||||
- eslint app/assets/javascripts
|
||||
- eslint --ext .es6 app/assets/javascripts
|
||||
- eslint --ext .es6 test/javascripts
|
||||
|
|
134
CONTRIBUTING.md
134
CONTRIBUTING.md
|
@ -1,129 +1,27 @@
|
|||
# Contributing to Discourse
|
||||
|
||||
## Before You Start
|
||||
## Important note for Developers
|
||||
|
||||
Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
|
||||
Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
|
||||
|
||||
## Reporting Bugs
|
||||
For more information on
|
||||
|
||||
1. Always update to the most recent master release; the bug may already be resolved.
|
||||
- how to set up your development environment
|
||||
- first-time project suggestions
|
||||
- code conventions
|
||||
- step-by-step guide for GitHub commits
|
||||
|
||||
2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem.
|
||||
**please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)**
|
||||
|
||||
3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org)
|
||||
## Everything Else
|
||||
|
||||
4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**.
|
||||
There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below.
|
||||
|
||||
5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section).
|
||||
- [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070)
|
||||
- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986)
|
||||
- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
|
||||
- Documentation (TBA)
|
||||
|
||||
6. When the bug is fixed, we will do our best to update the Discourse topic.
|
||||
For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on.
|
||||
|
||||
## Requesting New Features
|
||||
|
||||
1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing.
|
||||
|
||||
2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit.
|
||||
|
||||
3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below).
|
||||
|
||||
## Contributing (Step-by-step)
|
||||
|
||||
1. Clone the Repo:
|
||||
|
||||
git clone git://github.com/discourse/discourse.git
|
||||
|
||||
2. Create a new Branch:
|
||||
|
||||
cd discourse
|
||||
git checkout -b new_discourse_branch
|
||||
|
||||
> Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead.
|
||||
|
||||
3. Code
|
||||
* Adhere to common conventions you see in the existing code
|
||||
* Include tests, and ensure they pass
|
||||
* Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate
|
||||
|
||||
4. Follow the Coding Conventions
|
||||
* two spaces, no tabs
|
||||
* no trailing whitespaces, blank lines should have no spaces
|
||||
* use spaces around operators, after commas, colons, semicolons, around `{` and before `}`
|
||||
* no space after `(`, `[` or before `]`, `)`
|
||||
* use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }`
|
||||
* prefer `class << self; def method; end` over `def self.method` for class methods
|
||||
* prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks
|
||||
* avoid `return` when not required
|
||||
|
||||
> However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome.
|
||||
|
||||
5. Commit
|
||||
|
||||
For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling.
|
||||
|
||||
**NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit!
|
||||
|
||||
|
||||
6. Update your branch
|
||||
|
||||
```
|
||||
git fetch origin
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
7. Fork
|
||||
|
||||
```
|
||||
git remote add mine git@github.com:<your user name>/discourse.git
|
||||
```
|
||||
|
||||
8. Push to your remote
|
||||
|
||||
```
|
||||
git push mine new_discourse_branch
|
||||
```
|
||||
|
||||
9. Issue a Pull Request
|
||||
|
||||
Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command:
|
||||
|
||||
```
|
||||
git fetch origin
|
||||
git checkout new_discourse_branch
|
||||
git rebase origin/master
|
||||
git rebase -i
|
||||
|
||||
< the editor opens and allows you to change the commit history >
|
||||
< follow the instructions on the bottom of the editor >
|
||||
|
||||
git push -f mine new_discourse_branch
|
||||
```
|
||||
|
||||
|
||||
In order to make a pull request,
|
||||
* Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse)
|
||||
* Click "Pull Request".
|
||||
* Write your branch name in the branch field (this is filled with "master" by default)
|
||||
* Click "Update Commit Range".
|
||||
* Ensure the changesets you introduced are included in the "Commits" tab.
|
||||
* Ensure that the "Files Changed" incorporate all of your changes.
|
||||
* Fill in some details about your potential patch including a meaningful title.
|
||||
* Click "Send pull request".
|
||||
|
||||
Thanks for that -- we'll get to your pull request ASAP, we love pull requests!
|
||||
|
||||
10. Responding to Feedback
|
||||
|
||||
The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own.
|
||||
|
||||
> Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top).
|
||||
|
||||
## Translations
|
||||
|
||||
Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics:
|
||||
|
||||
* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
|
||||
* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970)
|
||||
|
||||
|
||||
|
||||
[m]: http://meta.discourse.org
|
||||
*Thanks for contributing!*
|
||||
|
|
|
@ -209,7 +209,7 @@ GEM
|
|||
omniauth-twitter (1.0.1)
|
||||
multi_json (~> 1.3)
|
||||
omniauth-oauth (~> 1.0)
|
||||
onebox (1.5.24)
|
||||
onebox (1.5.25)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import UploadMixin from "discourse/mixins/upload";
|
||||
|
||||
export default Em.Component.extend(UploadMixin, {
|
||||
|
@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, {
|
|||
tagName: "span",
|
||||
imageIsNotASquare: false,
|
||||
|
||||
uploadButtonText: function() {
|
||||
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||
}.property("uploading"),
|
||||
@computed("uploading")
|
||||
uploadButtonText(uploading) {
|
||||
return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.setProperties({
|
||||
imageIsNotASquare: upload.width !== upload.height,
|
||||
uploadedAvatarTemplate: upload.url,
|
||||
custom_avatar_upload_id: upload.id,
|
||||
uploadedAvatarId: upload.id,
|
||||
});
|
||||
|
||||
this.sendAction("done");
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return { user_id: this.get("user_id") };
|
||||
}.property("user_id")
|
||||
@computed("user_id")
|
||||
data(user_id) {
|
||||
return { user_id };
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,16 +7,24 @@ export default buildCategoryPanel('security', {
|
|||
|
||||
actions: {
|
||||
editPermissions() {
|
||||
this.set('editingPermissions', true);
|
||||
if (!this.get('category.is_special')) {
|
||||
this.set('editingPermissions', true);
|
||||
}
|
||||
},
|
||||
|
||||
addPermission(group, id) {
|
||||
this.get('category').addPermission({group_name: group + "",
|
||||
permission: Discourse.PermissionType.create({id})});
|
||||
if (!this.get('category.is_special')) {
|
||||
this.get('category').addPermission({
|
||||
group_name: group + "",
|
||||
permission: Discourse.PermissionType.create({id})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removePermission(permission) {
|
||||
this.get('category').removePermission(permission);
|
||||
if (!this.get('category.is_special')) {
|
||||
this.get('category').removePermission(permission);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
|
||||
$panelBody.height('100%');
|
||||
this.$().css({ left: "auto", top: (menuTop) + "px", height });
|
||||
this.$().css({ top: menuTop + "px", height });
|
||||
$('body').removeClass('drop-down-visible');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {searchForTerm, searchContextDescription} from 'discourse/lib/search';
|
||||
import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
@ -61,8 +61,8 @@ export default Ember.Component.extend({
|
|||
@observes('searchService.term', 'typeFilter')
|
||||
newSearchNeeded() {
|
||||
this.set('noResults', false);
|
||||
const term = (this.get('searchService.term') || '').trim();
|
||||
if (term.length >= Discourse.SiteSettings.min_search_term_length) {
|
||||
const term = this.get('searchService.term')
|
||||
if (isValidSearchTerm(term)) {
|
||||
this.set('loading', true);
|
||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
||||
} else {
|
||||
|
@ -134,7 +134,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
showedSearch() {
|
||||
$('#search-term').focus();
|
||||
$('#search-term').focus().select();
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
|
@ -154,8 +154,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
keyDown(e) {
|
||||
const term = this.get('searchService.term');
|
||||
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
|
||||
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
|
||||
this.set('visible', false);
|
||||
this.send('fullSearch');
|
||||
}
|
||||
|
|
|
@ -5,5 +5,13 @@ export default TextField.extend({
|
|||
@computed('searchService.searchContextEnabled')
|
||||
placeholder: function(searchContextEnabled) {
|
||||
return searchContextEnabled ? "" : I18n.t('search.title');
|
||||
}
|
||||
},
|
||||
|
||||
focusIn: function() {
|
||||
Em.run.later(() => { this.$().select(); });
|
||||
},
|
||||
|
||||
becomeFocused: function() {
|
||||
if (this.get('hasAutofocus')) this.$().focus();
|
||||
}.on('didInsertElement')
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ export default Ember.Component.extend(StringBuffer, {
|
|||
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
|
||||
iconsHtml += Discourse.Utilities.avatarImg({
|
||||
size: 'small',
|
||||
avatarTemplate: u.get('avatarTemplate'),
|
||||
avatarTemplate: u.get('avatar_template'),
|
||||
title: u.get('username')
|
||||
});
|
||||
iconsHtml += "</a>";
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
uploadedAvatarTemplate: null,
|
||||
saveDisabled: Em.computed.alias("uploading"),
|
||||
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'),
|
||||
|
||||
selectedUploadId: function() {
|
||||
switch (this.get("selected")) {
|
||||
case "system": return this.get("system_avatar_upload_id");
|
||||
case "gravatar": return this.get("gravatar_avatar_upload_id");
|
||||
default: return this.get("custom_avatar_upload_id");
|
||||
@computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
|
||||
selectedUploadId(selected, system, gravatar, custom) {
|
||||
switch (selected) {
|
||||
case "system": return system;
|
||||
case "gravatar": return gravatar;
|
||||
default: return custom;
|
||||
}
|
||||
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'),
|
||||
},
|
||||
|
||||
allowImageUpload: function() {
|
||||
@computed("selected", "system_avatar_template", "gravatar_avatar_template", "custom_avatar_template")
|
||||
selectedAvatarTemplate(selected, system, gravatar, custom) {
|
||||
switch (selected) {
|
||||
case "system": return system;
|
||||
case "gravatar": return gravatar;
|
||||
default: return custom;
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
allowImageUpload() {
|
||||
return Discourse.Utilities.allowsImages();
|
||||
}.property(),
|
||||
},
|
||||
|
||||
actions: {
|
||||
useUploadedAvatar() { this.set("selected", "uploaded"); },
|
||||
|
@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
refreshGravatar() {
|
||||
this.set("gravatarRefreshDisabled", true);
|
||||
return Discourse
|
||||
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' })
|
||||
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id))
|
||||
.ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
|
||||
.then(result => this.setProperties({
|
||||
gravatar_avatar_template: result.gravatar_avatar_template,
|
||||
gravatar_upload_id: result.gravatar_upload_id,
|
||||
}))
|
||||
.finally(() => this.set("gravatarRefreshDisabled", false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import DiscourseURL from 'discourse/lib/url';
|
|||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
|
@ -64,6 +65,11 @@ export default Ember.Controller.extend({
|
|||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.action')
|
||||
canWhisper(action) {
|
||||
return this.siteSettings.enable_whispers && action === Composer.REPLY;
|
||||
},
|
||||
|
||||
showWarning: function() {
|
||||
if (!Discourse.User.currentProp('staff')) { return false; }
|
||||
|
||||
|
@ -132,7 +138,6 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
hitEsc() {
|
||||
|
||||
const messages = this.get('controllers.composer-messages.model');
|
||||
if (messages.length) {
|
||||
messages.popObject();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { translateResults, searchContextDescription, getSearchKey } from "discourse/lib/search";
|
||||
import { translateResults, searchContextDescription, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Category from 'discourse/models/category';
|
||||
|
@ -13,6 +13,11 @@ export default Ember.Controller.extend({
|
|||
context_id: null,
|
||||
context: null,
|
||||
|
||||
@computed('q')
|
||||
hasAutofocus(q) {
|
||||
return Em.isEmpty(q);
|
||||
},
|
||||
|
||||
@computed('skip_context', 'context')
|
||||
searchContextEnabled: {
|
||||
get(skip,context){
|
||||
|
@ -37,7 +42,12 @@ export default Ember.Controller.extend({
|
|||
|
||||
@computed('q')
|
||||
searchActive(q){
|
||||
return q && q.length > 0;
|
||||
return isValidSearchTerm(q);
|
||||
},
|
||||
|
||||
@computed('searchTerm')
|
||||
isNotValidSearchTerm(searchTerm) {
|
||||
return !isValidSearchTerm(searchTerm);
|
||||
},
|
||||
|
||||
@observes('model')
|
||||
|
@ -129,6 +139,7 @@ export default Ember.Controller.extend({
|
|||
},
|
||||
|
||||
search() {
|
||||
if (this.get("isNotValidSearchTerm")) return;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { setting } from 'discourse/lib/computed';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
|
@ -10,18 +11,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
editHistoryVisible: setting('edit_history_visible_to_public'),
|
||||
|
||||
selectedCategories: function(){
|
||||
return [].concat(this.get("model.watchedCategories"),
|
||||
this.get("model.trackedCategories"),
|
||||
this.get("model.mutedCategories"));
|
||||
}.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"),
|
||||
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
|
||||
selectedCategories(watched, tracked, muted) {
|
||||
return [].concat(watched, tracked, muted);
|
||||
},
|
||||
|
||||
// By default we haven't saved anything
|
||||
saved: false,
|
||||
|
||||
newNameInput: null,
|
||||
|
||||
userFields: function() {
|
||||
@computed("model.user_fields.@each.value")
|
||||
userFields() {
|
||||
let siteUserFields = this.site.get('user_fields');
|
||||
if (!Ember.isEmpty(siteUserFields)) {
|
||||
const userFields = this.get('model.user_fields');
|
||||
|
@ -35,34 +36,37 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
return Ember.Object.create({ value, field });
|
||||
});
|
||||
}
|
||||
}.property('model.user_fields.@each.value'),
|
||||
},
|
||||
|
||||
cannotDeleteAccount: Em.computed.not('can_delete_account'),
|
||||
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
|
||||
|
||||
canEditName: setting('enable_names'),
|
||||
|
||||
nameInstructions: function() {
|
||||
@computed()
|
||||
nameInstructions() {
|
||||
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
}.property(),
|
||||
},
|
||||
|
||||
canSelectTitle: function() {
|
||||
return this.siteSettings.enable_badges && this.get('model.has_title_badges');
|
||||
}.property('model.badge_count'),
|
||||
@computed("model.has_title_badges")
|
||||
canSelectTitle(hasTitleBadges) {
|
||||
return this.siteSettings.enable_badges && hasTitleBadges;
|
||||
},
|
||||
|
||||
canChangePassword: function() {
|
||||
@computed()
|
||||
canChangePassword() {
|
||||
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
|
||||
}.property(),
|
||||
},
|
||||
|
||||
canReceiveDigest: function() {
|
||||
@computed()
|
||||
canReceiveDigest() {
|
||||
return !this.siteSettings.disable_digest_emails;
|
||||
}.property(),
|
||||
},
|
||||
|
||||
availableLocales: function() {
|
||||
return this.siteSettings.available_locales.split('|').map( function(s) {
|
||||
return {name: s, value: s};
|
||||
});
|
||||
}.property(),
|
||||
@computed()
|
||||
availableLocales() {
|
||||
return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s }));
|
||||
},
|
||||
|
||||
digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 },
|
||||
{ name: I18n.t('user.email_digests.every_three_days'), value: 3 },
|
||||
|
@ -86,16 +90,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
{ name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 },
|
||||
{ name: I18n.t('user.new_topic_duration.last_here'), value: -2 }],
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save');
|
||||
}.property('model.isSaving'),
|
||||
@computed("model.isSaving")
|
||||
saveButtonText(isSaving) {
|
||||
return isSaving ? I18n.t('saving') : I18n.t('save');
|
||||
},
|
||||
|
||||
passwordProgress: null,
|
||||
|
||||
actions: {
|
||||
|
||||
save() {
|
||||
const self = this;
|
||||
this.set('saved', false);
|
||||
|
||||
const model = this.get('model');
|
||||
|
@ -113,28 +117,27 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
|
||||
// Cook the bio for preview
|
||||
model.set('name', this.get('newNameInput'));
|
||||
return model.save().then(function() {
|
||||
return model.save().then(() => {
|
||||
if (Discourse.User.currentProp('id') === model.get('id')) {
|
||||
Discourse.User.currentProp('name', model.get('name'));
|
||||
}
|
||||
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
|
||||
self.set('saved', true);
|
||||
this.set('saved', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
changePassword() {
|
||||
const self = this;
|
||||
if (!this.get('passwordProgress')) {
|
||||
this.set('passwordProgress', I18n.t("user.change_password.in_progress"));
|
||||
return this.get('model').changePassword().then(function() {
|
||||
return this.get('model').changePassword().then(() => {
|
||||
// password changed
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
changePasswordProgress: false,
|
||||
passwordProgress: I18n.t("user.change_password.success")
|
||||
});
|
||||
}, function() {
|
||||
}).catch(() => {
|
||||
// password failed to change
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
changePasswordProgress: false,
|
||||
passwordProgress: I18n.t("user.change_password.error")
|
||||
});
|
||||
|
|
|
@ -1,37 +1,29 @@
|
|||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter';
|
||||
|
||||
const safe = Handlebars.SafeString;
|
||||
|
||||
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
||||
Em.Handlebars.helper('bound-avatar', (user, size) => {
|
||||
if (Em.isEmpty(user)) {
|
||||
return new safe("<div class='avatar-placeholder'></div>");
|
||||
}
|
||||
|
||||
const username = Em.get(user, 'username');
|
||||
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
|
||||
const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId);
|
||||
|
||||
const avatar = Em.get(user, 'avatar_template');
|
||||
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));
|
||||
}, 'username', 'uploaded_avatar_id', 'avatar_template');
|
||||
}, 'username', 'avatar_template');
|
||||
|
||||
/*
|
||||
* Used when we only have a template
|
||||
*/
|
||||
Em.Handlebars.helper('bound-avatar-template', function(at, size) {
|
||||
Em.Handlebars.helper('bound-avatar-template', (at, size) => {
|
||||
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at }));
|
||||
});
|
||||
|
||||
registerUnbound('raw-date', function(dt) {
|
||||
return longDate(new Date(dt));
|
||||
});
|
||||
registerUnbound('raw-date', dt => longDate(new Date(dt)));
|
||||
|
||||
registerUnbound('age-with-tooltip', function(dt) {
|
||||
return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}));
|
||||
});
|
||||
registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
|
||||
|
||||
registerUnbound('number', function(orig, params) {
|
||||
registerUnbound('number', (orig, params) => {
|
||||
orig = parseInt(orig, 10);
|
||||
if (isNaN(orig)) { orig = 0; }
|
||||
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
|
||||
function renderAvatar(user, options) {
|
||||
options = options || {};
|
||||
|
||||
if (user) {
|
||||
var username = Em.get(user, 'username');
|
||||
if (!username) {
|
||||
if (!options.usernamePath) { return ''; }
|
||||
username = Em.get(user, options.usernamePath);
|
||||
}
|
||||
|
||||
var title;
|
||||
const username = Em.get(user, options.usernamePath || 'username');
|
||||
const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template');
|
||||
|
||||
if (!username || !avatarTemplate) { return ''; }
|
||||
|
||||
let title;
|
||||
if (!options.ignoreTitle) {
|
||||
// first try to get a title
|
||||
title = Em.get(user, 'title');
|
||||
// if there was no title provided
|
||||
if (!title) {
|
||||
// try to retrieve a description
|
||||
var description = Em.get(user, 'description');
|
||||
const description = Em.get(user, 'description');
|
||||
// if a description has been provided
|
||||
if (description && description.length > 0) {
|
||||
// preprend the username before the description
|
||||
|
@ -27,14 +26,11 @@ function renderAvatar(user, options) {
|
|||
}
|
||||
}
|
||||
|
||||
// this is simply done to ensure we cache images correctly
|
||||
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
|
||||
|
||||
return Discourse.Utilities.avatarImg({
|
||||
size: options.imageSize,
|
||||
extraClasses: Em.get(user, 'extras') || options.extraClasses,
|
||||
title: title || username,
|
||||
avatarTemplate: avatarTemplate(username, uploadedAvatarId)
|
||||
avatarTemplate: avatarTemplate
|
||||
});
|
||||
} else {
|
||||
return '';
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import interceptClick from 'discourse/lib/intercept-click';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default {
|
||||
name: "click-interceptor",
|
||||
initialize() {
|
||||
$('#main').on('click.discourse', 'a', interceptClick);
|
||||
$(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
let _splitAvatars;
|
||||
|
||||
function defaultAvatar(username) {
|
||||
const defaultAvatars = Discourse.SiteSettings.default_avatars;
|
||||
if (defaultAvatars && defaultAvatars.length) {
|
||||
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
|
||||
|
||||
if (_splitAvatars.length) {
|
||||
const hash = hashString(username);
|
||||
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
|
||||
}
|
||||
}
|
||||
|
||||
return Discourse.getURLWithCDN("/letter_avatar/" +
|
||||
username.toLowerCase() +
|
||||
"/{size}/" +
|
||||
Discourse.LetterAvatarVersion + ".png");
|
||||
}
|
||||
|
||||
export default function(username, uploadedAvatarId) {
|
||||
if (uploadedAvatarId) {
|
||||
return Discourse.getURLWithCDN("/user_avatar/" +
|
||||
Discourse.BaseUrl +
|
||||
"/" +
|
||||
username.toLowerCase() +
|
||||
"/{size}/" +
|
||||
uploadedAvatarId + ".png");
|
||||
}
|
||||
return defaultAvatar(username);
|
||||
}
|
|
@ -32,6 +32,7 @@ KeyValueStore.prototype = {
|
|||
},
|
||||
|
||||
remove(key) {
|
||||
if (!safeLocalStorage) { return; }
|
||||
return safeLocalStorage.removeItem(this.context + key);
|
||||
},
|
||||
|
||||
|
|
|
@ -106,4 +106,12 @@ const getSearchKey = function(args){
|
|||
((args.searchContext && args.searchContext.id) || "")
|
||||
};
|
||||
|
||||
export { searchForTerm, searchContextDescription, getSearchKey };
|
||||
const isValidSearchTerm = function(searchTerm) {
|
||||
if (searchTerm) {
|
||||
return searchTerm.trim().length >= Discourse.SiteSettings.min_search_term_length;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export { searchForTerm, searchContextDescription, getSearchKey, isValidSearchTerm };
|
||||
|
|
|
@ -105,7 +105,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
|||
It contains the logic necessary to route within a topic using replaceState to
|
||||
keep the history intact.
|
||||
**/
|
||||
routeTo: function(path, opts) {
|
||||
routeTo(path, opts) {
|
||||
if (Em.isEmpty(path)) { return; }
|
||||
|
||||
if (Discourse.get('requiresRefresh')) {
|
||||
|
@ -122,6 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
|||
// Scroll to the same page, different anchor
|
||||
if (path.indexOf('#') === 0) {
|
||||
this.scrollToId(path);
|
||||
history.replaceState(undefined, undefined, path);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ const CLOSED = 'closed',
|
|||
category: 'categoryId',
|
||||
topic_id: 'topic.id',
|
||||
is_warning: 'isWarning',
|
||||
whisper: 'whisper',
|
||||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
|
@ -557,6 +558,9 @@ const Composer = RestModel.extend({
|
|||
|
||||
let addedToStream = false;
|
||||
|
||||
const postTypes = this.site.get('post_types');
|
||||
const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular;
|
||||
|
||||
// Build the post object
|
||||
const createdPost = this.store.createRecord('post', {
|
||||
imageSizes: opts.imageSizes,
|
||||
|
@ -567,9 +571,9 @@ const Composer = RestModel.extend({
|
|||
username: user.get('username'),
|
||||
user_id: user.get('id'),
|
||||
user_title: user.get('title'),
|
||||
uploaded_avatar_id: user.get('uploaded_avatar_id'),
|
||||
avatar_template: user.get('avatar_template'),
|
||||
user_custom_fields: user.get('custom_fields'),
|
||||
post_type: this.site.get('post_types.regular'),
|
||||
post_type: postType,
|
||||
actions_summary: [],
|
||||
moderator: user.get('moderator'),
|
||||
admin: user.get('admin'),
|
||||
|
@ -587,7 +591,7 @@ const Composer = RestModel.extend({
|
|||
reply_to_post_number: post.get('post_number'),
|
||||
reply_to_user: {
|
||||
username: post.get('username'),
|
||||
uploaded_avatar_id: post.get('uploaded_avatar_id')
|
||||
avatar_template: post.get('avatar_template')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import ActionSummary from 'discourse/models/action-summary';
|
||||
import { url, fmt, propertyEqual } from 'discourse/lib/computed';
|
||||
import { url, propertyEqual } from 'discourse/lib/computed';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
|
@ -77,7 +77,6 @@ const Post = RestModel.extend({
|
|||
|
||||
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),
|
||||
hasHistory: Em.computed.gt('version', 1),
|
||||
postElementId: fmt('post_number', 'post_%@'),
|
||||
|
||||
canViewRawEmail: function() {
|
||||
return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff');
|
||||
|
|
|
@ -174,8 +174,8 @@ const TopicTrackingState = Discourse.Model.extend({
|
|||
if (filter === "new") {
|
||||
list.topics.splice(i, 1);
|
||||
} else {
|
||||
list.topics[i].unseen = false;
|
||||
list.topics[i].dont_sync = true;
|
||||
list.topics[i].set('unseen', false);
|
||||
list.topics[i].set('dont_sync', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const UserActionTypes = {
|
||||
likes_given: 1,
|
||||
|
@ -17,21 +19,22 @@ const UserActionTypes = {
|
|||
};
|
||||
const InvertedActionTypes = {};
|
||||
|
||||
_.each(UserActionTypes, function (k, v) {
|
||||
_.each(UserActionTypes, (k, v) => {
|
||||
InvertedActionTypes[k] = v;
|
||||
});
|
||||
|
||||
const UserAction = RestModel.extend({
|
||||
|
||||
_attachCategory: function() {
|
||||
@on("init")
|
||||
_attachCategory() {
|
||||
const categoryId = this.get('category_id');
|
||||
if (categoryId) {
|
||||
this.set('category', Discourse.Category.findById(categoryId));
|
||||
}
|
||||
}.on('init'),
|
||||
},
|
||||
|
||||
descriptionKey: function() {
|
||||
const action = this.get('action_type');
|
||||
@computed("action_type")
|
||||
descriptionKey(action) {
|
||||
if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) {
|
||||
if (this.get('isPM')) {
|
||||
return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user';
|
||||
|
@ -59,34 +62,39 @@ const UserAction = RestModel.extend({
|
|||
return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user';
|
||||
}
|
||||
}
|
||||
}.property('action_type'),
|
||||
},
|
||||
|
||||
sameUser: function() {
|
||||
return this.get('username') === Discourse.User.currentProp('username');
|
||||
}.property('username'),
|
||||
@computed("username")
|
||||
sameUser(username) {
|
||||
return username === Discourse.User.currentProp('username');
|
||||
},
|
||||
|
||||
targetUser: function() {
|
||||
return this.get('target_username') === Discourse.User.currentProp('username');
|
||||
}.property('target_username'),
|
||||
@computed("target_username")
|
||||
targetUser(targetUsername) {
|
||||
return targetUsername === Discourse.User.currentProp('username');
|
||||
},
|
||||
|
||||
presentName: Em.computed.any('name', 'username'),
|
||||
targetDisplayName: Em.computed.any('target_name', 'target_username'),
|
||||
actingDisplayName: Em.computed.any('acting_name', 'acting_username'),
|
||||
targetUserUrl: url('target_username', '/users/%@'),
|
||||
|
||||
usernameLower: function() {
|
||||
return this.get('username').toLowerCase();
|
||||
}.property('username'),
|
||||
@computed("username")
|
||||
usernameLower(username) {
|
||||
return username.toLowerCase();
|
||||
},
|
||||
|
||||
userUrl: url('usernameLower', '/users/%@'),
|
||||
|
||||
postUrl: function() {
|
||||
@computed()
|
||||
postUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
|
||||
}.property(),
|
||||
},
|
||||
|
||||
replyUrl: function() {
|
||||
@computed()
|
||||
replyUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
|
||||
}.property(),
|
||||
},
|
||||
|
||||
replyType: Em.computed.equal('action_type', UserActionTypes.replies),
|
||||
postType: Em.computed.equal('action_type', UserActionTypes.posts),
|
||||
|
@ -99,7 +107,7 @@ const UserAction = RestModel.extend({
|
|||
postReplyType: Em.computed.or('postType', 'replyType'),
|
||||
removableBookmark: Em.computed.and('bookmarkType', 'sameUser'),
|
||||
|
||||
addChild: function(action) {
|
||||
addChild(action) {
|
||||
let groups = this.get("childGroups");
|
||||
if (!groups) {
|
||||
groups = {
|
||||
|
@ -143,22 +151,21 @@ const UserAction = RestModel.extend({
|
|||
"childGroups.edits.items", "childGroups.edits.items.@each",
|
||||
"childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"),
|
||||
|
||||
switchToActing: function() {
|
||||
switchToActing() {
|
||||
this.setProperties({
|
||||
username: this.get('acting_username'),
|
||||
uploaded_avatar_id: this.get('acting_uploaded_avatar_id'),
|
||||
name: this.get('actingDisplayName')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
UserAction.reopenClass({
|
||||
collapseStream: function(stream) {
|
||||
collapseStream(stream) {
|
||||
const uniq = {};
|
||||
const collapsed = [];
|
||||
let pos = 0;
|
||||
|
||||
stream.forEach(function(item) {
|
||||
stream.forEach(item => {
|
||||
const key = "" + item.topic_id + "-" + item.post_number;
|
||||
const found = uniq[key];
|
||||
if (found === void 0) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { url } from 'discourse/lib/computed';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import UserStream from 'discourse/models/user-stream';
|
||||
import UserPostsStream from 'discourse/models/user-posts-stream';
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
import { longDate } from 'discourse/lib/formatter';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Badge from 'discourse/models/badge';
|
||||
import UserBadge from 'discourse/models/user-badge';
|
||||
|
||||
|
@ -18,13 +18,15 @@ const User = RestModel.extend({
|
|||
hasNotPosted: Em.computed.not("hasPosted"),
|
||||
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
|
||||
|
||||
stream: function() {
|
||||
@computed()
|
||||
stream() {
|
||||
return UserStream.create({ user: this });
|
||||
}.property(),
|
||||
},
|
||||
|
||||
postsStream: function() {
|
||||
@computed()
|
||||
postsStream() {
|
||||
return UserPostsStream.create({ user: this });
|
||||
}.property(),
|
||||
},
|
||||
|
||||
staff: Em.computed.or('admin', 'moderator'),
|
||||
|
||||
|
@ -32,27 +34,22 @@ const User = RestModel.extend({
|
|||
return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'});
|
||||
},
|
||||
|
||||
searchContext: function() {
|
||||
@computed("username_lower")
|
||||
searchContext(username) {
|
||||
return {
|
||||
type: 'user',
|
||||
id: this.get('username_lower'),
|
||||
id: username,
|
||||
user: this
|
||||
};
|
||||
}.property('username_lower'),
|
||||
},
|
||||
|
||||
/**
|
||||
This user's display name. Returns the name if possible, otherwise returns the
|
||||
username.
|
||||
|
||||
@property displayName
|
||||
@type {String}
|
||||
**/
|
||||
displayName: function() {
|
||||
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) {
|
||||
return this.get('name');
|
||||
@computed("username", "name")
|
||||
displayName(username, name) {
|
||||
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
return this.get('username');
|
||||
}.property('username', 'name'),
|
||||
return username;
|
||||
},
|
||||
|
||||
@computed('profile_background')
|
||||
profileBackground(bgUrl) {
|
||||
|
@ -60,38 +57,23 @@ const User = RestModel.extend({
|
|||
return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe();
|
||||
},
|
||||
|
||||
path: function(){
|
||||
return Discourse.getURL('/users/' + this.get('username_lower'));
|
||||
@computed()
|
||||
path() {
|
||||
// no need to observe, requires a hard refresh to update
|
||||
}.property(),
|
||||
return Discourse.getURL(`/users/${this.get('username_lower')}`);
|
||||
},
|
||||
|
||||
/**
|
||||
Path to this user's administration
|
||||
|
||||
@property adminPath
|
||||
@type {String}
|
||||
**/
|
||||
adminPath: url('username_lower', "/admin/users/%@"),
|
||||
|
||||
/**
|
||||
This user's username in lowercase.
|
||||
@computed("username")
|
||||
username_lower(username) {
|
||||
return username.toLowerCase();
|
||||
},
|
||||
|
||||
@property username_lower
|
||||
@type {String}
|
||||
**/
|
||||
username_lower: function() {
|
||||
return this.get('username').toLowerCase();
|
||||
}.property('username'),
|
||||
|
||||
/**
|
||||
This user's trust level.
|
||||
|
||||
@property trustLevel
|
||||
@type {Integer}
|
||||
**/
|
||||
trustLevel: function() {
|
||||
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10));
|
||||
}.property('trust_level'),
|
||||
@computed("trust_level")
|
||||
trustLevel(trustLevel) {
|
||||
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10));
|
||||
},
|
||||
|
||||
isBasic: Em.computed.equal('trust_level', 0),
|
||||
isLeader: Em.computed.equal('trust_level', 3),
|
||||
|
@ -100,61 +82,36 @@ const User = RestModel.extend({
|
|||
|
||||
isSuspended: Em.computed.equal('suspended', true),
|
||||
|
||||
suspended: function() {
|
||||
return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter();
|
||||
}.property('suspended_till'),
|
||||
@computed("suspended_till")
|
||||
suspended(suspendedTill) {
|
||||
return suspendedTill && moment(suspendedTill).isAfter();
|
||||
},
|
||||
|
||||
suspendedTillDate: function() {
|
||||
return longDate(this.get('suspended_till'));
|
||||
}.property('suspended_till'),
|
||||
@computed("suspended_till")
|
||||
suspendedTillDate(suspendedTill) {
|
||||
return longDate(suspendedTill);
|
||||
},
|
||||
|
||||
/**
|
||||
Changes this user's username.
|
||||
|
||||
@method changeUsername
|
||||
@param {String} newUsername The user's new username
|
||||
@returns Result of ajax call
|
||||
**/
|
||||
changeUsername: function(newUsername) {
|
||||
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", {
|
||||
changeUsername(new_username) {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, {
|
||||
type: 'PUT',
|
||||
data: { new_username: newUsername }
|
||||
data: { new_username }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Changes this user's email address.
|
||||
|
||||
@method changeEmail
|
||||
@param {String} email The user's new email address\
|
||||
@returns Result of ajax call
|
||||
**/
|
||||
changeEmail: function(email) {
|
||||
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", {
|
||||
changeEmail(email) {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, {
|
||||
type: 'PUT',
|
||||
data: { email: email }
|
||||
data: { email }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a copy of this user.
|
||||
|
||||
@method copy
|
||||
@returns {User}
|
||||
**/
|
||||
copy: function() {
|
||||
copy() {
|
||||
return Discourse.User.create(this.getProperties(Ember.keys(this)));
|
||||
},
|
||||
|
||||
/**
|
||||
Save's this user's properties over AJAX via a PUT request.
|
||||
|
||||
@method save
|
||||
@returns {Promise} the result of the operation
|
||||
**/
|
||||
save: function() {
|
||||
const self = this,
|
||||
data = this.getProperties(
|
||||
save() {
|
||||
const data = this.getProperties(
|
||||
'auto_track_topics_after_msecs',
|
||||
'bio_raw',
|
||||
'website',
|
||||
|
@ -179,10 +136,10 @@ const User = RestModel.extend({
|
|||
'card_background'
|
||||
);
|
||||
|
||||
['muted','watched','tracked'].forEach(function(s){
|
||||
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
|
||||
['muted','watched','tracked'].forEach(s => {
|
||||
let cats = this.get(s + 'Categories').map(c => c.get('id'));
|
||||
// HACK: denote lack of categories
|
||||
if(cats.length === 0) { cats = [-1]; }
|
||||
if (cats.length === 0) { cats = [-1]; }
|
||||
data[s + '_category_ids'] = cats;
|
||||
});
|
||||
|
||||
|
@ -192,26 +149,19 @@ const User = RestModel.extend({
|
|||
|
||||
// TODO: We can remove this when migrated fully to rest model.
|
||||
this.set('isSaving', true);
|
||||
return Discourse.ajax("/users/" + this.get('username_lower'), {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}`, {
|
||||
data: data,
|
||||
type: 'PUT'
|
||||
}).then(function(result) {
|
||||
self.set('bio_excerpt', result.user.bio_excerpt);
|
||||
|
||||
const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
|
||||
}).then(result => {
|
||||
this.set('bio_excerpt', result.user.bio_excerpt);
|
||||
const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
|
||||
Discourse.User.current().setProperties(userProps);
|
||||
}).finally(() => {
|
||||
this.set('isSaving', false);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Changes the password and calls the callback function on AJAX.complete.
|
||||
|
||||
@method changePassword
|
||||
@returns {Promise} the result of the change password operation
|
||||
**/
|
||||
changePassword: function() {
|
||||
changePassword() {
|
||||
return Discourse.ajax("/session/forgot_password", {
|
||||
dataType: 'json',
|
||||
data: { login: this.get('username') },
|
||||
|
@ -219,73 +169,63 @@ const User = RestModel.extend({
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Loads a single user action by id.
|
||||
|
||||
@method loadUserAction
|
||||
@param {Integer} id The id of the user action being loaded
|
||||
@returns A stream of the user's actions containing the action of id
|
||||
**/
|
||||
loadUserAction: function(id) {
|
||||
var self = this,
|
||||
stream = this.get('stream');
|
||||
return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) {
|
||||
loadUserAction(id) {
|
||||
const stream = this.get('stream');
|
||||
return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {
|
||||
if (result && result.user_action) {
|
||||
var ua = result.user_action;
|
||||
const ua = result.user_action;
|
||||
|
||||
if ((self.get('stream.filter') || ua.action_type) !== ua.action_type) return;
|
||||
if (!self.get('stream.filter') && !self.inAllStream(ua)) return;
|
||||
if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return;
|
||||
if (!this.get('stream.filter') && !this.inAllStream(ua)) return;
|
||||
|
||||
var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]);
|
||||
const action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]);
|
||||
stream.set('itemsLoaded', stream.get('itemsLoaded') + 1);
|
||||
stream.get('content').insertAt(0, action[0]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
inAllStream: function(ua) {
|
||||
inAllStream(ua) {
|
||||
return ua.action_type === Discourse.UserAction.TYPES.posts ||
|
||||
ua.action_type === Discourse.UserAction.TYPES.topics;
|
||||
},
|
||||
|
||||
// The user's stat count, excluding PMs.
|
||||
statsCountNonPM: function() {
|
||||
var self = this;
|
||||
|
||||
@computed("statsExcludingPms.@each.count")
|
||||
statsCountNonPM() {
|
||||
if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0;
|
||||
var count = 0;
|
||||
_.each(this.get('statsExcludingPms'), function(val) {
|
||||
if (self.inAllStream(val)){
|
||||
let count = 0;
|
||||
_.each(this.get('statsExcludingPms'), val => {
|
||||
if (this.inAllStream(val)) {
|
||||
count += val.count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}.property('statsExcludingPms.@each.count'),
|
||||
},
|
||||
|
||||
// The user's stats, excluding PMs.
|
||||
statsExcludingPms: function() {
|
||||
@computed("stats.@each.isPM")
|
||||
statsExcludingPms() {
|
||||
if (Ember.isEmpty(this.get('stats'))) return [];
|
||||
return this.get('stats').rejectProperty('isPM');
|
||||
}.property('stats.@each.isPM'),
|
||||
},
|
||||
|
||||
findDetails: function(options) {
|
||||
var user = this;
|
||||
findDetails(options) {
|
||||
const user = this;
|
||||
|
||||
return PreloadStore.getAndRemove("user_" + user.get('username'), function() {
|
||||
return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options});
|
||||
}).then(function (json) {
|
||||
return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => {
|
||||
return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options });
|
||||
}).then(json => {
|
||||
|
||||
if (!Em.isEmpty(json.user.stats)) {
|
||||
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) {
|
||||
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => {
|
||||
if (s.count) s.count = parseInt(s.count, 10);
|
||||
return Discourse.UserActionStat.create(s);
|
||||
}));
|
||||
}
|
||||
|
||||
if (!Em.isEmpty(json.user.custom_groups)) {
|
||||
json.user.custom_groups = json.user.custom_groups.map(function (g) {
|
||||
return Discourse.Group.create(g);
|
||||
});
|
||||
json.user.custom_groups = json.user.custom_groups.map(g => Discourse.Group.create(g));
|
||||
}
|
||||
|
||||
if (json.user.invited_by) {
|
||||
|
@ -294,12 +234,10 @@ const User = RestModel.extend({
|
|||
|
||||
if (!Em.isEmpty(json.user.featured_user_badge_ids)) {
|
||||
const userBadgesMap = {};
|
||||
UserBadge.createFromJson(json).forEach(function(userBadge) {
|
||||
UserBadge.createFromJson(json).forEach(userBadge => {
|
||||
userBadgesMap[ userBadge.get('id') ] = userBadge;
|
||||
});
|
||||
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) {
|
||||
return userBadgesMap[id];
|
||||
});
|
||||
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]);
|
||||
}
|
||||
|
||||
if (json.user.card_badge) {
|
||||
|
@ -311,81 +249,62 @@ const User = RestModel.extend({
|
|||
});
|
||||
},
|
||||
|
||||
findStaffInfo: function() {
|
||||
findStaffInfo() {
|
||||
if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); }
|
||||
var self = this;
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/staff-info.json").then(function(info) {
|
||||
self.setProperties(info);
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => {
|
||||
this.setProperties(info);
|
||||
});
|
||||
},
|
||||
|
||||
avatarTemplate: function() {
|
||||
return avatarTemplate(this.get('username'), this.get('uploaded_avatar_id'));
|
||||
}.property('uploaded_avatar_id', 'username'),
|
||||
|
||||
/*
|
||||
Change avatar selection
|
||||
*/
|
||||
pickAvatar: function(uploadId) {
|
||||
var self = this;
|
||||
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", {
|
||||
pickAvatar(upload_id, type, avatar_template) {
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
|
||||
type: 'PUT',
|
||||
data: { upload_id: uploadId }
|
||||
}).then(function(){
|
||||
self.set('uploaded_avatar_id', uploadId);
|
||||
});
|
||||
data: { upload_id, type }
|
||||
}).then(() => this.setProperties({
|
||||
avatar_template,
|
||||
uploaded_avatar_id: upload_id
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
Determines whether the current user is allowed to upload a file.
|
||||
|
||||
@method isAllowedToUploadAFile
|
||||
@param {String} type The type of the upload (image, attachment)
|
||||
@returns true if the current user is allowed to upload a file
|
||||
**/
|
||||
isAllowedToUploadAFile: function(type) {
|
||||
isAllowedToUploadAFile(type) {
|
||||
return this.get('staff') ||
|
||||
this.get('trust_level') > 0 ||
|
||||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
Invite a user to the site
|
||||
|
||||
@method createInvite
|
||||
@param {String} email The email address of the user to invite to the site
|
||||
@returns {Promise} the result of the server call
|
||||
**/
|
||||
createInvite: function(email, groupNames) {
|
||||
createInvite(email, group_names) {
|
||||
return Discourse.ajax('/invites', {
|
||||
type: 'POST',
|
||||
data: {email: email, group_names: groupNames}
|
||||
data: { email, group_names }
|
||||
});
|
||||
},
|
||||
|
||||
generateInviteLink: function(email, groupNames, topicId) {
|
||||
generateInviteLink(email, group_names, topic_id) {
|
||||
return Discourse.ajax('/invites/link', {
|
||||
type: 'POST',
|
||||
data: {email: email, group_names: groupNames, topic_id: topicId}
|
||||
data: { email, group_names, topic_id }
|
||||
});
|
||||
},
|
||||
|
||||
updateMutedCategories: function() {
|
||||
@observes("muted_category_ids")
|
||||
updateMutedCategories() {
|
||||
this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids));
|
||||
}.observes("muted_category_ids"),
|
||||
},
|
||||
|
||||
updateTrackedCategories: function() {
|
||||
@observes("tracked_category_ids")
|
||||
updateTrackedCategories() {
|
||||
this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids));
|
||||
}.observes("tracked_category_ids"),
|
||||
},
|
||||
|
||||
updateWatchedCategories: function() {
|
||||
@observes("watched_category_ids")
|
||||
updateWatchedCategories() {
|
||||
this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids));
|
||||
}.observes("watched_category_ids"),
|
||||
},
|
||||
|
||||
canDeleteAccount: function() {
|
||||
return !Discourse.SiteSettings.enable_sso && this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1;
|
||||
}.property('can_delete_account', 'reply_count', 'topic_count'),
|
||||
@computed("can_delete_account", "reply_count", "topic_count")
|
||||
canDeleteAccount(canDeleteAccount, replyCount, topicCount) {
|
||||
return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1;
|
||||
},
|
||||
|
||||
"delete": function() {
|
||||
if (this.get('can_delete_account')) {
|
||||
|
@ -398,27 +317,26 @@ const User = RestModel.extend({
|
|||
}
|
||||
},
|
||||
|
||||
dismissBanner: function (bannerKey) {
|
||||
dismissBanner(bannerKey) {
|
||||
this.set("dismissed_banner_key", bannerKey);
|
||||
Discourse.ajax("/users/" + this.get('username'), {
|
||||
Discourse.ajax(`/users/${this.get('username')}`, {
|
||||
type: 'PUT',
|
||||
data: { dismissed_banner_key: bannerKey }
|
||||
});
|
||||
},
|
||||
|
||||
checkEmail: function () {
|
||||
var self = this;
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/emails.json", {
|
||||
checkEmail() {
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, {
|
||||
type: "PUT",
|
||||
data: { context: window.location.pathname }
|
||||
}).then(function (result) {
|
||||
}).then(result => {
|
||||
if (result) {
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
email: result.email,
|
||||
associated_accounts: result.associated_accounts
|
||||
});
|
||||
}
|
||||
}, function () {});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -426,14 +344,14 @@ const User = RestModel.extend({
|
|||
User.reopenClass(Singleton, {
|
||||
|
||||
// Find a `Discourse.User` for a given username.
|
||||
findByUsername: function(username, options) {
|
||||
findByUsername(username, options) {
|
||||
const user = User.create({username: username});
|
||||
return user.findDetails(options);
|
||||
},
|
||||
|
||||
// TODO: Use app.register and junk Singleton
|
||||
createCurrent: function() {
|
||||
var userJson = PreloadStore.get('currentUser');
|
||||
createCurrent() {
|
||||
const userJson = PreloadStore.get('currentUser');
|
||||
if (userJson) {
|
||||
const store = Discourse.__container__.lookup('store:main');
|
||||
return store.createRecord('user', userJson);
|
||||
|
@ -441,56 +359,38 @@ User.reopenClass(Singleton, {
|
|||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
Checks if given username is valid for this email address
|
||||
|
||||
@method checkUsername
|
||||
@param {String} username A username to check
|
||||
@param {String} email An email address to check
|
||||
@param {Number} forUserId user id - provide when changing username
|
||||
**/
|
||||
checkUsername: function(username, email, forUserId) {
|
||||
checkUsername(username, email, for_user_id) {
|
||||
return Discourse.ajax('/users/check_username', {
|
||||
data: { username: username, email: email, for_user_id: forUserId }
|
||||
data: { username, email, for_user_id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Groups the user's statistics
|
||||
|
||||
@method groupStats
|
||||
@param {Array} stats Given stats
|
||||
@returns {Object}
|
||||
**/
|
||||
groupStats: function(stats) {
|
||||
var responses = Discourse.UserActionStat.create({
|
||||
groupStats(stats) {
|
||||
const responses = Discourse.UserActionStat.create({
|
||||
count: 0,
|
||||
action_type: Discourse.UserAction.TYPES.replies
|
||||
});
|
||||
|
||||
stats.filterProperty('isResponse').forEach(function (stat) {
|
||||
stats.filterProperty('isResponse').forEach(stat => {
|
||||
responses.set('count', responses.get('count') + stat.get('count'));
|
||||
});
|
||||
|
||||
var result = Em.A();
|
||||
const result = Em.A();
|
||||
result.pushObjects(stats.rejectProperty('isResponse'));
|
||||
|
||||
var insertAt = 0;
|
||||
result.forEach(function(item, index){
|
||||
if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){
|
||||
let insertAt = 0;
|
||||
result.forEach((item, index) => {
|
||||
if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) {
|
||||
insertAt = index + 1;
|
||||
}
|
||||
});
|
||||
if(responses.count > 0) {
|
||||
if (responses.count > 0) {
|
||||
result.insertAt(insertAt, responses);
|
||||
}
|
||||
return(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Creates a new account
|
||||
**/
|
||||
createAccount: function(attrs) {
|
||||
createAccount(attrs) {
|
||||
return Discourse.ajax("/users", {
|
||||
data: {
|
||||
name: attrs.accountName,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { translateResults, getSearchKey } from "discourse/lib/search";
|
||||
import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: { q: {}, context_id: {}, context: {} },
|
||||
|
@ -23,7 +23,7 @@ export default Discourse.Route.extend({
|
|||
}
|
||||
|
||||
return PreloadStore.getAndRemove("search", function() {
|
||||
if (params.q && params.q.length > 2) {
|
||||
if (isValidSearchTerm(params.q)) {
|
||||
return Discourse.ajax("/search", { data: args });
|
||||
} else {
|
||||
return null;
|
||||
|
|
|
@ -18,50 +18,52 @@ export default RestrictedUserRoute.extend({
|
|||
showModal('avatar-selector');
|
||||
|
||||
// all the properties needed for displaying the avatar selector modal
|
||||
const controller = this.controllerFor('avatar-selector'),
|
||||
props = this.modelFor('user').getProperties(
|
||||
const props = this.modelFor('user').getProperties(
|
||||
'id',
|
||||
'email',
|
||||
'username',
|
||||
'uploaded_avatar_id',
|
||||
'avatar_template',
|
||||
'system_avatar_template',
|
||||
'gravatar_avatar_template',
|
||||
'custom_avatar_template',
|
||||
'system_avatar_upload_id',
|
||||
'gravatar_avatar_upload_id',
|
||||
'custom_avatar_upload_id'
|
||||
);
|
||||
|
||||
switch (props.uploaded_avatar_id) {
|
||||
case props.system_avatar_upload_id:
|
||||
switch (props.avatar_template) {
|
||||
case props.system_avatar_template:
|
||||
props.selected = "system";
|
||||
break;
|
||||
case props.gravatar_avatar_upload_id:
|
||||
case props.gravatar_avatar_template:
|
||||
props.selected = "gravatar";
|
||||
break;
|
||||
default:
|
||||
props.selected = "uploaded";
|
||||
}
|
||||
|
||||
controller.setProperties(props);
|
||||
this.controllerFor('avatar-selector').setProperties(props);
|
||||
},
|
||||
|
||||
saveAvatarSelection() {
|
||||
const user = this.modelFor('user'),
|
||||
avatarSelector = this.controllerFor('avatar-selector');
|
||||
controller = this.controllerFor('avatar-selector'),
|
||||
selectedUploadId = controller.get("selectedUploadId"),
|
||||
selectedAvatarTemplate = controller.get("selectedAvatarTemplate"),
|
||||
type = controller.get("selected");
|
||||
|
||||
// sends the information to the server if it has changed
|
||||
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
|
||||
user.pickAvatar(avatarSelector.get('selectedUploadId'))
|
||||
.then(() => {
|
||||
user.setProperties(avatarSelector.getProperties(
|
||||
'system_avatar_upload_id',
|
||||
'gravatar_avatar_upload_id',
|
||||
'custom_avatar_upload_id'
|
||||
));
|
||||
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
|
||||
});
|
||||
}
|
||||
user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate)
|
||||
.then(() => {
|
||||
user.setProperties(controller.getProperties(
|
||||
'system_avatar_template',
|
||||
'gravatar_avatar_template',
|
||||
'custom_avatar_template'
|
||||
));
|
||||
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
|
||||
});
|
||||
|
||||
// saves the data back
|
||||
avatarSelector.send('closeModal');
|
||||
controller.send('closeModal');
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<section class='field'>
|
||||
{{#if category.is_special}}
|
||||
<p class="warning">{{i18n 'category.special_warning'}}</p>
|
||||
{{/if}}
|
||||
<ul class='permission-list'>
|
||||
{{#each category.permissions as |p|}}
|
||||
<li>
|
||||
|
@ -16,6 +19,8 @@
|
|||
{{view 'select' class="permission-selector" optionValuePath="content.id" optionLabelPath="content.description" content=category.availablePermissions value=selectedPermission}}
|
||||
<button {{action "addPermission" selectedGroup selectedPermission}} class="btn btn-small">{{i18n 'category.add_permission'}}</button>
|
||||
{{else}}
|
||||
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
|
||||
{{#unless category.is_special}}
|
||||
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</section>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
|
||||
</button>
|
||||
{{else}}
|
||||
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}</div></a>
|
||||
{{#if grandChild.edit_reason}} — <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
|
|
@ -60,6 +60,16 @@
|
|||
{{/unless}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if canWhisper}}
|
||||
<div class='form-element clearfix'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.whisper tabindex="3"}}
|
||||
{{i18n "composer.add_whisper"}}
|
||||
</label>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "composer-fields"}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="search row clearfix">
|
||||
{{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}}
|
||||
{{d-button action="search" icon="search" class="btn-primary"}}
|
||||
{{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search" hasAutofocus=hasAutofocus}}
|
||||
{{d-button action="search" icon="search" class="btn-primary" disabled=isNotValidSearchTerm}}
|
||||
{{#if canBulkSelect}}
|
||||
{{#if model.posts}}
|
||||
{{d-button icon="list" class="bulk-select" title="topics.bulk.toggle" action="toggleBulkSelect"}}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{{#if view.showBadges}}
|
||||
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
|
||||
{{else}}
|
||||
{{raw "list/posts-count-column" topic=topic tagName="div"}}
|
||||
{{/if}}
|
|
@ -1,5 +1,5 @@
|
|||
<td class='posters'>
|
||||
{{#each poster in posters}}
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster usernamePath="user.username" imageSize="small"}}</a>
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
|
||||
{{/each}}
|
||||
</td>
|
||||
|
|
|
@ -5,13 +5,9 @@
|
|||
{{#each t in topics}}
|
||||
<tr {{bind-attr class="t.archived"}}>
|
||||
<td>
|
||||
<div class='main-link clearfix'>
|
||||
<div class='main-link'>
|
||||
{{topic-status topic=t}}
|
||||
{{topic-link t}}
|
||||
{{topic-post-badges unread=t.unread
|
||||
newPosts=t.new_posts
|
||||
unseen=t.unseen
|
||||
url=t.lastUnreadUrl}}
|
||||
|
||||
{{#if t.hasExcerpt}}
|
||||
<div class="topic-excerpt">
|
||||
|
@ -25,10 +21,14 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='pull-right'>
|
||||
{{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}}
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
<div class="topic-item-stats clearfix">
|
||||
<div class="pull-right">
|
||||
{{raw "list/posts-count-column" topic=t tagName="div"}}
|
||||
{{raw "list/activity-column" topic=t tagName="div" class="num activity last"}}
|
||||
<a href="{{t.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date t.bumped_at}}}'>{{t.last_poster_username}}</a>
|
||||
</div>
|
||||
{{#unless controller.hideCategory}}
|
||||
<div class='category'>
|
||||
|
|
|
@ -2,13 +2,10 @@
|
|||
<div class='main-link'>
|
||||
{{raw "topic-status" topic=content}}
|
||||
{{topic-link content}}
|
||||
{{#if controller.showTopicPostBadges}}
|
||||
{{raw "topic-post-badges" unread=content.unread newPosts=content.displayNewPosts unseen=content.unseen url=content.lastUnreadUrl}}
|
||||
{{/if}}
|
||||
{{raw "list/topic-excerpt" topic=content}}
|
||||
</div>
|
||||
<div class='pull-right'>
|
||||
{{raw "list/posts-count-column" topic=content tagName="div"}}
|
||||
{{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
@ -22,8 +19,8 @@
|
|||
|
||||
<div class="pull-right">
|
||||
<div class='num activity last'>
|
||||
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
|
||||
{{raw "list/activity-column" topic=content tagName="span" class="age"}}
|
||||
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
|
@ -2,32 +2,27 @@
|
|||
<div>
|
||||
<div>
|
||||
<input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}>
|
||||
<label class="radio" for="system-avatar">{{bound-avatar controller "large" system_avatar_upload_id}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
|
||||
<label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}>
|
||||
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
|
||||
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
|
||||
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
|
||||
</div>
|
||||
{{#if allowImageUpload}}
|
||||
<div>
|
||||
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
|
||||
<label class="radio" for="uploaded_avatar">
|
||||
{{#if hasUploadedAvatar}}
|
||||
{{#if uploadedAvatarTemplate}}
|
||||
{{bound-avatar-template uploadedAvatarTemplate "large"}}
|
||||
{{else}}
|
||||
{{bound-avatar controller "large" custom_avatar_upload_id}}
|
||||
{{/if}}
|
||||
{{#if custom_avatar_template}}
|
||||
{{bound-avatar-template custom_avatar_template "large"}}
|
||||
{{i18n 'user.change_avatar.uploaded_avatar'}}
|
||||
{{else}}
|
||||
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{avatar-uploader username=username
|
||||
user_id=id
|
||||
uploadedAvatarTemplate=uploadedAvatarTemplate
|
||||
custom_avatar_upload_id=custom_avatar_upload_id
|
||||
{{avatar-uploader user_id=id
|
||||
uploadedAvatarTemplate=custom_avatar_template
|
||||
uploadedAvatarId=custom_avatar_upload_id
|
||||
uploading=uploading
|
||||
done="useUploadedAvatar"}}
|
||||
</div>
|
||||
|
@ -36,6 +31,6 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=saveDisabled label="save"}}
|
||||
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}}
|
||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{{view 'reply-history' content=replyHistory}}
|
||||
</div>
|
||||
|
||||
<article {{bind-attr class=":boxed via_email" id="postElementId" data-post-id="id" data-user-id="user_id"}}>
|
||||
<article class="boxed {{if via_email 'via-email'}}" id={{postElementId}} data-post-id={{id}} data-user-id={{user_id}}>
|
||||
<div class='row'>
|
||||
|
||||
<div class='topic-avatar'>
|
||||
|
@ -45,15 +45,20 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{#if wiki}}
|
||||
<div class="post-info wiki" title="{{i18n 'post.wiki.about'}}" {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div>
|
||||
<div class="post-info wiki" title={{i18n 'post.wiki.about'}} {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div>
|
||||
{{/if}}
|
||||
{{#if via_email}}
|
||||
{{#if canViewRawEmail}}
|
||||
<div class="post-info via-email raw-email" title="{{i18n 'post.via_email'}}" {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div>
|
||||
<div class="post-info via-email raw-email" title={{i18n 'post.via_email'}} {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div>
|
||||
{{else}}
|
||||
<div class="post-info via-email" title="{{i18n 'post.via_email'}}">{{fa-icon "envelope-o"}}</div>
|
||||
<div class="post-info via-email" title={{i18n 'post.via_email'}}>{{fa-icon "envelope-o"}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if view.whisper}}
|
||||
<div class="post-info whisper" title={{i18n 'post.whisper'}}>{{fa-icon "eye-slash"}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showUserReplyTab}}
|
||||
<a href {{action "toggleReplyHistory" this target="view"}} class='reply-to-tab'>
|
||||
{{#if loadingReplyHistory}}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { on, observes } from "ember-addons/ember-computed-decorators";
|
||||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
|
@ -6,11 +7,14 @@ export default ModalBodyView.extend({
|
|||
title: I18n.t('user.change_avatar.title'),
|
||||
|
||||
// *HACK* used to select the proper radio button, because {{action}} stops the default behavior
|
||||
selectedChanged: function() {
|
||||
@on("didInsertElement")
|
||||
@observes("controller.selected")
|
||||
selectedChanged() {
|
||||
Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')]));
|
||||
}.observes('controller.selected').on("didInsertElement"),
|
||||
},
|
||||
|
||||
_focusSelectedButton: function() {
|
||||
@on("didInsertElement")
|
||||
_focusSelectedButton() {
|
||||
Em.run.next(() => $('input:radio[value="' + this.get('controller.selected') + '"]').focus());
|
||||
}.on("didInsertElement")
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import userSearch from 'discourse/lib/user-search';
|
||||
import afterTransition from 'discourse/lib/after-transition';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import positioningWorkaround from 'discourse/lib/safari-hacks';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
|
@ -251,10 +250,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
|||
if (posts && topicId === self.get('controller.controllers.topic.model.id')) {
|
||||
const quotedPost = posts.findProperty("post_number", postNumber);
|
||||
if (quotedPost) {
|
||||
const username = quotedPost.get('username'),
|
||||
uploadId = quotedPost.get('uploaded_avatar_id');
|
||||
|
||||
return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId));
|
||||
return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export default Ember.Object.extend({
|
||||
postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts', 'topic.unseen'),
|
||||
showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent')
|
||||
});
|
|
@ -1,6 +1,8 @@
|
|||
import ScreenTrack from 'discourse/lib/screen-track';
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
|
||||
const DAY = 60 * 50 * 1000;
|
||||
|
||||
|
@ -12,10 +14,18 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
'post.deleted:deleted',
|
||||
'post.topicOwner:topic-owner',
|
||||
'groupNameClass',
|
||||
'post.wiki:wiki'],
|
||||
'post.wiki:wiki',
|
||||
'whisper'],
|
||||
|
||||
post: Ember.computed.alias('content'),
|
||||
|
||||
postElementId: fmt('post.post_number', 'post_%@'),
|
||||
|
||||
@computed('post.post_type')
|
||||
whisper(postType) {
|
||||
return postType === this.site.get('post_types.whisper');
|
||||
},
|
||||
|
||||
templateName: function() {
|
||||
return (this.get('post.post_type') === this.site.get('post_types.small_action')) ? 'post-small-action' : 'post';
|
||||
}.property('post.post_type'),
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
//= require ./discourse/lib/load-script
|
||||
//= require ./discourse/lib/notification-levels
|
||||
//= require ./discourse/lib/app-events
|
||||
//= require ./discourse/lib/avatar-template
|
||||
//= require ./discourse/lib/url
|
||||
//= require ./discourse/lib/debounce
|
||||
//= require ./discourse/lib/quote
|
||||
|
@ -41,7 +40,6 @@
|
|||
//= require ./discourse/lib/autocomplete
|
||||
//= require ./discourse/lib/after-transition
|
||||
//= require ./discourse/lib/debounce
|
||||
//= require ./discourse/lib/avatar-template
|
||||
//= require ./discourse/lib/safari-hacks
|
||||
//= require_tree ./discourse/adapters
|
||||
//= require ./discourse/models/rest
|
||||
|
|
|
@ -147,7 +147,7 @@ aside.quote {
|
|||
}
|
||||
|
||||
.post-info {
|
||||
&.wiki, &.via-email {
|
||||
&.wiki, &.via-email, &.whisper {
|
||||
margin-right: 5px;
|
||||
i.fa {
|
||||
font-size: 1em;
|
||||
|
@ -317,3 +317,12 @@ table.md-table {
|
|||
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.whisper {
|
||||
.topic-body {
|
||||
.cooked {
|
||||
font-style: italic;
|
||||
color: dark-light-diff($primary, $secondary, 55%, -40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -324,8 +324,8 @@
|
|||
}
|
||||
.title-input .popup-tip {
|
||||
width: 300px;
|
||||
left: 0px;
|
||||
top: -30px;
|
||||
margin-top: 8px;
|
||||
left: 150px;
|
||||
}
|
||||
.category-input .popup-tip {
|
||||
width: 240px;
|
||||
|
|
|
@ -14,8 +14,8 @@ article.post {
|
|||
}
|
||||
|
||||
.quote .title {
|
||||
border-left: 5px solid darken(dark-light-diff($primary, $secondary, 90%, -60%), 10%);
|
||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%);
|
||||
background-color: dark-light-diff($primary, $secondary, 97%, -45%);
|
||||
padding: 10px 10px 0 12px;
|
||||
.avatar { margin-right: 7px; }
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ article.post {
|
|||
blockquote {
|
||||
padding: 10px 8px 10px 13px;
|
||||
margin: 0 0 10px 0;
|
||||
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
border-left: 5px solid darken(dark-light-diff($primary, $secondary, 90%, -60%), 10%);
|
||||
background-color: dark-light-diff($primary, $secondary, 97%, -45%);
|
||||
border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%);
|
||||
overflow: hidden;
|
||||
p {
|
||||
margin: 0 0 10px 0;
|
||||
|
@ -137,9 +137,3 @@ footer {
|
|||
float: right;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #eee;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,17 @@
|
|||
.badge-notification {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
i {color: $secondary;}
|
||||
font-size: 1.071em;
|
||||
padding: 4px 6px 3px 6px;
|
||||
i {color: $secondary;}
|
||||
|
||||
&.new-topic::before {
|
||||
content: none;
|
||||
margin-right: 0;
|
||||
}
|
||||
&.new-topic {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-item-stats {
|
||||
|
@ -91,7 +101,7 @@
|
|||
.category a {
|
||||
max-width: 160px;
|
||||
}
|
||||
.num .fa {
|
||||
.num .fa, a, a:visited {
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +120,29 @@
|
|||
|
||||
// Category list
|
||||
// --------------------------------------------------
|
||||
.categories-list .list-container {
|
||||
margin-left: -10px; // Extend past the .wrap padding to the edge of the window
|
||||
}
|
||||
|
||||
.category-list-item.category {
|
||||
// Allow percentage widths on table cells to include their padding
|
||||
box-sizing: border-box;
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.main-link {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.posts {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.age {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
tr.category-topic-link {
|
||||
border-top: darken($secondary, 3%) 1px solid;
|
||||
|
@ -357,7 +390,7 @@ td .main-link {
|
|||
padding: 5px 10px 5px 0;
|
||||
}
|
||||
}
|
||||
.topic-list-item {
|
||||
.topic-list {
|
||||
.posts-map {
|
||||
font-size: 1.071em;
|
||||
padding-top: 2px;
|
||||
|
|
|
@ -465,6 +465,10 @@ class PostsController < ApplicationController
|
|||
result[:is_warning] = false
|
||||
end
|
||||
|
||||
if SiteSetting.enable_whispers? && params[:whisper] == "true"
|
||||
result[:post_type] = Post.types[:whisper]
|
||||
end
|
||||
|
||||
PostRevisor.tracked_topic_fields.each_key do |f|
|
||||
params.permit(f => [])
|
||||
result[f] = params[f] if params.has_key?(f)
|
||||
|
|
|
@ -13,7 +13,10 @@ class UserAvatarsController < ApplicationController
|
|||
user.create_user_avatar(user_id: user.id) unless user.user_avatar
|
||||
user.user_avatar.update_gravatar!
|
||||
|
||||
render json: { upload_id: user.user_avatar.gravatar_upload_id }
|
||||
render json: {
|
||||
gravatar_upload_id: user.user_avatar.gravatar_upload_id,
|
||||
gravatar_avatar_template: User.avatar_template(user.username, user.user_avatar.gravatar_upload_id)
|
||||
}
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
|
|
|
@ -515,13 +515,13 @@ class UsersController < ApplicationController
|
|||
|
||||
results = UserSearch.new(term, topic_id: topic_id, topic_allowed_users: topic_allowed_users, searching_user: current_user).search
|
||||
|
||||
user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id]
|
||||
user_fields = [:username, :upload_avatar_template]
|
||||
user_fields << :name if SiteSetting.enable_names?
|
||||
|
||||
to_render = { users: results.as_json(only: user_fields, methods: :avatar_template) }
|
||||
to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) }
|
||||
|
||||
if params[:include_groups] == "true"
|
||||
to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} }
|
||||
to_render[:groups] = Group.search_group(term, current_user).map { |m| { name: m.name, usernames: m.usernames.split(",") } }
|
||||
end
|
||||
|
||||
render json: to_render
|
||||
|
@ -531,13 +531,15 @@ class UsersController < ApplicationController
|
|||
user = fetch_user_from_params
|
||||
guardian.ensure_can_edit!(user)
|
||||
|
||||
type = params[:type]
|
||||
upload_id = params[:upload_id]
|
||||
|
||||
user.uploaded_avatar_id = upload_id
|
||||
|
||||
# ensure we associate the custom avatar properly
|
||||
if upload_id && user.user_avatar.custom_upload_id != upload_id
|
||||
if type == "uploaded" || type == "custom"
|
||||
user.user_avatar.custom_upload_id = upload_id
|
||||
elsif type == "gravatar"
|
||||
user.user_avatar.gravatar_upload_id = upload_id
|
||||
end
|
||||
|
||||
user.save!
|
||||
|
|
|
@ -74,7 +74,7 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.types
|
||||
@types ||= Enum.new(:regular, :moderator_action, :small_action)
|
||||
@types ||= Enum.new(:regular, :moderator_action, :small_action, :whisper)
|
||||
end
|
||||
|
||||
def self.cook_methods
|
||||
|
@ -96,15 +96,24 @@ class Post < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def publish_change_to_clients!(type)
|
||||
# special failsafe for posts missing topics
|
||||
# consistency checks should fix, but message
|
||||
|
||||
channel = "/topic/#{topic_id}"
|
||||
msg = { id: id,
|
||||
post_number: post_number,
|
||||
updated_at: Time.now,
|
||||
type: type }
|
||||
|
||||
# special failsafe for posts missing topics consistency checks should fix, but message
|
||||
# is safe to skip
|
||||
MessageBus.publish("/topic/#{topic_id}", {
|
||||
id: id,
|
||||
post_number: post_number,
|
||||
updated_at: Time.now,
|
||||
type: type
|
||||
}, group_ids: topic.secure_group_ids) if topic
|
||||
return unless topic
|
||||
|
||||
# Whispers should not be published to everyone
|
||||
if post_type == Post.types[:whisper]
|
||||
user_ids = User.where('admin or moderator or id = ?', user_id).pluck(:id)
|
||||
MessageBus.publish(channel, msg, user_ids: user_ids)
|
||||
else
|
||||
MessageBus.publish(channel, msg, group_ids: topic.secure_group_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def trash!(trashed_by=nil)
|
||||
|
|
|
@ -218,6 +218,13 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def visible_post_types(viewed_by=nil)
|
||||
types = Post.types
|
||||
result = [types[:regular], types[:moderator_action], types[:small_action]]
|
||||
result << types[:whisper] if viewed_by.try(:staff?)
|
||||
result
|
||||
end
|
||||
|
||||
def self.top_viewed(max = 10)
|
||||
Topic.listable_topics.visible.secured.order('views desc').limit(max)
|
||||
end
|
||||
|
|
|
@ -50,7 +50,7 @@ class Upload < ActiveRecord::Base
|
|||
end
|
||||
|
||||
# list of image types that will be cropped
|
||||
CROPPED_IMAGE_TYPES ||= ["avatar", "profile_background", "card_background"]
|
||||
CROPPED_IMAGE_TYPES ||= %w{avatar profile_background card_background}
|
||||
|
||||
# options
|
||||
# - content_type
|
||||
|
|
|
@ -454,26 +454,41 @@ class User < ActiveRecord::Base
|
|||
[((result << 5) - result) + char.ord].pack('L').unpack('l').first
|
||||
end
|
||||
|
||||
avatar_template = split_avatars[hash.abs % split_avatars.size]
|
||||
split_avatars[hash.abs % split_avatars.size]
|
||||
end
|
||||
else
|
||||
system_avatar_template(username)
|
||||
end
|
||||
end
|
||||
|
||||
def self.avatar_template(username, uploaded_avatar_id)
|
||||
username ||= ""
|
||||
return default_template(username) if !uploaded_avatar_id
|
||||
hostname = RailsMultisite::ConnectionManagement.current_hostname
|
||||
UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def self.system_avatar_template(username)
|
||||
# TODO it may be worth caching this in a distributed cache, should be benched
|
||||
if SiteSetting.external_system_avatars_enabled
|
||||
url = SiteSetting.external_system_avatars_url.dup
|
||||
url.gsub! "{color}", letter_avatar_color(username.downcase)
|
||||
url.gsub! "{username}", username
|
||||
url.gsub! "{first_letter}", username[0].downcase
|
||||
url
|
||||
else
|
||||
"#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png"
|
||||
end
|
||||
end
|
||||
|
||||
def self.avatar_template(username,uploaded_avatar_id)
|
||||
return default_template(username) if !uploaded_avatar_id
|
||||
def self.letter_avatar_color(username)
|
||||
username ||= ""
|
||||
hostname = RailsMultisite::ConnectionManagement.current_hostname
|
||||
UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def self.letter_avatar_template(username)
|
||||
"#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png"
|
||||
color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length]
|
||||
color.map { |c| c.to_s(16).rjust(2, '0') }.join
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
self.class.avatar_template(username,uploaded_avatar_id)
|
||||
self.class.avatar_template(username, uploaded_avatar_id)
|
||||
end
|
||||
|
||||
# The following count methods are somewhat slow - definitely don't use them in a loop.
|
||||
|
|
|
@ -39,8 +39,7 @@ class UserAvatar < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.local_avatar_url(hostname, username, upload_id, size)
|
||||
version = self.version(upload_id)
|
||||
"#{Discourse.base_uri}/user_avatar/#{hostname}/#{username}/#{size}/#{version}.png"
|
||||
self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s)
|
||||
end
|
||||
|
||||
def self.local_avatar_template(hostname, username, upload_id)
|
||||
|
@ -49,8 +48,7 @@ class UserAvatar < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.external_avatar_url(user_id, upload_id, size)
|
||||
version = self.version(upload_id)
|
||||
"#{Discourse.store.absolute_base_url}/avatars/#{user_id}/#{size}/#{version}.png"
|
||||
self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s)
|
||||
end
|
||||
|
||||
def self.external_avatar_template(user_id, upload_id)
|
||||
|
|
|
@ -3,7 +3,7 @@ class AdminPostSerializer < ApplicationSerializer
|
|||
attributes :id,
|
||||
:created_at,
|
||||
:post_number,
|
||||
:name, :username, :avatar_template, :uploaded_avatar_id,
|
||||
:name, :username, :avatar_template,
|
||||
:topic_id, :topic_slug, :topic_title,
|
||||
:category_id,
|
||||
:excerpt,
|
||||
|
@ -29,10 +29,6 @@ class AdminPostSerializer < ApplicationSerializer
|
|||
object.user.avatar_template
|
||||
end
|
||||
|
||||
def uploaded_avatar_id
|
||||
object.user.uploaded_avatar_id
|
||||
end
|
||||
|
||||
def topic_slug
|
||||
topic.slug
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@ class BasicPostSerializer < ApplicationSerializer
|
|||
:name,
|
||||
:username,
|
||||
:avatar_template,
|
||||
:uploaded_avatar_id,
|
||||
:created_at,
|
||||
:cooked,
|
||||
:cooked_hidden
|
||||
|
@ -21,13 +20,10 @@ class BasicPostSerializer < ApplicationSerializer
|
|||
object.user.try(:avatar_template)
|
||||
end
|
||||
|
||||
def uploaded_avatar_id
|
||||
object.user.try(:uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def cooked_hidden
|
||||
object.hidden && !scope.is_staff?
|
||||
end
|
||||
|
||||
def include_cooked_hidden?
|
||||
cooked_hidden
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class BasicUserSerializer < ApplicationSerializer
|
||||
attributes :id, :username, :uploaded_avatar_id, :avatar_template
|
||||
attributes :id, :username, :avatar_template
|
||||
|
||||
def include_name?
|
||||
SiteSetting.enable_names?
|
||||
|
@ -9,7 +9,7 @@ class BasicUserSerializer < ApplicationSerializer
|
|||
if Hash === object
|
||||
User.avatar_template(user[:username], user[:uploaded_avatar_id])
|
||||
else
|
||||
object.avatar_template
|
||||
user.try(:avatar_template)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ class CategorySerializer < BasicCategorySerializer
|
|||
:suppress_from_homepage,
|
||||
:can_delete,
|
||||
:cannot_delete_reason,
|
||||
:is_special,
|
||||
:allow_badges,
|
||||
:custom_fields
|
||||
|
||||
|
@ -37,6 +38,15 @@ class CategorySerializer < BasicCategorySerializer
|
|||
true
|
||||
end
|
||||
|
||||
def include_is_special?
|
||||
[SiteSetting.lounge_category_id, SiteSetting.meta_category_id, SiteSetting.staff_category_id, SiteSetting.uncategorized_category_id]
|
||||
.include? object.id
|
||||
end
|
||||
|
||||
def is_special
|
||||
true
|
||||
end
|
||||
|
||||
def include_can_delete?
|
||||
scope && scope.can_delete?(object)
|
||||
end
|
||||
|
|
|
@ -9,10 +9,6 @@ class PostActionUserSerializer < BasicUserSerializer
|
|||
object.user.username
|
||||
end
|
||||
|
||||
def uploaded_avatar_id
|
||||
object.user.uploaded_avatar_id
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object.user.avatar_template
|
||||
end
|
||||
|
|
|
@ -177,8 +177,7 @@ class PostSerializer < BasicPostSerializer
|
|||
def reply_to_user
|
||||
{
|
||||
username: object.reply_to_user.username,
|
||||
avatar_template: object.reply_to_user.avatar_template,
|
||||
uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id
|
||||
avatar_template: object.reply_to_user.avatar_template
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -14,8 +14,4 @@ class TopicPostCountSerializer < BasicUserSerializer
|
|||
object[:post_count]
|
||||
end
|
||||
|
||||
def uploaded_avatar_id
|
||||
object[:user].uploaded_avatar_id
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -26,10 +26,8 @@ class UserActionSerializer < ApplicationSerializer
|
|||
:action_code,
|
||||
:edit_reason,
|
||||
:category_id,
|
||||
:uploaded_avatar_id,
|
||||
:closed,
|
||||
:archived,
|
||||
:acting_uploaded_avatar_id
|
||||
:archived
|
||||
|
||||
def excerpt
|
||||
cooked = object.cooked || PrettyText.cook(object.raw)
|
||||
|
|
|
@ -1,20 +1,3 @@
|
|||
class UserNameSerializer < ApplicationSerializer
|
||||
attributes :id, :username, :name, :title, :uploaded_avatar_id, :avatar_template
|
||||
|
||||
def include_name?
|
||||
SiteSetting.enable_names?
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
if Hash === object
|
||||
User.avatar_template(user[:username], user[:uploaded_avatar_id])
|
||||
else
|
||||
object.avatar_template
|
||||
end
|
||||
end
|
||||
|
||||
def user
|
||||
object[:user] || object
|
||||
end
|
||||
|
||||
class UserNameSerializer < BasicUserSerializer
|
||||
attributes :name, :title
|
||||
end
|
||||
|
|
|
@ -93,8 +93,12 @@ class UserSerializer < BasicUserSerializer
|
|||
:watched_category_ids,
|
||||
:private_messages_stats,
|
||||
:disable_jump_reply,
|
||||
:system_avatar_upload_id,
|
||||
:system_avatar_template,
|
||||
:gravatar_avatar_upload_id,
|
||||
:gravatar_avatar_template,
|
||||
:custom_avatar_upload_id,
|
||||
:custom_avatar_template,
|
||||
:has_title_badges,
|
||||
:card_image_badge,
|
||||
:card_image_badge_id,
|
||||
|
@ -278,14 +282,32 @@ class UserSerializer < BasicUserSerializer
|
|||
UserAction.private_messages_stats(object.id, scope)
|
||||
end
|
||||
|
||||
def system_avatar_upload_id
|
||||
# should be left blank
|
||||
end
|
||||
|
||||
def system_avatar_template
|
||||
User.system_avatar_template(object.username)
|
||||
end
|
||||
|
||||
def gravatar_avatar_upload_id
|
||||
object.user_avatar.try(:gravatar_upload_id)
|
||||
end
|
||||
|
||||
def gravatar_avatar_template
|
||||
return unless gravatar_upload_id = object.user_avatar.try(:gravatar_upload_id)
|
||||
User.avatar_template(object.username, gravatar_upload_id)
|
||||
end
|
||||
|
||||
def custom_avatar_upload_id
|
||||
object.user_avatar.try(:custom_upload_id)
|
||||
end
|
||||
|
||||
def custom_avatar_template
|
||||
return unless custom_upload_id = object.user_avatar.try(:custom_upload_id)
|
||||
User.avatar_template(object.username, custom_upload_id)
|
||||
end
|
||||
|
||||
def has_title_badges
|
||||
object.badges.where(allow_title: true).count > 0
|
||||
end
|
||||
|
@ -323,4 +345,5 @@ class UserSerializer < BasicUserSerializer
|
|||
def pending_count
|
||||
0
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span itemprop='name'><%= t.title %></span>
|
||||
</a>
|
||||
<%= page_links(t) %>
|
||||
<% if !@category && t.category %>
|
||||
<% if (!@category || @category.has_children?) && t.category %>
|
||||
<span>[<a href='<%= t.category.url %>'><%= t.category.name %></a>]</span>
|
||||
<% end %>
|
||||
<span title='<%= t 'posts' %>'>(<a href="<%=t.last_post_url%>"><%= t.posts_count %></a>)</span>
|
||||
|
|
|
@ -28,8 +28,10 @@ class NoFallbackLocaleList < FallbackLocaleList
|
|||
end
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
I18n.fallbacks = FallbackLocaleList.new
|
||||
else
|
||||
|
||||
if Rails.env.development?
|
||||
I18n.fallbacks = NoFallbackLocaleList.new
|
||||
else
|
||||
I18n.fallbacks = FallbackLocaleList.new
|
||||
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
|
||||
end
|
||||
|
|
|
@ -813,6 +813,7 @@ en:
|
|||
emoji: "Emoji :smile:"
|
||||
|
||||
add_warning: "This is an official warning."
|
||||
add_whisper: "This is a whisper only visible to moderators"
|
||||
posting_not_on_topic: "Which topic do you want to reply to?"
|
||||
saving_draft_tip: "saving..."
|
||||
saved_draft_tip: "saved"
|
||||
|
@ -1353,6 +1354,7 @@ en:
|
|||
yes_value: "Yes, abandon"
|
||||
|
||||
via_email: "this post arrived via email"
|
||||
whisper: "this post is a private whisper for moderators"
|
||||
|
||||
wiki:
|
||||
about: "this post is a wiki; basic users can edit it"
|
||||
|
@ -1544,6 +1546,7 @@ en:
|
|||
change_in_category_topic: "Edit Description"
|
||||
already_used: 'This color has been used by another category'
|
||||
security: "Security"
|
||||
special_warning: "Warning: This category is a pre-seeded category and the security settings cannot be edited. If you do not wish to use this category, delete it instead of repurposing it."
|
||||
images: "Images"
|
||||
auto_close_label: "Auto-close topics after:"
|
||||
auto_close_units: "hours"
|
||||
|
|
|
@ -94,7 +94,7 @@ en:
|
|||
min_username_length_exists: "You cannot set the minimum username length above the shortest username."
|
||||
min_username_length_range: "You cannot set the minimum above the maximum."
|
||||
max_username_length_exists: "You cannot set the maximum username length below the longest username."
|
||||
max_username_length_range: "You cannot set the maximum above the minimum."
|
||||
max_username_length_range: "You cannot set the maximum below the minimum."
|
||||
default_categories_already_selected: "You cannot select a category used in another list."
|
||||
|
||||
activemodel:
|
||||
|
@ -880,6 +880,7 @@ en:
|
|||
email_token_grace_period_hours: "Forgot password / activate account tokens are still valid for a grace period of (n) hours after being redeemed."
|
||||
|
||||
enable_badges: "Enable the badge system"
|
||||
enable_whispers: "Allow users to whisper to moderators"
|
||||
|
||||
allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines."
|
||||
email_domains_blacklist: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net"
|
||||
|
@ -979,6 +980,9 @@ en:
|
|||
|
||||
avatar_sizes: "List of automatically generated avatar sizes."
|
||||
|
||||
external_system_avatars_enabled: "Use external system avatars service."
|
||||
external_system_avatars_url: "URL of the external system avatars service. Allowed substitions are {username} {first_letter} {color} {size}"
|
||||
|
||||
enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks."
|
||||
|
||||
default_invitee_trust_level: "Default trust level (0-4) for invited users."
|
||||
|
@ -1199,8 +1203,8 @@ en:
|
|||
default_email_mailing_list_mode: "Send an email for every new post by default."
|
||||
default_email_always: "Send an email notification even when the user is active by default."
|
||||
|
||||
default_other_new_topic_duration_minutes: "Global default number of minutes a topic is considered new."
|
||||
default_other_auto_track_topics_after_msecs: "Global default milliseconds before a topic is automatically tracked."
|
||||
default_other_new_topic_duration_minutes: "Global default condition for which a topic is considered new."
|
||||
default_other_auto_track_topics_after_msecs: "Global default time before a topic is automatically tracked."
|
||||
default_other_external_links_in_new_tab: "Open external links in a new tab by default."
|
||||
default_other_enable_quoting: "Enable quote reply for highlighted text by default."
|
||||
default_other_dynamic_favicon: "Show new/updated topic count on browser icon by default."
|
||||
|
@ -1344,7 +1348,7 @@ en:
|
|||
unique: "must be unique"
|
||||
blank: "must be present"
|
||||
must_begin_with_alphanumeric: "must begin with a letter or number or an underscore"
|
||||
must_end_with_alphanumeric: "must end with a letter or number"
|
||||
must_end_with_alphanumeric: "must end with a letter or number or an underscore"
|
||||
must_not_contain_two_special_chars_in_seq: "must not contain a sequence of 2 or more special chars (.-_)"
|
||||
must_not_contain_confusing_suffix: "must not contain a confusing suffix like .json or .png etc."
|
||||
email:
|
||||
|
|
|
@ -182,6 +182,9 @@ basic:
|
|||
enable_badges:
|
||||
client: true
|
||||
default: true
|
||||
enable_whispers:
|
||||
client: true
|
||||
default: false
|
||||
|
||||
login:
|
||||
invite_only:
|
||||
|
@ -418,9 +421,6 @@ posting:
|
|||
newuser_max_attachments:
|
||||
client: true
|
||||
default: 0
|
||||
uncategorized_category_id:
|
||||
default: -1
|
||||
hidden: true
|
||||
post_excerpt_maxlength: 300
|
||||
display_name_on_posts:
|
||||
client: true
|
||||
|
@ -572,6 +572,14 @@ files:
|
|||
avatar_sizes:
|
||||
default: '20|25|32|45|60|120'
|
||||
type: list
|
||||
external_system_avatars_enabled:
|
||||
default: false
|
||||
client: true
|
||||
shadowed_by_global: true
|
||||
external_system_avatars_url:
|
||||
default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png"
|
||||
client: true
|
||||
regex: '^https?:\/\/.+[^\/]'
|
||||
|
||||
trust:
|
||||
default_trust_level:
|
||||
|
@ -922,14 +930,15 @@ uncategorized:
|
|||
lounge_category_id:
|
||||
default: -1
|
||||
hidden: true
|
||||
|
||||
meta_category_id:
|
||||
default: -1
|
||||
hidden: true
|
||||
|
||||
staff_category_id:
|
||||
default: -1
|
||||
hidden: true
|
||||
uncategorized_category_id:
|
||||
default: -1
|
||||
hidden: true
|
||||
|
||||
performance_report_topic_id:
|
||||
default: -1
|
||||
|
|
|
@ -12,10 +12,7 @@ class AvatarLookup
|
|||
private
|
||||
|
||||
def self.lookup_columns
|
||||
@lookup_columns ||= [:id,
|
||||
:email,
|
||||
:username,
|
||||
:uploaded_avatar_id]
|
||||
@lookup_columns ||= %i{id email username uploaded_avatar_id}
|
||||
end
|
||||
|
||||
def users
|
||||
|
|
|
@ -144,10 +144,13 @@ module PostGuardian
|
|||
end
|
||||
|
||||
def can_see_post?(post)
|
||||
post.present? &&
|
||||
(is_admin? ||
|
||||
((is_moderator? || !post.deleted_at.present?) &&
|
||||
can_see_topic?(post.topic)))
|
||||
return false if post.blank?
|
||||
return true if is_admin?
|
||||
return false unless can_see_topic?(post.topic)
|
||||
return false unless post.user == @user || post.topic.visible_post_types(@user).include?(post.post_type)
|
||||
return false if !is_moderator? && post.deleted_at.present?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def can_view_edit_history?(post)
|
||||
|
|
|
@ -191,11 +191,9 @@ class TopicView
|
|||
|
||||
# Find the sort order for a post in the topic
|
||||
def sort_order_for_post_number(post_number)
|
||||
Post.where(topic_id: @topic.id, post_number: post_number)
|
||||
.with_deleted
|
||||
.select(:sort_order)
|
||||
.first
|
||||
.try(:sort_order)
|
||||
posts = Post.where(topic_id: @topic.id, post_number: post_number).with_deleted
|
||||
posts = filter_post_types(posts)
|
||||
posts.select(:sort_order).first.try(:sort_order)
|
||||
end
|
||||
|
||||
# Filter to all posts near a particular post number
|
||||
|
@ -332,11 +330,22 @@ class TopicView
|
|||
|
||||
private
|
||||
|
||||
def filter_post_types(posts)
|
||||
visible_types = @topic.visible_post_types(@user)
|
||||
|
||||
if @user.present?
|
||||
posts.where("user_id = ? OR post_type IN (?)", @user.id, visible_types)
|
||||
else
|
||||
posts.where(post_type: visible_types)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_posts_by_ids(post_ids)
|
||||
# TODO: Sort might be off
|
||||
@posts = Post.where(id: post_ids, topic_id: @topic.id)
|
||||
.includes(:user, :reply_to_user)
|
||||
.order('sort_order')
|
||||
@posts = filter_post_types(@posts)
|
||||
@posts = @posts.with_deleted if @guardian.can_see_deleted_posts?
|
||||
@posts
|
||||
end
|
||||
|
@ -355,13 +364,13 @@ class TopicView
|
|||
end
|
||||
|
||||
def find_topic(topic_id)
|
||||
finder = Topic.where(id: topic_id).includes(:category)
|
||||
finder = finder.with_deleted if @guardian.can_see_deleted_topics?
|
||||
# with_deleted covered in #check_and_raise_exceptions
|
||||
finder = Topic.with_deleted.where(id: topic_id).includes(:category)
|
||||
finder.first
|
||||
end
|
||||
|
||||
def unfiltered_posts
|
||||
result = @topic.posts
|
||||
result = filter_post_types(@topic.posts)
|
||||
result = result.with_deleted if @guardian.can_see_deleted_posts?
|
||||
result = @topic.posts.where("user_id IS NOT NULL") if @exclude_deleted_users
|
||||
result
|
||||
|
|
|
@ -38,6 +38,7 @@ module UserNameSuggester
|
|||
name = name.gsub(/^[^[:alnum:]]+|\W+$/, "")
|
||||
.gsub(/\W+/, "_")
|
||||
.gsub(/^\_+/, '')
|
||||
.gsub(/[\-_\.]{2,}/, "_")
|
||||
name
|
||||
end
|
||||
|
||||
|
|
|
@ -437,6 +437,32 @@ describe Guardian do
|
|||
expect(Guardian.new(user).can_see?(post)).to be_falsey
|
||||
expect(Guardian.new(admin).can_see?(post)).to be_truthy
|
||||
end
|
||||
|
||||
it 'respects whispers' do
|
||||
regular_post = Fabricate.build(:post)
|
||||
whisper_post = Fabricate.build(:post, post_type: Post.types[:whisper])
|
||||
|
||||
anon_guardian = Guardian.new
|
||||
expect(anon_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(anon_guardian.can_see?(whisper_post)).to eq(false)
|
||||
|
||||
regular_user = Fabricate.build(:user)
|
||||
regular_guardian = Guardian.new(regular_user)
|
||||
expect(regular_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(regular_guardian.can_see?(whisper_post)).to eq(false)
|
||||
|
||||
# can see your own whispers
|
||||
regular_whisper = Fabricate.build(:post, post_type: Post.types[:whisper], user: regular_user)
|
||||
expect(regular_guardian.can_see?(regular_whisper)).to eq(true)
|
||||
|
||||
mod_guardian = Guardian.new(Fabricate.build(:moderator))
|
||||
expect(mod_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(mod_guardian.can_see?(whisper_post)).to eq(true)
|
||||
|
||||
admin_guardian = Guardian.new(Fabricate.build(:admin))
|
||||
expect(admin_guardian.can_see?(regular_post)).to eq(true)
|
||||
expect(admin_guardian.can_see?(whisper_post)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a PostRevision' do
|
||||
|
|
|
@ -13,6 +13,7 @@ describe TopicView do
|
|||
expect { TopicView.new(1231232, coding_horror) }.to raise_error(Discourse::NotFound)
|
||||
end
|
||||
|
||||
# see also spec/controllers/topics_controller_spec.rb TopicsController::show::permission errors
|
||||
it "raises an error if the user can't see the topic" do
|
||||
Guardian.any_instance.expects(:can_see?).with(topic).returns(false)
|
||||
expect { topic_view }.to raise_error(Discourse::InvalidAccess)
|
||||
|
@ -21,7 +22,7 @@ describe TopicView do
|
|||
it "handles deleted topics" do
|
||||
admin = Fabricate(:admin)
|
||||
topic.trash!(admin)
|
||||
expect { TopicView.new(topic.id, Fabricate(:user)) }.to raise_error(Discourse::NotFound)
|
||||
expect { TopicView.new(topic.id, Fabricate(:user)) }.to raise_error(Discourse::InvalidAccess)
|
||||
expect { TopicView.new(topic.id, admin) }.not_to raise_error
|
||||
end
|
||||
|
||||
|
@ -251,6 +252,23 @@ describe TopicView do
|
|||
|
||||
end
|
||||
|
||||
context 'whispers' do
|
||||
it "handles their visibility properly" do
|
||||
p1 = Fabricate(:post, topic: topic, user: coding_horror)
|
||||
p2 = Fabricate(:post, topic: topic, user: coding_horror, post_type: Post.types[:whisper])
|
||||
p3 = Fabricate(:post, topic: topic, user: coding_horror)
|
||||
|
||||
ch_posts = TopicView.new(topic.id, coding_horror).posts
|
||||
expect(ch_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
|
||||
|
||||
anon_posts = TopicView.new(topic.id).posts
|
||||
expect(anon_posts.map(&:id)).to eq([p1.id, p3.id])
|
||||
|
||||
admin_posts = TopicView.new(topic.id, Fabricate(:moderator)).posts
|
||||
expect(admin_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
|
||||
end
|
||||
end
|
||||
|
||||
context '.posts' do
|
||||
|
||||
# Create the posts in a different order than the sort_order
|
||||
|
|
|
@ -75,6 +75,11 @@ describe UserNameSuggester do
|
|||
expect(UserNameSuggester.suggest("myname.")).to eq('myname')
|
||||
end
|
||||
|
||||
it 'handles usernames with a sequence of 2 or more special chars' do
|
||||
expect(UserNameSuggester.suggest('Darth__Vader')).to eq('Darth_Vader')
|
||||
expect(UserNameSuggester.suggest('Darth_-_Vader')).to eq('Darth_Vader')
|
||||
end
|
||||
|
||||
it 'should handle typical facebook usernames' do
|
||||
expect(UserNameSuggester.suggest('roger.nelson.3344913')).to eq('roger_nelson_33')
|
||||
end
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
require 'spec_helper'
|
||||
|
||||
def topics_controller_show_gen_perm_tests(expected, ctx)
|
||||
expected.each do |sym, status|
|
||||
params = "topic_id: #{sym}.id, slug: #{sym}.slug"
|
||||
if sym == :nonexist
|
||||
params = "topic_id: nonexist_topic_id"
|
||||
end
|
||||
ctx.instance_eval("
|
||||
it 'returns #{status} for #{sym}' do
|
||||
begin
|
||||
xhr :get, :show, #{params}
|
||||
expect(response.status).to eq(#{status})
|
||||
rescue Discourse::NotLoggedIn
|
||||
expect(302).to eq(#{status})
|
||||
end
|
||||
end")
|
||||
end
|
||||
end
|
||||
|
||||
describe TopicsController do
|
||||
|
||||
context 'wordpress' do
|
||||
|
@ -554,6 +572,108 @@ describe TopicsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'permission errors' do
|
||||
let(:allowed_user) { Fabricate(:user) }
|
||||
let(:allowed_group) { Fabricate(:group) }
|
||||
let(:secure_category) {
|
||||
c = Fabricate(:category)
|
||||
c.permissions = [[allowed_group, :full]]
|
||||
c.save
|
||||
allowed_user.groups = [allowed_group]
|
||||
allowed_user.save
|
||||
c }
|
||||
let(:normal_topic) { Fabricate(:topic) }
|
||||
let(:secure_topic) { Fabricate(:topic, category: secure_category) }
|
||||
let(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) }
|
||||
let(:deleted_topic) { Fabricate(:deleted_topic) }
|
||||
let(:nonexist_topic_id) { Topic.last.id + 10000 }
|
||||
|
||||
context 'anonymous' do
|
||||
expected = {
|
||||
:normal_topic => 200,
|
||||
:secure_topic => 403,
|
||||
:private_topic => 302,
|
||||
:deleted_topic => 403,
|
||||
:nonexist => 404
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
|
||||
context 'anonymous with login required' do
|
||||
before do
|
||||
SiteSetting.login_required = true
|
||||
end
|
||||
expected = {
|
||||
:normal_topic => 302,
|
||||
:secure_topic => 302,
|
||||
:private_topic => 302,
|
||||
:deleted_topic => 302,
|
||||
:nonexist => 302
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
|
||||
context 'normal user' do
|
||||
before do
|
||||
log_in(:user)
|
||||
end
|
||||
|
||||
expected = {
|
||||
:normal_topic => 200,
|
||||
:secure_topic => 403,
|
||||
:private_topic => 403,
|
||||
:deleted_topic => 403,
|
||||
:nonexist => 404
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
|
||||
context 'allowed user' do
|
||||
before do
|
||||
log_in_user(allowed_user)
|
||||
end
|
||||
|
||||
expected = {
|
||||
:normal_topic => 200,
|
||||
:secure_topic => 200,
|
||||
:private_topic => 200,
|
||||
:deleted_topic => 403,
|
||||
:nonexist => 404
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
|
||||
context 'moderator' do
|
||||
before do
|
||||
log_in(:moderator)
|
||||
end
|
||||
|
||||
expected = {
|
||||
:normal_topic => 200,
|
||||
:secure_topic => 403,
|
||||
:private_topic => 403,
|
||||
:deleted_topic => 200,
|
||||
:nonexist => 404
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
|
||||
context 'admin' do
|
||||
before do
|
||||
log_in(:admin)
|
||||
end
|
||||
|
||||
expected = {
|
||||
:normal_topic => 200,
|
||||
:secure_topic => 200,
|
||||
:private_topic => 200,
|
||||
:deleted_topic => 200,
|
||||
:nonexist => 404
|
||||
}
|
||||
topics_controller_show_gen_perm_tests(expected, self)
|
||||
end
|
||||
end
|
||||
|
||||
it 'records a view' do
|
||||
expect { xhr :get, :show, topic_id: topic.id, slug: topic.slug }.to change(TopicViewItem, :count).by(1)
|
||||
end
|
||||
|
|
|
@ -1301,7 +1301,7 @@ describe UsersController do
|
|||
describe '.pick_avatar' do
|
||||
|
||||
it 'raises an error when not logged in' do
|
||||
expect { xhr :put, :pick_avatar, username: 'asdf', avatar_id: 1}.to raise_error(Discourse::NotLoggedIn)
|
||||
expect { xhr :put, :pick_avatar, username: 'asdf', avatar_id: 1, type: "custom"}.to raise_error(Discourse::NotLoggedIn)
|
||||
end
|
||||
|
||||
context 'while logged in' do
|
||||
|
@ -1310,12 +1310,12 @@ describe UsersController do
|
|||
|
||||
it 'raises an error when you don\'t have permission to toggle the avatar' do
|
||||
another_user = Fabricate(:user)
|
||||
xhr :put, :pick_avatar, username: another_user.username, upload_id: 1
|
||||
xhr :put, :pick_avatar, username: another_user.username, upload_id: 1, type: "custom"
|
||||
expect(response).to be_forbidden
|
||||
end
|
||||
|
||||
it 'it successful' do
|
||||
xhr :put, :pick_avatar, username: user.username, upload_id: 111
|
||||
xhr :put, :pick_avatar, username: user.username, upload_id: 111, type: "custom"
|
||||
expect(user.reload.uploaded_avatar_id).to eq(111)
|
||||
expect(user.user_avatar.reload.custom_upload_id).to eq(111)
|
||||
expect(response).to be_success
|
||||
|
@ -1326,13 +1326,6 @@ describe UsersController do
|
|||
expect(response).to be_success
|
||||
end
|
||||
|
||||
it 'returns success' do
|
||||
xhr :put, :pick_avatar, username: user.username, upload_id: 111
|
||||
expect(user.reload.uploaded_avatar_id).to eq(111)
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json['success']).to eq("OK")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -11,6 +11,40 @@ describe Topic do
|
|||
|
||||
it { is_expected.to rate_limit }
|
||||
|
||||
context '#visible_post_types' do
|
||||
let(:types) { Post.types }
|
||||
|
||||
it "returns the appropriate types for anonymous users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to_not include(types[:whisper])
|
||||
end
|
||||
|
||||
it "returns the appropriate types for regular users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types(Fabricate.build(:user))
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to_not include(types[:whisper])
|
||||
end
|
||||
|
||||
it "returns the appropriate types for staff users" do
|
||||
topic = Fabricate.build(:topic)
|
||||
post_types = topic.visible_post_types(Fabricate.build(:moderator))
|
||||
|
||||
expect(post_types).to include(types[:regular])
|
||||
expect(post_types).to include(types[:moderator_action])
|
||||
expect(post_types).to include(types[:small_action])
|
||||
expect(post_types).to include(types[:whisper])
|
||||
end
|
||||
end
|
||||
|
||||
context 'slug' do
|
||||
let(:title) { "hello world topic" }
|
||||
let(:slug) { "hello-world-topic" }
|
||||
|
|
|
@ -144,7 +144,6 @@ Spork.prefork do
|
|||
FileUtils.cp("#{Rails.root}/spec/fixtures/images/#{filename}", "#{Rails.root}/tmp/spec/#{filename}")
|
||||
File.new("#{Rails.root}/tmp/spec/#{filename}")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Spork.each_run do
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
|
||||
module('lib:avatar-template');
|
||||
|
||||
test("avatarTemplate", function(){
|
||||
var oldCDN = Discourse.CDN;
|
||||
var oldBase = Discourse.BaseUrl;
|
||||
Discourse.BaseUrl = "frogs.com";
|
||||
|
||||
equal(avatarTemplate("sam", 1), "/user_avatar/frogs.com/sam/{size}/1.png");
|
||||
Discourse.CDN = "http://awesome.cdn.com";
|
||||
equal(avatarTemplate("sam", 1), "http://awesome.cdn.com/user_avatar/frogs.com/sam/{size}/1.png");
|
||||
Discourse.CDN = oldCDN;
|
||||
Discourse.BaseUrl = oldBase;
|
||||
});
|
||||
|
Loading…
Add table
Reference in a new issue