Merge commit '507517fce5248ad37f6a239fce16ae92d0e0f5b0' into v1.18.x

v1.18.x
Misty (Bot) 4 years ago
commit 36653525bd

@ -1,3 +1,47 @@
#### v1.18.1 (2021-09-03)
##### Chores
* found some hooks that don't play well docgen (ae793b4a)
* incrementing version number - v1.18.0 (1e436ae7)
* update changelog for v1.18.0 (2fd9c095)
* **deps:** update dependency mocha to v9.1.1 (64bac178)
##### New Features
* create folders in ACP uploads #9638 (#9750) (3df79683)
* column based view on wide priv. tables (#9699) (61f02f17)
* als (#9749) (e59d3575)
* add quick reply key (e9314842)
* add new lang key for no best posts (6e73d8c9)
##### Bug Fixes
* **deps:**
* update dependency autoprefixer to v10.3.4 (67b932f4)
* update dependency nodebb-theme-persona to v11.2.4 (fe18e100)
* update dependency mongodb to v3.7.0 (31a35d7f)
* update socket.io packages to v4.2.0 (f2028d70)
* update dependency ioredis to v4.27.9 (6052eb16)
* update dependency mongodb to v3.6.12 (#9761) (5fa982c1)
* update dependency nodebb-plugin-composer-default to v7.0.2 (33d51201)
* update dependency nodebb-theme-slick to v1.4.12 (1b416d7e)
* update dependency nodebb-theme-slick to v1.4.11 (65b32fa1)
* update dependency nodebb-theme-persona to v11.2.3 (6ce321e4)
* update dependency autoprefixer to v10.3.3 (91ba7cdf)
* update dependency nodebb-theme-slick to v1.4.9 (d80b378f)
* update dependency jquery-deserialize to v2.0.0 (#9744) (7f9451ce)
* determine indeterminate checkboxes (760ea9df)
* move app.alert back into the conditionals (ca9bae3a)
* only show email confirmation warning toast on pages that it applies (1bd1cc74)
* updated email confirm warning to be more positive (2d1380dd)
* automated tests are a good thing to have (6afeac37)
##### Refactors
* consistent jquery element var naming (fc0e655e)
* var to const (1272da65)
#### v1.18.0 (2021-08-25)
##### Breaking Changes

@ -85,7 +85,7 @@
"@nodebb/bootswatch": "3.4.2",
"nconf": "^0.11.2",
"nodebb-plugin-composer-default": "7.0.2",
"nodebb-plugin-dbsearch": "5.0.2",
"nodebb-plugin-dbsearch": "5.0.3",
"nodebb-plugin-emoji": "^3.5.0",
"nodebb-plugin-emoji-android": "2.0.5",
"nodebb-plugin-markdown": "8.14.3",
@ -93,9 +93,9 @@
"nodebb-plugin-spam-be-gone": "0.7.9",
"nodebb-rewards-essentials": "0.1.5",
"nodebb-theme-lavender": "5.2.1",
"nodebb-theme-persona": "11.2.4",
"nodebb-theme-persona": "11.2.5",
"nodebb-theme-slick": "1.4.12",
"nodebb-theme-vanilla": "12.1.2",
"nodebb-theme-vanilla": "12.1.3",
"nodebb-widget-essentials": "5.0.4",
"nodemailer": "^6.5.0",
"nprogress": "0.2.0",
@ -116,7 +116,7 @@
"sanitize-html": "^2.3.2",
"semver": "^7.3.4",
"serve-favicon": "^2.5.0",
"sharp": "0.29.0",
"sharp": "0.29.1",
"sitemap": "^7.0.0",
"slideout": "1.0.1",
"socket.io": "4.2.0",
@ -142,8 +142,8 @@
},
"devDependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@commitlint/cli": "12.1.4",
"@commitlint/config-angular": "12.1.4",
"@commitlint/cli": "13.1.0",
"@commitlint/config-angular": "13.1.0",
"coveralls": "3.1.1",
"eslint": "7.32.0",
"eslint-config-nodebb": "0.0.2",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Изтриване",
"enable": "Включване",
"disable": "Изключване",
"control-panel": "Управление на наградите",
"new-reward": "Нова награда",
"alert.delete-success": "Наградата е изтрита успешно",
"alert.no-inputs-found": "Неправомерна награда — няма нищо въведено!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Odstranit",
"enable": "Povolit",
"disable": "Zakázat",
"control-panel": "Ovládací panel odměn",
"new-reward": "Nová odměna",
"alert.delete-success": "Odměna byla úspěšně smazána",
"alert.no-inputs-found": "Nepovolená odměna nebyl nalezen žádný záznam.",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Entfernen",
"enable": "Aktivieren",
"disable": "Deaktivieren",
"control-panel": "Belohnungseinstellungen",
"new-reward": "Neue Belohnung",
"alert.delete-success": "Die Belohnung wurde erfolgreich gelöscht",
"alert.no-inputs-found": "Ungültige Belohnung - keine Eingaben gefunden!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Eliminar",
"enable": "Habilitar",
"disable": "Deshabilitar",
"control-panel": "Control de recompensas",
"new-reward": "Nueva Recompensa",
"alert.delete-success": "Recompensa eliminada con éxito",
"alert.no-inputs-found": "¡Recompensa ilegal - no se encontraron inputs!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Supprimer",
"enable": "Activer",
"disable": "Désactiver",
"control-panel": "Contrôle des récompenses",
"new-reward": "Nouvelle récompense",
"alert.delete-success": "Récompense supprimée",
"alert.no-inputs-found": "Récompense invalide - aucune entrée trouvée !",

@ -6,6 +6,6 @@
"size/filecount": "Taille / nombre de fichiers",
"confirm-delete": "Voulez-vous vraiment supprimer ce fichier?",
"filecount": "%1 fichiers",
"new-folder": "New Folder",
"name-new-folder": "Enter a name for new the folder"
"new-folder": "Nouveau Dossier",
"name-new-folder": "Entrez un nom pour le nouveau dossier"
}

@ -25,14 +25,14 @@
"invalid-event": "Événement non valide: %1",
"local-login-disabled": "Le système de connexion local a été désactivé pour les comptes sans privilèges.",
"csrf-invalid": "Nous ne pouvons pas vous connectez, probablement car votre session a expiré. Merci de réessayer.",
"invalid-path": "Invalid path",
"folder-exists": "Folder exists",
"invalid-path": "Chemin invalide",
"folder-exists": "Le dossier existe",
"invalid-pagination-value": "Valeur de pagination invalide. Celle-ci doit être comprise entre %1 et %2.",
"username-taken": "Nom dutilisateur déjà utilisé",
"email-taken": "Email déjà utilisé",
"email-nochange": "Le mail saisi est déjà enregistré.",
"email-invited": "Cet utilisateur a déjà été invité.",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",
"email-not-confirmed": "La publication dans certaines catégories ou sujets sera activée après confirmation de e-mail, veuillez cliquer ici pour envoyer un e-mail de confirmation.",
"email-not-confirmed-chat": "Il ne vous est pas possible d'utiliser le chat tant que votre adresse email n'a pas été vérifiée. Veuillez cliquer ici pour confirmer votre adresse email.",
"email-not-confirmed-email-sent": "Votre email n'a pas encore été confirmé, veuillez vérifier votre boîte mail. Vous ne pourrez pas poster ou discuter avant que votre email ne soit confirmé.",
"no-email-to-confirm": "Votre compte n'a pas d'adresse mail définie. Un mail est nécessaire pour la récupération du compte. Veuillez cliquer ici pour entrer un courriel.",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "מחק",
"enable": "הפעל",
"disable": "השבת",
"control-panel": "בקרת תגמולים",
"new-reward": "תגמול חדש",
"alert.delete-success": "תגמול נמחק בהצלחה",
"alert.no-inputs-found": "תגמול לא חוקי - לא נמצא מידע!",

@ -8,8 +8,6 @@
"delete": "Obriši",
"enable": "Omogući",
"disable": "onemogući",
"control-panel": "Kontrola nagrada",
"new-reward": "Nova nagrada",
"alert.delete-success": "Uspješno obrisana nagrada",
"alert.no-inputs-found": "Ilegalna nagrada - nije pronađen unos!",

@ -8,8 +8,6 @@
"delete": "Törlés",
"enable": "Engedélyezés",
"disable": "Tiltás",
"control-panel": "Jutalom vezérlés",
"new-reward": "Új jutalom",
"alert.delete-success": "Jutalom sikeresen törölve",
"alert.no-inputs-found": "Helytelen jutalom - nem található bevitel!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Elimina",
"enable": "Abilita",
"disable": "Disabilita",
"control-panel": "Controllo dei premi",
"new-reward": "Nuovo premio",
"alert.delete-success": "Premi eliminati con successo",
"alert.no-inputs-found": "Premio illegale - immissioni non trovate!",

@ -51,13 +51,13 @@
"alert.saved": "Modifiche ai privilegi salvate e applicate",
"alert.confirm-discard": "Sei sicuro di voler annullare le modifiche ai privilegi?",
"alert.discarded": "Modifiche ai privilegi ignorate",
"alert.confirm-copyToAll": "Are you sure you wish to apply this set of <strong>%1</strong> to <strong>all categories</strong>?",
"alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of <strong>%1</strong> to <strong>all categories</strong>?",
"alert.confirm-copyToChildren": "Are you sure you wish to apply this set of <strong>%1</strong> to <strong>all descendant (child) categories</strong>?",
"alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of <strong>%1</strong> to <strong>all descendant (child) categories</strong>?",
"alert.confirm-copyToAll": "Sei sicuro di voler applicare questa serie di <strong>%1</strong> a <strong>tutte le categorie</strong>?",
"alert.confirm-copyToAllGroup": "Sei sicuro di voler applicare questa serie di <strong>%1</strong> del gruppo a <strong>tutte le categorie</strong>?",
"alert.confirm-copyToChildren": "Sei sicuro di voler applicare questa serie di <strong>%1</strong> a <strong>tutte le categorie discendenti (figli)</strong>?",
"alert.confirm-copyToChildrenGroup": "Sei sicuro di voler applicare questa serie di <strong>%1</strong> del questo gruppo a <strong>tutte le categorie discendenti (figli)</strong>?",
"alert.no-undo": "<em>Questa azione non può essere annullata.</em>",
"alert.admin-warning": "Gli amministratori ottengono implicitamente tutti i privilegi",
"alert.copyPrivilegesFrom-title": "Select a category to copy from",
"alert.copyPrivilegesFrom-warning": "This will copy <strong>%1</strong> from the selected category.",
"alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of <strong>%1</strong> from the selected category."
"alert.copyPrivilegesFrom-title": "Seleziona una categoria da cui copiare",
"alert.copyPrivilegesFrom-warning": "Questo copierà <strong>1%</strong> dalla categoria selezionata.",
"alert.copyPrivilegesFromGroup-warning": "Questo copierà la serie di <strong>%1</strong> da questo gruppo dalla categoria selezionata."
}

@ -6,6 +6,6 @@
"size/filecount": "Dimensione / Numero file",
"confirm-delete": "Vuoi davvero cancellare questo file?",
"filecount": "%1 file",
"new-folder": "New Folder",
"name-new-folder": "Enter a name for new the folder"
"new-folder": "Nuova cartella",
"name-new-folder": "Inserisci un nome per la nuova cartella"
}

@ -25,14 +25,14 @@
"invalid-event": "Evento non valido: %1",
"local-login-disabled": "Il sistema di accesso locale è stato disabilitato per gli account senza privilegi.",
"csrf-invalid": "Non siamo riusciti a farti accedere, probabilmente perché la sessione è scaduta. Per favore riprova.",
"invalid-path": "Invalid path",
"folder-exists": "Folder exists",
"invalid-path": "Percorso non valido",
"folder-exists": "La cartella esiste",
"invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2",
"username-taken": "Nome utente già esistente",
"email-taken": "Email già esistente",
"email-nochange": "L'email inserita è la stessa dell'email già presente in archivio.",
"email-invited": "L'email è già stata invitata",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",
"email-not-confirmed": "Sarai abilitato a postare in alcune categorie o discussioni una volta che la tua email sarà confermata, per favore clicca qui per inviare una email di conferma.",
"email-not-confirmed-chat": "Non puoi chattare finché non confermi la tua email, per favore clicca qui per confermare la tua email.",
"email-not-confirmed-email-sent": "La tua email non è stata ancora confermata, controlla la tua casella di posta per l'email di conferma. Non potrai pubblicare post o chattare fino a quando la tua email non sarà confermata.",
"no-email-to-confirm": "Il tuo account non ha un'email impostata. Un'email è necessaria per il recupero dell'account. Clicca qui per inserire un'email.",

@ -8,8 +8,6 @@
"delete": "削除",
"enable": "有効",
"disable": "無効",
"control-panel": "報酬コントロール",
"new-reward": "新しい報酬",
"alert.delete-success": "報酬を削除しました",
"alert.no-inputs-found": "違法報酬 - 入力が見つかりません!",

@ -8,8 +8,6 @@
"delete": "삭제",
"enable": "활성화",
"disable": "비활성화",
"control-panel": "보상 제어판",
"new-reward": "새로운 보상",
"alert.delete-success": "성공적으로 보상을 삭제했습니다.",
"alert.no-inputs-found": "잘못된 보상 - 입력값이 없습니다!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Izdzēst",
"enable": "Iespējot",
"disable": "Atspējot",
"control-panel": "Balvu vadības panelis",
"new-reward": "Jauna balva",
"alert.delete-success": "Veiksmīgi izdzēsta balva",
"alert.no-inputs-found": "Nederīga balva - nav ievažu!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Usuń",
"enable": "Włącz",
"disable": "Wyłącz",
"control-panel": "Ustawienia nagród",
"new-reward": "Nowa nagroda",
"alert.delete-success": "Pomyślnie usunięto nagrodę",
"alert.no-inputs-found": "Niepoprawnie dodana nagroda ",

@ -8,8 +8,6 @@
"delete": "Deletar",
"enable": "Ativar",
"disable": "Desativar",
"control-panel": "Controle de Recompensas",
"new-reward": "Nova Recompensa",
"alert.delete-success": "Recompensa excluída com sucesso",
"alert.no-inputs-found": "Recompensa ilegal - nenhuma entrada encontrada!",

@ -8,8 +8,6 @@
"delete": "Apagar",
"enable": "Ativar",
"disable": "Desativar",
"control-panel": "Controlo de Recompensas",
"new-reward": "Nova Recompensa",
"alert.delete-success": "Recompensa apagada com sucesso",
"alert.no-inputs-found": "Recompensa ilegal - não foram encontradas entradas!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Удалить",
"enable": "Включить",
"disable": "Выключить",
"control-panel": "Управление наградами",
"new-reward": "Новая награда",
"alert.delete-success": "Награда успешно удалена",
"alert.no-inputs-found": "Некорректная награда!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Odstrániť",
"enable": "Povoliť",
"disable": "Zakázať",
"control-panel": "Kontrola odmien",
"new-reward": "Nová odmena",
"alert.delete-success": "Odmena bola úspešne vymazaná",
"alert.no-inputs-found": "Nepovolená odmena - nebol nájdený žiadny záznam.",

@ -1,7 +1,7 @@
{
"alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?",
"alert.confirm-restart": "Are you sure you wish to restart NodeBB?",
"alert.confirm-rebuild-and-restart": "Ste prepričani, da želite znova zagnati NodeBB?",
"alert.confirm-restart": "Ste prepričani, da želite znova zagnati NodeBB?",
"acp-title": "%1 | NodeBB Admin Control Panel",
"acp-title": "NodeBB administracijska nadzorna plošča",
"settings-header-contents": "Contents"
}

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -10,9 +10,9 @@
"watch": "Spremljaj.",
"ignore": "Prezri.",
"watching": "Spremljano",
"not-watching": "Not Watching",
"not-watching": "Ni spremljano",
"ignoring": "Prezrto",
"watching.description": "Show topics in unread and recent",
"watching.description": "Prikaži teme v nedavno in nazadnje",
"not-watching.description": "Do not show topics in unread, show in recent",
"ignoring.description": "Do not show topics in unread and recent",
"watching.message": "You are now watching updates from this category and all subcategories",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"control-panel": "Rewards Control",
"new-reward": "New Reward",
"alert.delete-success": "Successfully deleted reward",
"alert.no-inputs-found": "Illegal reward - no inputs found!",

@ -8,8 +8,6 @@
"delete": "Sil",
"enable": "Etkinleştir",
"disable": "Etkinsizleştir",
"control-panel": "Ödül Kontrol Paneli",
"new-reward": "Yeni Ödül Ekle",
"alert.delete-success": "Ödül başarıyla silindi",
"alert.no-inputs-found": "Usulsüz ödül - girdi bulunamadı!",

@ -8,8 +8,6 @@
"delete": "Видалити",
"enable": "Увімкнути",
"disable": "Вимкнути",
"control-panel": "Керування нагородами",
"new-reward": "Нова нагорода",
"alert.delete-success": "Нагороду успішно видалено",
"alert.no-inputs-found": "Невірна нагорода — поля пусті!",

@ -8,8 +8,6 @@
"delete": "Xóa",
"enable": "Bật",
"disable": "Tắt",
"control-panel": "Kiểm Soát Phần Thưởng",
"new-reward": "Phần Thưởng Mới",
"alert.delete-success": "Đã xóa thành công phần thưởng",
"alert.no-inputs-found": "Phần thưởng không hợp lệ - không tìm thấy đầu vào!",

@ -59,5 +59,5 @@
"alert.admin-warning": "Quản trị viên ngầm có tất cả các đặc quyền",
"alert.copyPrivilegesFrom-title": "Chọn một danh mục để sao chép từ",
"alert.copyPrivilegesFrom-warning": "Điều này sẽ sao chép <strong>%1</strong> từ danh mục đã chọn.",
"alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of <strong>%1</strong> from the selected category."
"alert.copyPrivilegesFromGroup-warning": "Thao tác này sẽ sao chép cài đặt <strong>%1</strong> của nhóm này từ danh mục đã chọn."
}

@ -8,8 +8,6 @@
"delete": "删除",
"enable": "启用",
"disable": "禁用",
"control-panel": "奖励控制",
"new-reward": "新奖励",
"alert.delete-success": "已成功删除奖励",
"alert.no-inputs-found": "非法奖励 输入为空!",

@ -8,8 +8,6 @@
"delete": "刪除",
"enable": "啟用",
"disable": "禁用",
"control-panel": "獎勵控制",
"new-reward": "新獎勵",
"alert.delete-success": "已成功刪除獎勵",
"alert.no-inputs-found": "非法獎勵 輸入為空!",

@ -16,6 +16,36 @@
> li {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
&:last-child {
border-bottom: 0;
}
}
}
.rewards { width: 100%; }
.well {
border-radius: 2px;
border-width: 2px;
color: #333;
&.if-block {
border-color: @brand-primary;
}
&.this-block {
border-color: @brand-warning;
}
&.then-block {
border-color: @brand-success;
}
&.reward-block {
border-color: @brand-success;
background-color: lighten(@brand-success, 15%);
color: #fff;
a, select, input { color: #fff; }
select > option { color: #333; }
width: 100%;
min-height: 110px;
}
}
}

@ -35,4 +35,18 @@
[component="category/list"] li {
cursor: pointer;
}
.fa-nbb-none {
border: 1px dotted black;
height: 35px;
width: 35px;
}
.fa-icons .fa-nbb-none {
vertical-align: -6px;
}
#group-icon-preview.fa-nbb-none {
display: none;
}
}

@ -52,6 +52,8 @@ paths:
$ref: 'write/users.yaml'
/users/{uid}:
$ref: 'write/users/uid.yaml'
/users/{uid}/picture:
$ref: 'write/users/uid/picture.yaml'
/users/{uid}/content:
$ref: 'write/users/uid/content.yaml'
/users/{uid}/account:

@ -0,0 +1,43 @@
put:
tags:
- users
summary: update user picture or icon background colour
parameters:
- in: path
name: uid
schema:
type: integer
required: true
description: uid of the user
example: 1
requestBody:
content:
application/json:
schema:
type: object
properties:
type:
type: string
description: The source of the picture
enum: ['default', 'uploaded', 'external']
example: default
url:
type: string
description: Only used for `external` type, specifies the source of the external image to use as avatar
example: ''
bgColor:
type: string
description: A hexadecimal colour representation
example: '#ff0000'
responses:
'200':
description: successfully updated user picture
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object

@ -79,6 +79,12 @@ define('admin/extend/plugins', [
callback: toggleActivate,
},
},
onShown: function () {
const saveEl = this.querySelector('button.btn-primary');
if (saveEl) {
saveEl.focus();
}
},
});
});
} else {

@ -95,13 +95,13 @@ define('admin/extend/rewards', [], function () {
html += '<label for="' + input.name + '">' + input.label + '<br />';
switch (input.type) {
case 'select':
html += '<select name="' + input.name + '">';
html += '<select class="form-control" name="' + input.name + '">';
input.values.forEach(function (value) {
html += '<option value="' + value.value + '">' + value.name + '</option>';
});
break;
case 'text':
html += '<input type="text" name="' + input.name + '" />';
html += '<input type="text" class="form-control" name="' + input.name + '" />';
break;
}
html += '</label><br />';

@ -2,7 +2,8 @@
define('accounts/picture', [
'pictureCropper',
], (pictureCropper) => {
'api',
], (pictureCropper, api) => {
const Picture = {};
Picture.openChangeModal = () => {
@ -89,14 +90,10 @@ define('accounts/picture', [
var type = modal.find('.list-group-item.active').attr('data-type');
const iconBgColor = document.querySelector('.modal.picture-switcher input[type="radio"]:checked').value || 'transparent';
changeUserPicture(type, iconBgColor, function (err) {
if (err) {
return app.alertError(err.message);
}
changeUserPicture(type, iconBgColor).then(() => {
Picture.updateHeader(type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'), iconBgColor);
ajaxify.refresh();
});
}).catch(app.alertError);
}
function onCloseModal() {
@ -212,12 +209,8 @@ define('accounts/picture', [
});
}
function changeUserPicture(type, bgColor, callback) {
socket.emit('user.changePicture', {
type,
bgColor,
uid: ajaxify.data.theirid,
}, callback);
function changeUserPicture(type, bgColor) {
return api.put(`/users/${ajaxify.data.theirid}/picture`, { type, bgColor });
}
return Picture;

@ -65,10 +65,24 @@ categoriesAPI.setPrivilege = async (caller, data) => {
if (!userExists && !groupExists) {
throw new Error('[[error:no-user-or-group]]');
}
await privileges.categories[data.set ? 'give' : 'rescind'](
Array.isArray(data.privilege) ? data.privilege : [data.privilege], data.cid, data.member
);
const privs = Array.isArray(data.privilege) ? data.privilege : [data.privilege];
const type = data.set ? 'give' : 'rescind';
if (!privs.length) {
throw new Error('[[error:invalid-data]]');
}
if (parseInt(data.cid, 10) === 0) {
const adminPrivs = privs.filter(priv => privileges.admin.privilegeList.includes(priv));
const globalPrivs = privs.filter(priv => privileges.global.privilegeList.includes(priv));
if (adminPrivs.length) {
await privileges.admin[type](adminPrivs, data.member);
}
if (globalPrivs.length) {
await privileges.global[type](globalPrivs, data.member);
}
} else {
const categoryPrivs = privs.filter(priv => privileges.categories.privilegeList.includes(priv));
await privileges.categories[type](categoryPrivs, data.cid, data.member);
}
await events.log({
uid: caller.uid,

@ -227,14 +227,9 @@ usersAPI.unban = async function (caller, data) {
async function isPrivilegedOrSelfAndPasswordMatch(caller, data) {
const { uid } = caller;
const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10);
const canEdit = await privileges.users.canEdit(uid, data.uid);
const [isAdmin, isTargetAdmin, isGlobalMod] = await Promise.all([
user.isAdministrator(uid),
user.isAdministrator(data.uid),
user.isGlobalModerator(uid),
]);
if ((isTargetAdmin && !isAdmin) || (!isSelf && !(isAdmin || isGlobalMod))) {
if (!canEdit) {
throw new Error('[[error:no-privileges]]');
}
const [hasPassword, passwordMatch] = await Promise.all([
@ -341,3 +336,44 @@ usersAPI.search = async function (caller, data) {
filters: filters,
});
};
usersAPI.changePicture = async (caller, data) => {
if (!data) {
throw new Error('[[error:invalid-data]]');
}
const { type, url } = data;
let picture = '';
await user.checkMinReputation(caller.uid, data.uid, 'min:rep:profile-picture');
const canEdit = await privileges.users.canEdit(caller.uid, data.uid);
if (!canEdit) {
throw new Error('[[error:no-privileges]]');
}
if (type === 'default') {
picture = '';
} else if (type === 'uploaded') {
picture = await user.getUserField(data.uid, 'uploadedpicture');
} else if (type === 'external' && url) {
picture = validator.escape(url);
} else {
const returnData = await plugins.hooks.fire('filter:user.getPicture', {
uid: caller.uid,
type: type,
picture: undefined,
});
picture = returnData && returnData.picture;
}
const validBackgrounds = await user.getIconBackgrounds(caller.uid);
if (!validBackgrounds.includes(data.bgColor)) {
data.bgColor = validBackgrounds[0];
}
await user.updateProfile(caller.uid, {
uid: data.uid,
picture: picture,
'icon:bgColor': data.bgColor,
}, ['picture', 'icon:bgColor']);
};

@ -43,13 +43,13 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
userData.emailClass = 'hide';
if (!isAdmin && !isGlobalModerator && !isSelf && (!userSettings.showemail || meta.config.hideEmail)) {
if (!results.canEdit && (!userSettings.showemail || meta.config.hideEmail)) {
userData.email = '';
} else if (!userSettings.showemail) {
userData.emailClass = '';
}
if (!isAdmin && !isGlobalModerator && !isSelf && (!userSettings.showfullname || meta.config.hideFullname)) {
if (!results.canEdit && (!userSettings.showfullname || meta.config.hideFullname)) {
userData.fullname = '';
}
@ -78,7 +78,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
userData.isSelf = isSelf;
userData.isFollowing = results.isFollowing;
userData.hasPrivateChat = results.hasPrivateChat;
userData.showHidden = isSelf || isAdmin || (isGlobalModerator && !results.isTargetAdmin);
userData.showHidden = results.canEdit; // remove in v1.19.0
userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : [];
userData.disableSignatures = meta.config.disableSignatures === 1;
userData['reputation:disabled'] = meta.config['reputation:disabled'] === 1;

@ -138,16 +138,14 @@ async function addToApprovalQueue(req, userData) {
return { message: message };
}
authenticationController.registerComplete = function (req, res, next) {
// For the interstitials that respond, execute the callback with the form body
plugins.hooks.fire('filter:register.interstitial', {
req,
userData: req.session.registration,
interstitials: [],
}, async (err, data) => {
if (err) {
return next(err);
}
authenticationController.registerComplete = async function (req, res) {
try {
// For the interstitials that respond, execute the callback with the form body
const data = await plugins.hooks.fire('filter:register.interstitial', {
req,
userData: req.session.registration,
interstitials: [],
});
const callbacks = data.interstitials.reduce((memo, cur) => {
if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') {
@ -165,13 +163,10 @@ authenticationController.registerComplete = function (req, res, next) {
return memo;
}, []);
const done = function (err, data) {
const done = function (data) {
delete req.session.registration;
if (err) {
return res.redirect(`${nconf.get('relative_path')}/?register=${encodeURIComponent(err.message)}`);
}
if (!err && data && data.message) {
if (data && data.message) {
return res.redirect(`${nconf.get('relative_path')}/?register=${encodeURIComponent(data.message)}`);
}
@ -199,8 +194,7 @@ authenticationController.registerComplete = function (req, res, next) {
if (!data) {
return winston.warn('[register] Interstitial callbacks processed with no errors, but one or more interstitials remain. This is likely an issue with one of the interstitials not properly handling a null case or invalid value.');
}
done();
done(data);
} else {
// Update user hash, clear registration data in session
const payload = req.session.registration;
@ -217,7 +211,10 @@ authenticationController.registerComplete = function (req, res, next) {
await user.setUserFields(uid, payload);
done();
}
});
} catch (err) {
delete req.session.registration;
res.redirect(`${nconf.get('relative_path')}/?register=${encodeURIComponent(err.message)}`);
}
};
authenticationController.registerAbort = function (req, res) {

@ -76,6 +76,11 @@ Users.deleteMany = async (req, res) => {
helpers.formatApiResponse(200, res);
};
Users.changePicture = async (req, res) => {
await api.users.changePicture(req, { ...req.body, uid: req.params.uid });
helpers.formatApiResponse(200, res);
};
Users.updateSettings = async (req, res) => {
const settings = await api.users.updateSettings(req, { ...req.body, uid: req.params.uid });
helpers.formatApiResponse(200, res, settings);

@ -14,6 +14,9 @@ module.exports = function (module) {
}
const writeData = helpers.serializeData(data);
if (!Object.keys(writeData).length) {
return;
}
try {
if (isArray) {
const bulk = module.client.collection('objects').initializeUnorderedBulkOp();
@ -39,9 +42,18 @@ module.exports = function (module) {
const writeData = data.map(helpers.serializeData);
try {
const bulk = module.client.collection('objects').initializeUnorderedBulkOp();
keys.forEach((key, i) => bulk.find({ _key: key }).upsert().updateOne({ $set: writeData[i] }));
await bulk.execute();
let bulk;
keys.forEach((key, i) => {
if (Object.keys(writeData[i]).length) {
if (!bulk) {
bulk = module.client.collection('objects').initializeUnorderedBulkOp();
}
bulk.find({ _key: key }).upsert().updateOne({ $set: writeData[i] });
}
});
if (bulk) {
await bulk.execute();
}
} catch (err) {
if (err && err.message.startsWith('E11000 duplicate key error')) {
return await module.setObjectBulk(keys, data);

@ -11,7 +11,9 @@ module.exports = function (module) {
if (data.hasOwnProperty('')) {
delete data[''];
}
if (!Object.keys(data).length) {
return;
}
await module.transaction(async (client) => {
const dataString = JSON.stringify(data);
async function setOne(key) {

@ -41,7 +41,11 @@ module.exports = function (module) {
return;
}
const batch = module.client.batch();
keys.forEach((k, i) => batch.hmset(k, data[i]));
keys.forEach((k, i) => {
if (Object.keys(data[i]).length) {
batch.hmset(k, data[i]);
}
});
await helpers.execBatch(batch);
cache.del(keys);
};

@ -324,7 +324,7 @@ Emailer.sendToEmail = async (template, email, language, params) => {
!Plugins.hooks.hasListeners('static:email.send');
try {
if (Plugins.hooks.hasListeners('filter:email.send')) {
// Deprecated, remove in v1.18.0
// Deprecated, remove in v1.19.0
await Plugins.hooks.fire('filter:email.send', data);
} else if (Plugins.hooks.hasListeners('static:email.send')) {
await Plugins.hooks.fire('static:email.send', data);

@ -66,6 +66,7 @@ Tags.parse = async (req, data, meta, link) => {
}, {
rel: 'manifest',
href: `${relative_path}/manifest.webmanifest`,
crossorigin: `use-credentials`,
}];
if (plugins.hooks.hasListeners('filter:search.query')) {

@ -82,7 +82,7 @@ module.exports = function (middleware) {
return !res.headersSent;
}
// TODO: Remove in v1.18.0
// TODO: Remove in v1.19.0
middleware.authenticate = helpers.try(async (req, res, next) => {
winston.warn(`[middleware] middleware.authenticate has been deprecated, page and API routes are now automatically authenticated via setup(Page|API)Route. Use middleware.authenticateRequest (if not using route helper) and middleware.ensureLoggedIn instead. (request path: ${req.path})`);
if (!await authenticate(req, res)) {
@ -101,7 +101,7 @@ module.exports = function (middleware) {
next();
});
// TODO: Remove in v1.18.0
// TODO: Remove in v1.19.0
middleware.authenticateOrGuest = (req, res, next) => {
winston.warn(`[middleware] middleware.authenticateOrGuest has been renamed, use middleware.authenticateRequest instead. (request path: ${req.path})`);
middleware.authenticateRequest(req, res, next);

@ -8,7 +8,7 @@ const utils = require('../utils');
const Hooks = module.exports;
Hooks.deprecatedHooks = {
'filter:email.send': 'static:email.send', // 👋 @ 1.18.0
'filter:email.send': 'static:email.send', // 👋 @ 1.19.0
'filter:router.page': 'response:router.page', // 👋 @ 2.0.0
};

@ -1,7 +1,6 @@
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const winston = require('winston');
const semver = require('semver');
@ -25,23 +24,6 @@ require('./usage')(Plugins);
Plugins.data = require('./data');
Plugins.hooks = require('./hooks');
// Backwards compatibility for hooks, remove in v1.18.0
const _deprecate = async function (...args) {
const oldMethod = args.shift();
const newMethod = args.shift();
const method = args.shift();
const stack = new Error().stack.toString().split(os.EOL);
const context = stack[stack.findIndex(line => line.startsWith(' at Object.wrapperCallback')) + 1];
winston.warn(`[plugins/hooks] ${oldMethod} has been deprecated, call ${newMethod} instead.`);
winston.warn(`[plugins/hooks] ${context}`);
return method.apply(Plugins.hooks, args);
};
Plugins.registerHook = _deprecate.bind(null, 'Plugins.registerHook', 'Plugins.hooks.register', Plugins.hooks.register);
Plugins.unregisterHook = _deprecate.bind(null, 'Plugins.unregisterHook', 'Plugins.hooks.unregister', Plugins.hooks.unregister);
Plugins.fireHook = _deprecate.bind(null, 'Plugins.fireHook', 'Plugins.hooks.fire', Plugins.hooks.fire);
Plugins.hasListeners = _deprecate.bind(null, 'Plugins.hasListeners', 'Plugins.hooks.hasListeners', Plugins.hooks.hasListeners);
// end
Plugins.getPluginPaths = Plugins.data.getPluginPaths;
Plugins.loadPluginInfo = Plugins.data.loadPluginInfo;

@ -35,6 +35,8 @@ privsAdmin.userPrivilegeList = [
privsAdmin.groupPrivilegeList = privsAdmin.userPrivilegeList.map(privilege => `groups:${privilege}`);
privsAdmin.privilegeList = privsAdmin.userPrivilegeList.concat(privsAdmin.groupPrivilegeList);
// Mapping for a page route (via direct match or regexp) to a privilege
privsAdmin.routeMap = {
dashboard: 'admin:dashboard',

@ -49,6 +49,8 @@ privsGlobal.userPrivilegeList = [
privsGlobal.groupPrivilegeList = privsGlobal.userPrivilegeList.map(privilege => `groups:${privilege}`);
privsGlobal.privilegeList = privsGlobal.userPrivilegeList.concat(privsGlobal.groupPrivilegeList);
privsGlobal.list = async function () {
async function getLabels() {
return await utils.promiseParallel({

@ -9,20 +9,3 @@ privileges.posts = require('./posts');
privileges.users = require('./users');
require('../promisify')(privileges);
// TODO: backwards compatibility remove in 1.18.0
[
'privilegeLabels',
'userPrivilegeList',
'groupPrivilegeList',
'privilegeList',
].forEach((fieldName) => {
Object.defineProperty(privileges, fieldName, {
configurable: true,
enumerable: true,
get: function () {
console.warn(`[deprecated] privileges.${fieldName} is deprecated. Use privileges.categories.${fieldName}`);
return privileges.categories[fieldName];
},
});
});

@ -22,6 +22,7 @@ function authenticatedRoutes() {
setupApiRoute(router, 'get', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.get);
setupApiRoute(router, 'put', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.update);
setupApiRoute(router, 'delete', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.delete);
setupApiRoute(router, 'put', '/:uid/picture', [...middlewares, middleware.assert.user], controllers.write.users.changePicture);
setupApiRoute(router, 'delete', '/:uid/content', [...middlewares, middleware.assert.user], controllers.write.users.deleteContent);
setupApiRoute(router, 'delete', '/:uid/account', [...middlewares, middleware.assert.user], controllers.write.users.deleteAccount);

@ -3,42 +3,17 @@
const user = require('../../user');
const plugins = require('../../plugins');
const websockets = require('../index');
const api = require('../../api');
module.exports = function (SocketUser) {
SocketUser.changePicture = async function (socket, data) {
if (!socket.uid) {
throw new Error('[[error:invalid-uid]]');
}
if (!data) {
throw new Error('[[error:invalid-data]]');
}
const { type } = data;
let picture = '';
await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid);
if (type === 'default') {
picture = '';
} else if (type === 'uploaded') {
picture = await user.getUserField(data.uid, 'uploadedpicture');
} else {
const returnData = await plugins.hooks.fire('filter:user.getPicture', {
uid: socket.uid,
type: type,
picture: undefined,
});
picture = returnData && returnData.picture;
}
const validBackgrounds = await user.getIconBackgrounds(socket.uid);
if (!validBackgrounds.includes(data.bgColor)) {
data.bgColor = validBackgrounds[0];
}
await user.updateProfile(socket.uid, {
uid: data.uid,
picture: picture,
'icon:bgColor': data.bgColor,
}, ['picture', 'icon:bgColor']);
websockets.warnDeprecated(socket, 'PUT /api/v3/users/:uid/picture');
await api.users.changePicture(socket, data);
};
SocketUser.removeUploadedPicture = async function (socket, data) {

@ -6,6 +6,7 @@ const api = require('../../api');
const user = require('../../user');
const events = require('../../events');
const notifications = require('../../notifications');
const privileges = require('../../privileges');
const db = require('../../database');
const plugins = require('../../plugins');
const sockets = require('..');
@ -31,10 +32,10 @@ module.exports = function (SocketUser) {
};
SocketUser.uploadCroppedPicture = async function (socket, data) {
if (!socket.uid) {
if (!socket.uid || !(await privileges.users.canEdit(socket.uid, data.uid))) {
throw new Error('[[error:no-privileges]]');
}
await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid);
await user.checkMinReputation(socket.uid, data.uid, 'min:rep:profile-picture');
data.callerUid = socket.uid;
return await user.uploadCroppedPicture(data);

@ -1,76 +1,82 @@
<div id="rewards" class="row">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading">[[admin/extend/rewards:rewards]]</div>
<div class="panel-body">
<ul id="active">
{{{ each active }}}
<li data-rid="{active.rid}" data-id="{active.id}">
<form class="main inline-block">
<div class="well inline-block">
<label for="condition">[[admin/extend/rewards:condition-if-users]]</label><br />
<select name="condition" data-selected="{active.condition}">
{{{ each conditions }}}
<option value="{conditions.condition}">{conditions.name}</option>
{{{ end }}}
</select>
</div>
<div class="well inline-block">
<label for="condition">[[admin/extend/rewards:condition-is]]</label><br />
<select name="conditional" data-selected="{active.conditional}">
{{{ each conditionals }}}
<option value="{conditionals.conditional}">{conditionals.name}</option>
{{{ end }}}
</select>
<input type="text" name="value" value="{active.value}" />
</div>
<div class="well inline-block">
<label for="condition">[[admin/extend/rewards:condition-then]]</label><br />
<select name="rid" data-selected="{active.rid}">
<!-- BEGIN ../../rewards -->
<option value="{rewards.rid}">{rewards.name}</option>
<!-- END ../../rewards -->
</select>
</div>
</form>
<form class="rewards inline-block">
<div class="inputs well inline-block"></div>
</form>
<div class="clearfix"></div>
<div class="pull-right">
<div class="panel-body inline-block">
<form class="main">
<label for="claimable">[[admin/extend/rewards:max-claims]]</label><br />
<input type="text" name="claimable" value="{active.claimable}" placeholder="1" />
<small>[[admin/extend/rewards:zero-infinite]]</small>
</form>
</div>
<div class="panel-body inline-block">
<button class="btn btn-danger delete">[[admin/extend/rewards:delete]]</button>
<!-- IF active.disabled -->
<button class="btn btn-success toggle">[[admin/extend/rewards:enable]]</button>
<!-- ELSE -->
<button class="btn btn-warning toggle">[[admin/extend/rewards:disable]]</button>
<!-- ENDIF active.disabled -->
<div id="rewards">
<ul id="active">
{{{ each active }}}
<li data-rid="{active.rid}" data-id="{active.id}">
<div class="row">
<div class="col-xs-12 col-sm-12 col-lg-8">
<form class="main inline-block">
<div class="well inline-block if-block">
<label for="condition">[[admin/extend/rewards:condition-if-users]]</label><br />
<select class="form-control" name="condition" data-selected="{active.condition}">
{{{ each conditions }}}
<option value="{conditions.condition}">{conditions.name}</option>
{{{ end }}}
</select>
</div>
<div class="well inline-block this-block">
<label for="condition">[[admin/extend/rewards:condition-is]]</label><br />
<div class="row">
<div class="col-xs-6">
<select class="form-control" name="conditional" data-selected="{active.conditional}">
{{{ each conditionals }}}
<option value="{conditionals.conditional}">{conditionals.name}</option>
{{{ end }}}
</select>
</div>
<div class="col-xs-6">
<input class="form-control" type="text" name="value" value="{active.value}" />
</div>
</div>
</div>
<div class="clearfix"></div>
</li>
{{{ end }}}
</ul>
<div class="well inline-block then-block">
<label for="condition">[[admin/extend/rewards:condition-then]]</label><br />
<select class="form-control" name="rid" data-selected="{active.rid}">
<!-- BEGIN ../../rewards -->
<option value="{rewards.rid}">{rewards.name}</option>
<!-- END ../../rewards -->
</select>
</div>
</form>
</div>
<div class="col-xs-12 col-sm-12 col-lg-4">
<form class="rewards inline-block">
<div class="inputs well inline-block reward-block"></div>
</form>
</div>
</div>
<div class="pull-left">
<div class="panel-body inline-block">
<form class="main">
<label for="claimable">[[admin/extend/rewards:max-claims]] <small>[[admin/extend/rewards:zero-infinite]]</small></label><br />
<input class="form-control" type="text" name="claimable" value="{active.claimable}" placeholder="1" />
</form>
</div>
</div>
</div>
</div>
<div class="col-lg-3 acp-sidebar">
<div class="panel panel-default">
<div class="panel-heading">[[admin/extend/rewards:control-panel]]</div>
<div class="panel-body">
<button class="btn btn-success btn-md" id="new">[[admin/extend/rewards:new-reward]]</button>
<button class="btn btn-primary btn-md" id="save">[[global:save_changes]]</button>
<div class="pull-right">
<div class="panel-body inline-block">
<button class="btn btn-danger delete">[[admin/extend/rewards:delete]]</button>
<!-- IF active.disabled -->
<button class="btn btn-success toggle">[[admin/extend/rewards:enable]]</button>
<!-- ELSE -->
<button class="btn btn-warning toggle">[[admin/extend/rewards:disable]]</button>
<!-- ENDIF active.disabled -->
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
</li>
{{{ end }}}
</ul>
</div>
<div class="floating-button">
<button id="new" class="mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored" >
<i class="material-icons">add</i>
</button>
<button id="save" class="mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored primary">
<i class="material-icons">save</i>
</button>
</div>

@ -2,8 +2,10 @@
<div class="panel-heading">
[[admin/dashboard:forum-traffic]]
<div class="pull-right">
<a id="view-as-json" href="{config.relative_path}/api/v3/admin/analytics/{set}?type=hourly"><i class="fa fa-terminal"></i></a>
<i class="fa fa-expand"></i>
<a href="#"><i class="fa fa-fw fa-expand"></i></a>
</div>
<div class="pull-right">
<a target="_blank" id="view-as-json" href="{config.relative_path}/api/v3/admin/analytics/{set}?type=hourly"><i class="fa fa-fw fa-terminal"></i></a>
</div>
</div>
<div class="panel-body">

@ -72,7 +72,7 @@ describe('Hash methods', () => {
});
});
it('should set multiple keys to different okjects', async () => {
it('should set multiple keys to different objects', async () => {
const keys = ['bulkKey1', 'bulkKey2'];
const data = [{ foo: '1' }, { baz: 'baz' }];
@ -80,6 +80,39 @@ describe('Hash methods', () => {
const result = await db.getObjects(keys);
assert.deepStrictEqual(result, data);
});
it('should not error if object is empty', async () => {
const keys = ['bulkKey3', 'bulkKey4'];
const data = [{ foo: '1' }, { }];
await db.setObjectBulk(keys, data);
const result = await db.getObjects(keys);
assert.deepStrictEqual(result, [{ foo: '1' }, null]);
});
it('should not error if object is empty', async () => {
const keys = ['bulkKey5'];
const data = [{ }];
await db.setObjectBulk(keys, data);
const result = await db.getObjects(keys);
assert.deepStrictEqual(result, [null]);
});
it('should not error if object is empty', async () => {
const keys = ['bulkKey6', 'bulkKey7'];
const data = {};
await db.setObject(keys, data);
const result = await db.getObjects(keys);
assert.deepStrictEqual(result, [null, null]);
});
it('should not error if object is empty', async () => {
await db.setObject('emptykey', {});
const result = await db.getObject('emptykey');
assert.deepStrictEqual(result, null);
});
});
describe('setObjectField()', () => {

@ -10,6 +10,16 @@ const utils = require('../../public/src/utils');
const helpers = module.exports;
helpers.getCsrfToken = async (jar) => {
const { csrf_token: token } = await requestAsync({
url: `${nconf.get('url')}/api/config`,
json: true,
jar,
});
return token;
};
helpers.loginUser = function (username, password, callback) {
const jar = request.jar();

@ -5,6 +5,7 @@ const async = require('async');
const fs = require('fs');
const path = require('path');
const nconf = require('nconf');
const validator = require('validator');
const request = require('request');
const requestAsync = require('request-promise-native');
const jwt = require('jsonwebtoken');
@ -1070,6 +1071,28 @@ describe('User', () => {
});
});
it('should let you set an external image', async () => {
const token = await helpers.getCsrfToken(jar);
const body = await requestAsync(`${nconf.get('url')}/api/v3/users/${uid}/picture`, {
jar,
method: 'put',
json: true,
headers: {
'x-csrf-token': token,
},
body: {
type: 'external',
url: 'https://example.org/picture.jpg',
},
});
assert(body && body.status && body.response);
assert.strictEqual(body.status.code, 'ok');
const picture = await User.getUserField(uid, 'picture');
assert.strictEqual(picture, validator.escape('https://example.org/picture.jpg'));
});
it('should fail to change user picture with invalid data', (done) => {
socketUser.changePicture({ uid: uid }, null, (err) => {
assert.equal(err.message, '[[error:invalid-data]]');

Loading…
Cancel
Save