From 69fe8ec31a0827eda913e55ca464228168ffcaf1 Mon Sep 17 00:00:00 2001
From: rr- <rr-@sakuya.pl>
Date: Fri, 20 May 2016 21:35:12 +0200
Subject: [PATCH] client/general: refactor all the things

- Move controls to the "controls/" directory
- Make controls interface look similar to each other
- Prefix "private" methods and attributes with underscore
---
 client/js/api.js                              |   4 +-
 client/js/controllers/auth_controller.js      |  24 +-
 client/js/controllers/comments_controller.js  |   4 +-
 client/js/controllers/help_controller.js      |  12 +-
 client/js/controllers/history_controller.js   |   4 +-
 client/js/controllers/home_controller.js      |  12 +-
 client/js/controllers/page_controller.js      |  16 +-
 client/js/controllers/posts_controller.js     |  16 +-
 client/js/controllers/settings_controller.js  |   8 +-
 client/js/controllers/tags_controller.js      |  57 ++--
 client/js/controllers/top_nav_controller.js   |  48 +--
 client/js/controllers/users_controller.js     |  58 ++--
 client/js/controls/auto_complete_control.js   | 297 +++++++++++++++++
 client/js/controls/file_dropper_control.js    |  76 +++++
 .../tag_auto_complete_control.js              |   0
 .../{views => controls}/tag_input_control.js  |  13 +-
 client/js/views/auto_complete_control.js      | 298 ------------------
 client/js/views/empty_view.js                 |   4 +-
 client/js/views/endless_page_view.js          |  54 ++--
 client/js/views/file_dropper_control.js       |  67 ----
 client/js/views/help_view.js                  |  20 +-
 client/js/views/home_view.js                  |   4 +-
 client/js/views/login_view.js                 |   4 +-
 client/js/views/manual_page_view.js           |  20 +-
 client/js/views/password_reset_view.js        |   4 +-
 client/js/views/registration_view.js          |   4 +-
 client/js/views/settings_view.js              |   4 +-
 client/js/views/tag_categories_view.js        |   4 +-
 client/js/views/tag_delete_view.js            |   4 +-
 client/js/views/tag_merge_view.js             |   7 +-
 client/js/views/tag_summary_view.js           |   6 +-
 client/js/views/tag_view.js                   |  16 +-
 client/js/views/tags_header_view.js           |   7 +-
 client/js/views/tags_page_view.js             |   4 +-
 client/js/views/top_nav_view.js               |  10 +-
 client/js/views/user_delete_view.js           |   4 +-
 client/js/views/user_edit_view.js             |  28 +-
 client/js/views/user_summary_view.js          |   4 +-
 client/js/views/user_view.js                  |  16 +-
 client/js/views/users_header_view.js          |   4 +-
 client/js/views/users_page_view.js            |   4 +-
 41 files changed, 633 insertions(+), 617 deletions(-)
 create mode 100644 client/js/controls/auto_complete_control.js
 create mode 100644 client/js/controls/file_dropper_control.js
 rename client/js/{views => controls}/tag_auto_complete_control.js (100%)
 rename client/js/{views => controls}/tag_input_control.js (97%)
 delete mode 100644 client/js/views/auto_complete_control.js
 delete mode 100644 client/js/views/file_dropper_control.js

diff --git a/client/js/api.js b/client/js/api.js
index b23daa5..f5cadfa 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -51,7 +51,7 @@ class Api {
     }
 
     _process(url, requestFactory, data, files) {
-        const fullUrl = this.getFullUrl(url);
+        const fullUrl = this._getFullUrl(url);
         return new Promise((resolve, reject) => {
             nprogress.start();
             let req = requestFactory(fullUrl);
@@ -161,7 +161,7 @@ class Api {
         }
     }
 
-    getFullUrl(url) {
+    _getFullUrl(url) {
         return (config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
     }
 }
diff --git a/client/js/controllers/auth_controller.js b/client/js/controllers/auth_controller.js
index 01c0143..4470ae0 100644
--- a/client/js/controllers/auth_controller.js
+++ b/client/js/controllers/auth_controller.js
@@ -9,24 +9,24 @@ const PasswordResetView = require('../views/password_reset_view.js');
 
 class AuthController {
     constructor() {
-        this.loginView = new LoginView();
-        this.passwordResetView = new PasswordResetView();
+        this._loginView = new LoginView();
+        this._passwordResetView = new PasswordResetView();
     }
 
     registerRoutes() {
         page(/\/password-reset\/([^:]+):([^:]+)$/,
             (ctx, next) => {
-                this.passwordResetFinishRoute(ctx.params[0], ctx.params[1]);
+                this._passwordResetFinishRoute(ctx.params[0], ctx.params[1]);
             });
-        page('/password-reset', (ctx, next) => { this.passwordResetRoute(); });
-        page('/login', (ctx, next) => { this.loginRoute(); });
-        page('/logout', (ctx, next) => { this.logoutRoute(); });
+        page('/password-reset', (ctx, next) => { this._passwordResetRoute(); });
+        page('/login', (ctx, next) => { this._loginRoute(); });
+        page('/logout', (ctx, next) => { this._logoutRoute(); });
     }
 
-    loginRoute() {
+    _loginRoute() {
         api.forget();
         topNavController.activate('login');
-        this.loginView.render({
+        this._loginView.render({
             login: (name, password, doRemember) => {
                 return new Promise((resolve, reject) => {
                     api.forget();
@@ -43,22 +43,22 @@ class AuthController {
             }});
     }
 
-    logoutRoute() {
+    _logoutRoute() {
         api.forget();
         api.logout();
         page('/');
         events.notify(events.Success, 'Logged out');
     }
 
-    passwordResetRoute() {
+    _passwordResetRoute() {
         topNavController.activate('login');
-        this.passwordResetView.render({
+        this._passwordResetView.render({
             proceed: (...args) => {
                 return this._passwordReset(...args);
             }});
     }
 
-    passwordResetFinishRoute(name, token) {
+    _passwordResetFinishRoute(name, token) {
         api.forget();
         api.logout();
         api.post('/password-reset/' + name, {token: token})
diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js
index 1b20fca..11ed639 100644
--- a/client/js/controllers/comments_controller.js
+++ b/client/js/controllers/comments_controller.js
@@ -5,10 +5,10 @@ const topNavController = require('../controllers/top_nav_controller.js');
 
 class CommentsController {
     registerRoutes() {
-        page('/comments', (ctx, next) => { this.listCommentsRoute(); });
+        page('/comments', (ctx, next) => { this._listCommentsRoute(); });
     }
 
-    listCommentsRoute() {
+    _listCommentsRoute() {
         topNavController.activate('comments');
     }
 }
diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js
index 797d2ac..6c352c3 100644
--- a/client/js/controllers/help_controller.js
+++ b/client/js/controllers/help_controller.js
@@ -6,24 +6,24 @@ const HelpView = require('../views/help_view.js');
 
 class HelpController {
     constructor() {
-        this.helpView = new HelpView();
+        this._helpView = new HelpView();
     }
 
     registerRoutes() {
-        page('/help', () => { this.showHelpRoute(); });
+        page('/help', () => { this._showHelpRoute(); });
         page(
             '/help/:section',
-            (ctx, next) => { this.showHelpRoute(ctx.params.section); });
+            (ctx, next) => { this._showHelpRoute(ctx.params.section); });
         page(
             '/help/:section/:subsection',
             (ctx, next) => {
-                this.showHelpRoute(ctx.params.section, ctx.params.subsection);
+                this._showHelpRoute(ctx.params.section, ctx.params.subsection);
             });
     }
 
-    showHelpRoute(section, subsection) {
+    _showHelpRoute(section, subsection) {
         topNavController.activate('help');
-        this.helpView.render({
+        this._helpView.render({
             section: section,
             subsection: subsection,
         });
diff --git a/client/js/controllers/history_controller.js b/client/js/controllers/history_controller.js
index 8c0915e..fdd8562 100644
--- a/client/js/controllers/history_controller.js
+++ b/client/js/controllers/history_controller.js
@@ -5,10 +5,10 @@ const topNavController = require('../controllers/top_nav_controller.js');
 
 class HistoryController {
     registerRoutes() {
-        page('/history', (ctx, next) => { this.showHistoryRoute(); });
+        page('/history', (ctx, next) => { this._listHistoryRoute(); });
     }
 
-    listHistoryRoute() {
+    _listHistoryRoute() {
         topNavController.activate('');
     }
 }
diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js
index 32144e2..1e839c4 100644
--- a/client/js/controllers/home_controller.js
+++ b/client/js/controllers/home_controller.js
@@ -6,20 +6,20 @@ const HomeView = require('../views/home_view.js');
 
 class HomeController {
     constructor() {
-        this.homeView = new HomeView();
+        this._homeView = new HomeView();
     }
 
     registerRoutes() {
-        page('/', (ctx, next) => { this.indexRoute(); });
-        page('*', (ctx, next) => { this.notFoundRoute(); });
+        page('/', (ctx, next) => { this._indexRoute(); });
+        page('*', (ctx, next) => { this._notFoundRoute(); });
     }
 
-    indexRoute() {
+    _indexRoute() {
         topNavController.activate('home');
-        this.homeView.render({});
+        this._homeView.render({});
     }
 
-    notFoundRoute() {
+    _notFoundRoute() {
         topNavController.activate('');
     }
 }
diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js
index 7b589a3..ee4f4f1 100644
--- a/client/js/controllers/page_controller.js
+++ b/client/js/controllers/page_controller.js
@@ -8,27 +8,27 @@ const ManualPageView = require('../views/manual_page_view.js');
 class PageController {
     constructor() {
         events.listen(events.SettingsChange, () => {
-            this.update();
+            this._update();
             return true;
         });
-        this.update();
+        this._update();
     }
 
-    update() {
+    _update() {
         if (settings.getSettings().endlessScroll) {
-            this.pageView = new EndlessPageView();
+            this._pageView = new EndlessPageView();
         } else {
-            this.pageView = new ManualPageView();
+            this._pageView = new ManualPageView();
         }
     }
 
     run(ctx) {
-        this.pageView.unrender();
-        this.pageView.render(ctx);
+        this._pageView.unrender();
+        this._pageView.render(ctx);
     }
 
     stop() {
-        this.pageView.unrender();
+        this._pageView.unrender();
     }
 }
 
diff --git a/client/js/controllers/posts_controller.js b/client/js/controllers/posts_controller.js
index f55cdef..18cbc84 100644
--- a/client/js/controllers/posts_controller.js
+++ b/client/js/controllers/posts_controller.js
@@ -5,29 +5,29 @@ const topNavController = require('../controllers/top_nav_controller.js');
 
 class PostsController {
     registerRoutes() {
-        page('/upload', (ctx, next) => { this.uploadPostsRoute(); });
-        page('/posts', (ctx, next) => { this.listPostsRoute(); });
+        page('/upload', (ctx, next) => { this._uploadPostsRoute(); });
+        page('/posts', (ctx, next) => { this._listPostsRoute(); });
         page(
             '/post/:id',
-            (ctx, next) => { this.showPostRoute(ctx.params.id); });
+            (ctx, next) => { this._showPostRoute(ctx.params.id); });
         page(
             '/post/:id/edit',
-            (ctx, next) => { this.editPostRoute(ctx.params.id); });
+            (ctx, next) => { this._editPostRoute(ctx.params.id); });
     }
 
-    uploadPostsRoute() {
+    _uploadPostsRoute() {
         topNavController.activate('upload');
     }
 
-    listPostsRoute() {
+    _listPostsRoute() {
         topNavController.activate('posts');
     }
 
-    showPostRoute(id) {
+    _showPostRoute(id) {
         topNavController.activate('posts');
     }
 
-    editPostRoute(id) {
+    _editPostRoute(id) {
         topNavController.activate('posts');
     }
 }
diff --git a/client/js/controllers/settings_controller.js b/client/js/controllers/settings_controller.js
index 777bf3a..96da12c 100644
--- a/client/js/controllers/settings_controller.js
+++ b/client/js/controllers/settings_controller.js
@@ -7,16 +7,16 @@ const SettingsView = require('../views/settings_view.js');
 
 class SettingsController {
     constructor() {
-        this.settingsView = new SettingsView();
+        this._settingsView = new SettingsView();
     }
 
     registerRoutes() {
-        page('/settings', (ctx, next) => { this.settingsRoute(); });
+        page('/settings', (ctx, next) => { this._settingsRoute(); });
     }
 
-    settingsRoute() {
+    _settingsRoute() {
         topNavController.activate('settings');
-        this.settingsView.render({
+        this._settingsView.render({
             getSettings: () => settings.getSettings(),
             saveSettings: newSettings => settings.saveSettings(newSettings),
         });
diff --git a/client/js/controllers/tags_controller.js b/client/js/controllers/tags_controller.js
index c096c16..91330ba 100644
--- a/client/js/controllers/tags_controller.js
+++ b/client/js/controllers/tags_controller.js
@@ -15,31 +15,31 @@ const EmptyView = require('../views/empty_view.js');
 
 class TagsController {
     constructor() {
-        this.tagView = new TagView();
-        this.tagsHeaderView = new TagsHeaderView();
-        this.tagsPageView = new TagsPageView();
-        this.tagCategoriesView = new TagCategoriesView();
-        this.emptyView = new EmptyView();
+        this._tagView = new TagView();
+        this._tagsHeaderView = new TagsHeaderView();
+        this._tagsPageView = new TagsPageView();
+        this._tagCategoriesView = new TagCategoriesView();
+        this._emptyView = new EmptyView();
     }
 
     registerRoutes() {
-        page('/tag-categories', () => { this.tagCategoriesRoute(); });
+        page('/tag-categories', () => { this._tagCategoriesRoute(); });
         page(
             '/tag/:name',
-            (ctx, next) => { this.loadTagRoute(ctx, next); },
-            (ctx, next) => { this.showTagRoute(ctx, next); });
+            (ctx, next) => { this._loadTagRoute(ctx, next); },
+            (ctx, next) => { this._showTagRoute(ctx, next); });
         page(
             '/tag/:name/merge',
-            (ctx, next) => { this.loadTagRoute(ctx, next); },
-            (ctx, next) => { this.mergeTagRoute(ctx, next); });
+            (ctx, next) => { this._loadTagRoute(ctx, next); },
+            (ctx, next) => { this._mergeTagRoute(ctx, next); });
         page(
             '/tag/:name/delete',
-            (ctx, next) => { this.loadTagRoute(ctx, next); },
-            (ctx, next) => { this.deleteTagRoute(ctx, next); });
+            (ctx, next) => { this._loadTagRoute(ctx, next); },
+            (ctx, next) => { this._deleteTagRoute(ctx, next); });
         page(
             '/tags/:query?',
             (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
-            (ctx, next) => { this.listTagsRoute(ctx, next); });
+            (ctx, next) => { this._listTagsRoute(ctx, next); });
     }
 
     _saveTagCategories(addedCategories, changedCategories, removedCategories) {
@@ -64,34 +64,35 @@ class TagsController {
             });
     }
 
-    loadTagRoute(ctx, next) {
+    _loadTagRoute(ctx, next) {
         if (ctx.state.tag) {
             next();
-        } else if (this.tag && this.tag.names == ctx.params.names) {
-            ctx.state.tag = this.tag;
+        } else if (this._cachedTag &&
+                this._cachedTag.names == ctx.params.names) {
+            ctx.state.tag = this._cachedTag;
             next();
         } else {
             api.get('/tag/' + ctx.params.name).then(response => {
                 ctx.state.tag = response.tag;
                 ctx.save();
-                this.tag = response.tag;
+                this._cachedTag = response.tag;
                 next();
             }, response => {
-                this.emptyView.render();
+                this._emptyView.render();
                 events.notify(events.Error, response.description);
             });
         }
     }
 
-    showTagRoute(ctx, next) {
+    _showTagRoute(ctx, next) {
         this._show(ctx.state.tag, 'summary');
     }
 
-    mergeTagRoute(ctx, next) {
+    _mergeTagRoute(ctx, next) {
         this._show(ctx.state.tag, 'merge');
     }
 
-    deleteTagRoute(ctx, next) {
+    _deleteTagRoute(ctx, next) {
         this._show(ctx.state.tag, 'delete');
     }
 
@@ -101,7 +102,7 @@ class TagsController {
         for (let category of tags.getAllCategories()) {
             categories[category.name] = category.name;
         }
-        this.tagView.render({
+        this._tagView.render({
             tag: tag,
             section: section,
             canEditNames: api.hasPrivilege('tags:edit:names'),
@@ -152,10 +153,10 @@ class TagsController {
         });
     }
 
-    tagCategoriesRoute(ctx, next) {
+    _tagCategoriesRoute(ctx, next) {
         topNavController.activate('tags');
         api.get('/tag-categories/').then(response => {
-            this.tagCategoriesView.render({
+            this._tagCategoriesView.render({
                 tagCategories: response.results,
                 canEditName: api.hasPrivilege('tagCategories:edit:name'),
                 canEditColor: api.hasPrivilege('tagCategories:edit:color'),
@@ -173,12 +174,12 @@ class TagsController {
                 }
             });
         }, response => {
-            this.emptyView.render();
+            this._emptyView.render();
             events.notify(events.Error, response.description);
         });
     }
 
-    listTagsRoute(ctx, next) {
+    _listTagsRoute(ctx, next) {
         topNavController.activate('tags');
 
         pageController.run({
@@ -192,8 +193,8 @@ class TagsController {
             clientUrl: '/tags/' + misc.formatSearchQuery({
                 text: ctx.searchQuery.text, page: '{page}'}),
             searchQuery: ctx.searchQuery,
-            headerRenderer: this.tagsHeaderView,
-            pageRenderer: this.tagsPageView,
+            headerRenderer: this._tagsHeaderView,
+            pageRenderer: this._tagsPageView,
             canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
         });
     }
diff --git a/client/js/controllers/top_nav_controller.js b/client/js/controllers/top_nav_controller.js
index cab1a4d..f030770 100644
--- a/client/js/controllers/top_nav_controller.js
+++ b/client/js/controllers/top_nav_controller.js
@@ -16,10 +16,10 @@ class NavigationItem {
 
 class TopNavController {
     constructor() {
-        this.topNavView = new TopNavView();
-        this.activeItem = null;
+        this._topNavView = new TopNavView();
+        this._activeItem = null;
 
-        this.items = {
+        this._items = {
             'home':     new NavigationItem('H', 'Home',     '/'),
             'posts':    new NavigationItem('P', 'Posts',    '/posts'),
             'upload':   new NavigationItem('U', 'Upload',   '/upload'),
@@ -36,11 +36,11 @@ class TopNavController {
         };
 
         const rerender = () => {
-            this.updateVisibility();
-            this.topNavView.render({
-                items: this.items,
-                activeItem: this.activeItem});
-            this.topNavView.activate(this.activeItem);
+            this._updateVisibility();
+            this._topNavView.render({
+                items: this._items,
+                activeItem: this._activeItem});
+            this._topNavView.activate(this._activeItem);
         };
 
         events.listen(
@@ -49,41 +49,41 @@ class TopNavController {
         rerender();
     }
 
-    updateVisibility() {
-        this.items.account.url =  '/user/' + api.userName;
-        this.items.account.imageUrl = api.user ? api.user.avatarUrl : null;
+    _updateVisibility() {
+        this._items.account.url =  '/user/' + api.userName;
+        this._items.account.imageUrl = api.user ? api.user.avatarUrl : null;
 
-        const b = Object.keys(this.items);
+        const b = Object.keys(this._items);
         for (let key of b) {
-            this.items[key].available = true;
+            this._items[key].available = true;
         }
         if (!api.hasPrivilege('posts:list')) {
-            this.items.posts.available = false;
+            this._items.posts.available = false;
         }
         if (!api.hasPrivilege('posts:create')) {
-            this.items.upload.available = false;
+            this._items.upload.available = false;
         }
         if (!api.hasPrivilege('comments:list')) {
-            this.items.comments.available = false;
+            this._items.comments.available = false;
         }
         if (!api.hasPrivilege('tags:list')) {
-            this.items.tags.available = false;
+            this._items.tags.available = false;
         }
         if (!api.hasPrivilege('users:list')) {
-            this.items.users.available = false;
+            this._items.users.available = false;
         }
         if (api.isLoggedIn()) {
-            this.items.register.available = false;
-            this.items.login.available = false;
+            this._items.register.available = false;
+            this._items.login.available = false;
         } else {
-            this.items.account.available = false;
-            this.items.logout.available = false;
+            this._items.account.available = false;
+            this._items.logout.available = false;
         }
     }
 
     activate(itemName) {
-        this.activeItem = itemName;
-        this.topNavView.activate(this.activeItem);
+        this._activeItem = itemName;
+        this._topNavView.activate(this._activeItem);
     }
 }
 
diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js
index 529f490..33c8620 100644
--- a/client/js/controllers/users_controller.js
+++ b/client/js/controllers/users_controller.js
@@ -26,42 +26,42 @@ const rankNames = {
 
 class UsersController {
     constructor() {
-        this.registrationView = new RegistrationView();
-        this.userView = new UserView();
-        this.usersHeaderView = new UsersHeaderView();
-        this.usersPageView = new UsersPageView();
-        this.emptyView = new EmptyView();
+        this._registrationView = new RegistrationView();
+        this._userView = new UserView();
+        this._usersHeaderView = new UsersHeaderView();
+        this._usersPageView = new UsersPageView();
+        this._emptyView = new EmptyView();
     }
 
     registerRoutes() {
-        page('/register', () => { this.createUserRoute(); });
+        page('/register', () => { this._createUserRoute(); });
         page(
             '/users/:query?',
             (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
-            (ctx, next) => { this.listUsersRoute(ctx, next); });
+            (ctx, next) => { this._listUsersRoute(ctx, next); });
         page(
             '/user/:name',
-            (ctx, next) => { this.loadUserRoute(ctx, next); },
-            (ctx, next) => { this.showUserRoute(ctx, next); });
+            (ctx, next) => { this._loadUserRoute(ctx, next); },
+            (ctx, next) => { this._showUserRoute(ctx, next); });
         page(
             '/user/:name/edit',
-            (ctx, next) => { this.loadUserRoute(ctx, next); },
-            (ctx, next) => { this.editUserRoute(ctx, next); });
+            (ctx, next) => { this._loadUserRoute(ctx, next); },
+            (ctx, next) => { this._editUserRoute(ctx, next); });
         page(
             '/user/:name/delete',
-            (ctx, next) => { this.loadUserRoute(ctx, next); },
-            (ctx, next) => { this.deleteUserRoute(ctx, next); });
+            (ctx, next) => { this._loadUserRoute(ctx, next); },
+            (ctx, next) => { this._deleteUserRoute(ctx, next); });
         page.exit(/\/users\/.*/, (ctx, next) => {
             pageController.stop();
             next();
         });
         page.exit(/\/user\/.*/, (ctx, next) => {
-            this.user = null;
+            this._cachedUser = null;
             next();
         });
     }
 
-    listUsersRoute(ctx, next) {
+    _listUsersRoute(ctx, next) {
         topNavController.activate('users');
 
         pageController.run({
@@ -75,48 +75,48 @@ class UsersController {
             clientUrl: '/users/' + misc.formatSearchQuery({
                 text: ctx.searchQuery.text, page: '{page}'}),
             searchQuery: ctx.searchQuery,
-            headerRenderer: this.usersHeaderView,
-            pageRenderer: this.usersPageView,
+            headerRenderer: this._usersHeaderView,
+            pageRenderer: this._usersPageView,
         });
     }
 
-    createUserRoute() {
+    _createUserRoute() {
         topNavController.activate('register');
-        this.registrationView.render({
+        this._registrationView.render({
             register: (...args) => {
                 return this._register(...args);
             }});
     }
 
-    loadUserRoute(ctx, next) {
+    _loadUserRoute(ctx, next) {
         if (ctx.state.user) {
             next();
-        } else if (this.user && this.user.name == ctx.params.name) {
-            ctx.state.user = this.user;
+        } else if (this._cachedUser && this._cachedUser == ctx.params.name) {
+            ctx.state.user = this._cachedUser;
             next();
         } else {
             api.get('/user/' + ctx.params.name).then(response => {
                 response.user.rankName = rankNames[response.user.rank];
                 ctx.state.user = response.user;
                 ctx.save();
-                this.user = response.user;
+                this._cachedUser = response.user;
                 next();
             }, response => {
-                this.emptyView.render();
+                this._emptyView.render();
                 events.notify(events.Error, response.description);
             });
         }
     }
 
-    showUserRoute(ctx, next) {
+    _showUserRoute(ctx, next) {
         this._show(ctx.state.user, 'summary');
     }
 
-    editUserRoute(ctx, next) {
+    _editUserRoute(ctx, next) {
         this._show(ctx.state.user, 'edit');
     }
 
-    deleteUserRoute(ctx, next) {
+    _deleteUserRoute(ctx, next) {
         this._show(ctx.state.user, 'delete');
     }
 
@@ -171,7 +171,7 @@ class UsersController {
         return new Promise((resolve, reject) => {
             api.put('/user/' + user.name, data, files)
                 .then(response => {
-                    this.user = response.user;
+                    this._cachedUser = response.user;
                     return isLoggedIn ?
                         api.login(
                             data.name || api.userName,
@@ -236,7 +236,7 @@ class UsersController {
         } else {
             topNavController.activate('users');
         }
-        this.userView.render({
+        this._userView.render({
             user: user,
             section: section,
             isLoggedIn: isLoggedIn,
diff --git a/client/js/controls/auto_complete_control.js b/client/js/controls/auto_complete_control.js
new file mode 100644
index 0000000..7532759
--- /dev/null
+++ b/client/js/controls/auto_complete_control.js
@@ -0,0 +1,297 @@
+'use strict';
+
+const lodash = require('lodash');
+const views = require('../util/views.js');
+
+const KEY_TAB = 9;
+const KEY_RETURN = 13;
+const KEY_DELETE = 46;
+const KEY_ESCAPE = 27;
+const KEY_UP = 38;
+const KEY_DOWN = 40;
+
+function _getSelectionStart(input) {
+    if ('selectionStart' in input) {
+        return input.selectionStart;
+    }
+    if (document.selection) {
+        input.focus();
+        const sel = document.selection.createRange();
+        const selLen = document.selection.createRange().text.length;
+        sel.moveStart('character', -input.value.length);
+        return sel.text.length - selLen;
+    }
+    return 0;
+}
+
+class AutoCompleteControl {
+    constructor(sourceInputNode, options) {
+        this._sourceInputNode = sourceInputNode;
+        this._options = lodash.extend({}, {
+            verticalShift: 2,
+            source: null,
+            maxResults: 15,
+            getTextToFind: () => {
+                const value = sourceInputNode.value;
+                const start = _getSelectionStart(sourceInputNode);
+                return value.substring(0, start).replace(/.*\s+/, '');
+            },
+            confirm: text => {
+                const start = _getSelectionStart(sourceInputNode);
+                let prefix = '';
+                let suffix = sourceInputNode.value.substring(start);
+                let middle = sourceInputNode.value.substring(0, start);
+                const index = middle.lastIndexOf(' ');
+                if (index !== -1) {
+                    prefix = sourceInputNode.value.substring(0, index + 1);
+                    middle = sourceInputNode.value.substring(index + 1);
+                }
+                sourceInputNode.value = prefix +
+                    this._results[this._activeResult].value +
+                    ' ' +
+                    suffix.trimLeft();
+                sourceInputNode.focus();
+            },
+            delete: text => {
+            },
+            getMatches: null,
+        }, options);
+
+        this._showTimeout = null;
+        this._results = [];
+        this._activeResult = -1;
+
+        this._mutationObserver = new MutationObserver(
+            mutations => {
+                for (let mutation of mutations) {
+                    for (let node of mutation.removedNodes) {
+                        if (node.contains(this._sourceInputNode)) {
+                            this._uninstall();
+                            return;
+                        }
+                    }
+                }
+            });
+
+        this._install();
+    }
+
+    hide() {
+        window.clearTimeout(this._showTimeout);
+        this._suggestionDiv.style.display = 'none';
+        this._isVisible = false;
+    }
+
+    _show() {
+        this._suggestionDiv.style.display = 'block';
+        this._isVisible = true;
+    }
+
+    _showOrHide() {
+        const textToFind = this._options.getTextToFind();
+        if (!textToFind || !textToFind.length) {
+            this.hide();
+        } else {
+            this._updateResults(textToFind);
+            this._refreshList();
+        }
+    }
+
+    _install() {
+        if (!this._sourceInputNode) {
+            throw new Error('Input element was not found');
+        }
+        if (this._sourceInputNode.getAttribute('data-autocomplete')) {
+            throw new Error(
+                'Autocompletion was already added for this element');
+        }
+        this._sourceInputNode.setAttribute('data-autocomplete', true);
+        this._sourceInputNode.setAttribute('autocomplete', 'off');
+
+        this._mutationObserver.observe(
+            document.body, {childList: true, subtree: true});
+        this._sourceInputNode.addEventListener(
+            'keydown', e => this._evtKeyDown(e));
+        this._sourceInputNode.addEventListener(
+            'blur', e => this._evtBlur(e));
+
+        this._suggestionDiv = views.htmlToDom(
+            '<div class="autocomplete"><ul></ul></div>');
+        this._suggestionList = this._suggestionDiv.querySelector('ul');
+        document.body.appendChild(this._suggestionDiv);
+    }
+
+    _uninstall() {
+        window.clearTimeout(this._showTimeout);
+        this._mutationObserver.disconnect();
+        document.body.removeChild(this._suggestionDiv);
+    }
+
+    _evtKeyDown(e) {
+        const key = e.which;
+        const shift = e.shiftKey;
+        let func = null;
+        if (this._isVisible) {
+            if (key === KEY_ESCAPE) {
+                func = this.hide;
+            } else if (key === KEY_TAB && shift) {
+                func = () => { this._selectPrevious(); };
+            } else if (key === KEY_TAB && !shift) {
+                func = () => { this._selectNext(); };
+            } else if (key === KEY_UP) {
+                func = () => { this._selectPrevious(); };
+            } else if (key === KEY_DOWN) {
+                func = () => { this._selectNext(); };
+            } else if (key === KEY_RETURN && this._activeResult >= 0) {
+                func = () => {
+                    this._options.confirm(this._getActiveSuggestion());
+                    this.hide();
+                };
+            } else if (key === KEY_DELETE && this._activeResult >= 0) {
+                func = () => {
+                    this._options.delete(this._getActiveSuggestion());
+                    this.hide();
+                };
+            }
+        }
+
+        if (func !== null) {
+            e.preventDefault();
+            e.stopPropagation();
+            e.stopImmediatePropagation();
+            func();
+        } else {
+            window.clearTimeout(this._showTimeout);
+            this._showTimeout = window.setTimeout(
+                () => { this._showOrHide(); }, 250);
+        }
+    }
+
+    _evtBlur(e) {
+        window.clearTimeout(this._showTimeout);
+        window.setTimeout(() => { this.hide(); }, 50);
+    }
+
+    _getActiveSuggestion() {
+        if (this._activeResult === -1) {
+            return null;
+        }
+        return this._results[this._activeResult].value;
+    }
+
+    _selectPrevious() {
+        this._select(this._activeResult === -1 ?
+            this._results.length - 1 :
+            this._activeResult - 1);
+    }
+
+    _selectNext() {
+        this._select(this._activeResult === -1 ? 0 : this._activeResult + 1);
+    }
+
+    _select(newActiveResult) {
+        this._activeResult =
+            newActiveResult.between(0, this._results.length - 1, true) ?
+                newActiveResult :
+                -1;
+        this._refreshActiveResult();
+    }
+
+    _updateResults(textToFind) {
+        const oldResults = this._results.slice();
+        this._results =
+            this._options.getMatches(textToFind)
+            .slice(0, this._options.maxResults);
+        if (!lodash.isEqual(oldResults, this._results)) {
+            this._activeResult = -1;
+        }
+    }
+
+    _refreshList() {
+        if (this._results.length === 0) {
+            this.hide();
+            return;
+        }
+
+        while (this._suggestionList.firstChild) {
+            this._suggestionList.removeChild(this._suggestionList.firstChild);
+        }
+        lodash.each(
+            this._results,
+            (resultItem, resultIndex) => {
+                const listItem = document.createElement('li');
+                const link = document.createElement('a');
+                link.href = '#';
+                link.innerHTML = resultItem.caption;
+                link.setAttribute('data-key', resultItem.value);
+                link.addEventListener(
+                    'mouseenter',
+                    e => {
+                        e.preventDefault();
+                        this._activeResult = resultIndex;
+                        this._refreshActiveResult();
+                    });
+                link.addEventListener(
+                    'mousedown',
+                    e => {
+                        e.preventDefault();
+                        this._activeResult = resultIndex;
+                        this._options.confirm(this._getActiveSuggestion());
+                        this.hide();
+                    });
+                listItem.appendChild(link);
+                this._suggestionList.appendChild(listItem);
+            });
+        this._refreshActiveResult();
+
+        // display the suggestions offscreen to get the height
+        this._suggestionDiv.style.left = '-9999px';
+        this._suggestionDiv.style.top = '-9999px';
+        this._show();
+        const verticalShift = this._options.verticalShift;
+        const inputRect = this._sourceInputNode.getBoundingClientRect();
+        const bodyRect = document.body.getBoundingClientRect();
+        const viewPortHeight = bodyRect.bottom - bodyRect.top;
+        let listRect = this._suggestionDiv.getBoundingClientRect();
+
+        // choose where to view the suggestions: if there's more space above
+        // the input - draw the suggestions above it, otherwise below
+        const direction =
+            inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
+
+        let x = inputRect.left - bodyRect.left;
+        let y = direction == 1 ?
+            inputRect.bottom - bodyRect.top - verticalShift :
+            inputRect.top - bodyRect.top - listRect.height + verticalShift;
+
+        // remove offscreen items until whole suggestion list can fit on the
+        // screen
+        while ((y < 0 || y + listRect.height > viewPortHeight) &&
+                this._suggestionList.childNodes.length) {
+            this._suggestionList.removeChild(this._suggestionList.lastChild);
+            const prevHeight = listRect.height;
+            listRect = this._suggestionDiv.getBoundingClientRect();
+            const heightDelta = prevHeight - listRect.height;
+            if (direction == -1) {
+                y += heightDelta;
+            }
+        }
+
+        this._suggestionDiv.style.left = x + 'px';
+        this._suggestionDiv.style.top = y + 'px';
+    }
+
+    _refreshActiveResult() {
+        let activeItem = this._suggestionList.querySelector('li.active');
+        if (activeItem) {
+            activeItem.classList.remove('active');
+        }
+        if (this._activeResult >= 0) {
+            const allItems = this._suggestionList.querySelectorAll('li');
+            activeItem = allItems[this._activeResult];
+            activeItem.classList.add('active');
+        }
+    }
+};
+
+module.exports = AutoCompleteControl;
diff --git a/client/js/controls/file_dropper_control.js b/client/js/controls/file_dropper_control.js
new file mode 100644
index 0000000..c5b5360
--- /dev/null
+++ b/client/js/controls/file_dropper_control.js
@@ -0,0 +1,76 @@
+'use strict';
+
+const views = require('../util/views.js');
+
+class FileDropperControl {
+    constructor(target, options) {
+        this._options = options;
+        this._template = views.getTemplate('file-dropper');
+        const source = this._template({
+            allowMultiple: this._options.allowMultiple,
+            id: 'file-' + Math.random().toString(36).substring(7),
+        });
+
+        this._dropperNode = source.querySelector('.file-dropper');
+        this._fileInputNode = source.querySelector('input');
+        this._fileInputNode.style.display = 'none';
+        this._fileInputNode.multiple = this._options._allowMultiple || false;
+
+        this._counter = 0;
+        this._dropperNode.addEventListener(
+            'dragenter', e => this._evtDragEnter(e));
+        this._dropperNode.addEventListener(
+            'dragleave', e => this._evtDragLeave(e));
+        this._dropperNode.addEventListener(
+            'dragover', e => this._evtDragOver(e));
+        this._dropperNode.addEventListener(
+            'drop', e => this._evtDrop(e));
+        this._fileInputNode.addEventListener(
+            'change', e => this._evtFileChange(e));
+
+        views.showView(target, source);
+    }
+
+    _resolve(files) {
+        files = Array.from(files);
+        if (this._options.lock) {
+            this._dropperNode.innerText =
+                files.map(file => file.name).join(', ');
+        }
+        this._options.resolve(files);
+    };
+
+    _evtFileChange(e) {
+        this._resolve(e.target.files);
+    }
+
+    _evtDragEnter(e) {
+        this._dropperNode.classList.add('active');
+        counter++;
+    }
+
+    _evtDragLeave(e) {
+        this._counter--;
+        if (this._counter === 0) {
+            this._dropperNode.classList.remove('active');
+        }
+    }
+
+    _evtDragOver(e) {
+        e.preventDefault();
+    }
+
+    _evtDrop(e) {
+        e.preventDefault();
+        this._dropperNode.classList.remove('active');
+        if (!e.dataTransfer.files.length) {
+            window.alert('Only files are supported.');
+        }
+        if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) {
+            window.alert('Cannot select multiple files.');
+        }
+        this._resolve(e.dataTransfer.files);
+    }
+}
+
+module.exports = FileDropperControl;
diff --git a/client/js/views/tag_auto_complete_control.js b/client/js/controls/tag_auto_complete_control.js
similarity index 100%
rename from client/js/views/tag_auto_complete_control.js
rename to client/js/controls/tag_auto_complete_control.js
diff --git a/client/js/views/tag_input_control.js b/client/js/controls/tag_input_control.js
similarity index 97%
rename from client/js/views/tag_input_control.js
rename to client/js/controls/tag_input_control.js
index f608efa..2412879 100644
--- a/client/js/views/tag_input_control.js
+++ b/client/js/controls/tag_input_control.js
@@ -20,9 +20,12 @@ class TagInputControl {
         this.readOnly = sourceInputNode.readOnly;
 
         this._autoCompleteControls = [];
-
         this._sourceInputNode = sourceInputNode;
 
+        this._install();
+    }
+
+    _install() {
         // set up main edit area
         this._editAreaNode = views.htmlToDom('<div class="tag-input"></div>');
         this._editAreaNode.autocorrect = false;
@@ -43,12 +46,12 @@ class TagInputControl {
         this._editAreaNode.appendChild(this._tailWrapperNode);
 
         // add existing tags
-        this.addMultipleTags(sourceInputNode.value);
+        this.addMultipleTags(this._sourceInputNode.value);
 
         // show
-        sourceInputNode.style.display = 'none';
-        sourceInputNode.parentNode.insertBefore(
-            this._editAreaNode, sourceInputNode.nextSibling);
+        this._sourceInputNode.style.display = 'none';
+        this._sourceInputNode.parentNode.insertBefore(
+            this._editAreaNode, this._sourceInputNode.nextSibling);
     }
 
     addMultipleTags(text, sourceNode) {
diff --git a/client/js/views/auto_complete_control.js b/client/js/views/auto_complete_control.js
deleted file mode 100644
index 5300412..0000000
--- a/client/js/views/auto_complete_control.js
+++ /dev/null
@@ -1,298 +0,0 @@
-'use strict';
-
-const lodash = require('lodash');
-const views = require('../util/views.js');
-
-const KEY_TAB = 9;
-const KEY_RETURN = 13;
-const KEY_DELETE = 46;
-const KEY_ESCAPE = 27;
-const KEY_UP = 38;
-const KEY_DOWN = 40;
-
-function getSelectionStart(input) {
-    if ('selectionStart' in input) {
-        return input.selectionStart;
-    }
-    if (document.selection) {
-        input.focus();
-        const sel = document.selection.createRange();
-        const selLen = document.selection.createRange().text.length;
-        sel.moveStart('character', -input.value.length);
-        return sel.text.length - selLen;
-    }
-    return 0;
-}
-
-class AutoCompleteControl {
-    constructor(input, options) {
-        this.input = input;
-        this.options = lodash.extend({}, {
-            verticalShift: 2,
-            source: null,
-            maxResults: 15,
-            getTextToFind: () => {
-                const value = this.input.value;
-                const start = getSelectionStart(this.input);
-                return value.substring(0, start).replace(/.*\s+/, '');
-            },
-            confirm: text => {
-                const start = getSelectionStart(this.input);
-                let prefix = '';
-                let suffix = this.input.value.substring(start);
-                let middle = this.input.value.substring(0, start);
-                const index = middle.lastIndexOf(' ');
-                if (index !== -1) {
-                    prefix = this.input.value.substring(0, index + 1);
-                    middle = this.input.value.substring(index + 1);
-                }
-                this.input.value = prefix +
-                    this.results[this.activeResult].value +
-                    ' ' +
-                    suffix.trimLeft();
-                this.input.focus();
-            },
-            delete: text => {
-            },
-            getMatches: null,
-        }, options);
-
-        this.showTimeout = null;
-        this.results = [];
-        this.activeResult = -1;
-
-        this.mutationObserver = new MutationObserver(
-            mutations => {
-                for (let mutation of mutations) {
-                    for (let node of mutation.removedNodes) {
-                        if (node.contains(input)) {
-                            this.uninstall();
-                            return;
-                        }
-                    }
-                }
-            });
-
-        this.install();
-    }
-
-    uninstall() {
-        window.clearTimeout(this.showTimeout);
-        this.mutationObserver.disconnect();
-        document.body.removeChild(this.suggestionDiv);
-    }
-
-    install() {
-        if (!this.input) {
-            throw new Error('Input element was not found');
-        }
-        if (this.input.getAttribute('data-autocomplete')) {
-            throw new Error(
-                'Autocompletion was already added for this element');
-        }
-        this.input.setAttribute('data-autocomplete', true);
-        this.input.setAttribute('autocomplete', 'off');
-
-        this.mutationObserver.observe(
-            document.body, {childList: true, subtree: true});
-
-        this.input.addEventListener(
-            'keydown',
-            e => {
-                const key = e.which;
-                const shift = e.shiftKey;
-                let func = null;
-                if (this.isVisible) {
-                    if (key === KEY_ESCAPE) {
-                        func = this.hide;
-                    } else if (key === KEY_TAB && shift) {
-                        func = () => { this.selectPrevious(); };
-                    } else if (key === KEY_TAB && !shift) {
-                        func = () => { this.selectNext(); };
-                    } else if (key === KEY_UP) {
-                        func = () => { this.selectPrevious(); };
-                    } else if (key === KEY_DOWN) {
-                        func = () => { this.selectNext(); };
-                    } else if (key === KEY_RETURN && this.activeResult >= 0) {
-                        func = () => {
-                            this.options.confirm(this.getActiveSuggestion());
-                            this.hide();
-                        };
-                    } else if (key === KEY_DELETE && this.activeResult >= 0) {
-                        func = () => {
-                            this.options.delete(this.getActiveSuggestion());
-                            this.hide();
-                        };
-                    }
-                }
-
-                if (func !== null) {
-                    e.preventDefault();
-                    e.stopPropagation();
-                    e.stopImmediatePropagation();
-                    func();
-                } else {
-                    window.clearTimeout(this.showTimeout);
-                    this.showTimeout = window.setTimeout(
-                        () => { this.showOrHide(); },
-                        250);
-                }
-            });
-
-        this.input.addEventListener(
-            'blur',
-            e => {
-                window.clearTimeout(this.showTimeout);
-                window.setTimeout(() => { this.hide(); }, 50);
-            });
-
-        this.suggestionDiv = views.htmlToDom(
-            '<div class="autocomplete"><ul></ul></div>');
-        this.suggestionList = this.suggestionDiv.querySelector('ul');
-        document.body.appendChild(this.suggestionDiv);
-    }
-
-    getActiveSuggestion() {
-        if (this.activeResult === -1) {
-            return null;
-        }
-        return this.results[this.activeResult].value;
-    }
-
-    showOrHide() {
-        const textToFind = this.options.getTextToFind();
-        if (!textToFind || !textToFind.length) {
-            this.hide();
-        } else {
-            this.updateResults(textToFind);
-            this.refreshList();
-        }
-    }
-
-    show() {
-        this.suggestionDiv.style.display = 'block';
-        this.isVisible = true;
-    }
-
-    hide() {
-        window.clearTimeout(this.showTimeout);
-        this.suggestionDiv.style.display = 'none';
-        this.isVisible = false;
-    }
-
-    selectPrevious() {
-        this.select(this.activeResult === -1 ?
-            this.results.length - 1 :
-            this.activeResult - 1);
-    }
-
-    selectNext() {
-        this.select(this.activeResult === -1 ? 0 : this.activeResult + 1);
-    }
-
-    select(newActiveResult) {
-        this.activeResult =
-            newActiveResult.between(0, this.results.length - 1, true) ?
-                newActiveResult :
-                -1;
-        this.refreshActiveResult();
-    }
-
-    updateResults(textToFind) {
-        const oldResults = this.results.slice();
-        this.results =
-            this.options.getMatches(textToFind)
-            .slice(0, this.options.maxResults);
-        if (!lodash.isEqual(oldResults, this.results)) {
-            this.activeResult = -1;
-        }
-    }
-
-    refreshList() {
-        if (this.results.length === 0) {
-            this.hide();
-            return;
-        }
-
-        while (this.suggestionList.firstChild) {
-            this.suggestionList.removeChild(this.suggestionList.firstChild);
-        }
-        lodash.each(
-            this.results,
-            (resultItem, resultIndex) => {
-                const listItem = document.createElement('li');
-                const link = document.createElement('a');
-                link.href = '#';
-                link.innerHTML = resultItem.caption;
-                link.setAttribute('data-key', resultItem.value);
-                link.addEventListener(
-                    'mouseenter',
-                    e => {
-                        e.preventDefault();
-                        this.activeResult = resultIndex;
-                        this.refreshActiveResult();
-                    });
-                link.addEventListener(
-                    'mousedown',
-                    e => {
-                        e.preventDefault();
-                        this.activeResult = resultIndex;
-                        this.options.confirm(this.getActiveSuggestion());
-                        this.hide();
-                    });
-                listItem.appendChild(link);
-                this.suggestionList.appendChild(listItem);
-            });
-        this.refreshActiveResult();
-
-        // display the suggestions offscreen to get the height
-        this.suggestionDiv.style.left = '-9999px';
-        this.suggestionDiv.style.top = '-9999px';
-        this.show();
-        const verticalShift = this.options.verticalShift;
-        const inputRect = this.input.getBoundingClientRect();
-        const bodyRect = document.body.getBoundingClientRect();
-        const viewPortHeight = bodyRect.bottom - bodyRect.top;
-        let listRect = this.suggestionDiv.getBoundingClientRect();
-
-        // choose where to view the suggestions: if there's more space above
-        // the input - draw the suggestions above it, otherwise below
-        const direction =
-            inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
-
-        let x = inputRect.left - bodyRect.left;
-        let y = direction == 1 ?
-            inputRect.bottom - bodyRect.top - verticalShift :
-            inputRect.top - bodyRect.top - listRect.height + verticalShift;
-
-        // remove offscreen items until whole suggestion list can fit on the
-        // screen
-        while ((y < 0 || y + listRect.height > viewPortHeight) &&
-                this.suggestionList.childNodes.length) {
-            this.suggestionList.removeChild(this.suggestionList.lastChild);
-            const prevHeight = listRect.height;
-            listRect = this.suggestionDiv.getBoundingClientRect();
-            const heightDelta = prevHeight - listRect.height;
-            if (direction == -1) {
-                y += heightDelta;
-            }
-        }
-
-        this.suggestionDiv.style.left = x + 'px';
-        this.suggestionDiv.style.top = y + 'px';
-    }
-
-    refreshActiveResult() {
-        let activeItem = this.suggestionList.querySelector('li.active');
-        if (activeItem) {
-            activeItem.classList.remove('active');
-        }
-        if (this.activeResult >= 0) {
-            const allItems = this.suggestionList.querySelectorAll('li');
-            activeItem = allItems[this.activeResult];
-            activeItem.classList.add('active');
-        }
-    }
-};
-
-module.exports = AutoCompleteControl;
diff --git a/client/js/views/empty_view.js b/client/js/views/empty_view.js
index 48c06fd..3926865 100644
--- a/client/js/views/empty_view.js
+++ b/client/js/views/empty_view.js
@@ -4,7 +4,7 @@ const views = require('../util/views.js');
 
 class EmptyView {
     constructor() {
-        this.template = () => {
+        this._template = () => {
             return views.htmlToDom(
                 '<div class="wrapper"><div class="messages"></div></div>');
         };
@@ -12,7 +12,7 @@ class EmptyView {
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template();
+        const source = this._template();
         views.listenToMessages(source);
         views.showView(target, source);
     }
diff --git a/client/js/views/endless_page_view.js b/client/js/views/endless_page_view.js
index 784d58c..c4e3646 100644
--- a/client/js/views/endless_page_view.js
+++ b/client/js/views/endless_page_view.js
@@ -6,19 +6,19 @@ const views = require('../util/views.js');
 
 class EndlessPageView {
     constructor() {
-        this.holderTemplate = views.getTemplate('endless-pager');
-        this.pageTemplate = views.getTemplate('endless-pager-page');
+        this._holderTemplate = views.getTemplate('endless-pager');
+        this._pageTemplate = views.getTemplate('endless-pager-page');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.holderTemplate();
+        const source = this._holderTemplate();
         const pageHeaderHolder = source.querySelector('.page-header-holder');
         const pagesHolder = source.querySelector('.pages-holder');
         views.listenToMessages(source);
         views.showView(target, source);
-        this.active = true;
-        this.working = 0;
+        this._active = true;
+        this._working = 0;
 
         let headerRendererCtx = ctx;
         headerRendererCtx.target = pageHeaderHolder;
@@ -31,8 +31,8 @@ class EndlessPageView {
         this.totalPages = null;
         this.currentPage = null;
 
-        this.updater = () => {
-            if (this.working) {
+        this._updater = () => {
+            if (this._working) {
                 return;
             }
 
@@ -66,46 +66,46 @@ class EndlessPageView {
                 document.documentElement.clientHeight;
 
             if (this.minPageShown > 1 && window.scrollY - threshold < 0) {
-                this.loadPage(pagesHolder, ctx, this.minPageShown - 1, false)
-                    .then(() => this.updater());
+                this._loadPage(pagesHolder, ctx, this.minPageShown - 1, false)
+                    .then(() => this._updater());
             } else if (this.maxPageShown < this.totalPages &&
                     window.scrollY + threshold > scrollHeight) {
-                this.loadPage(pagesHolder, ctx, this.maxPageShown + 1, true)
-                    .then(() => this.updater());
+                this._loadPage(pagesHolder, ctx, this.maxPageShown + 1, true)
+                    .then(() => this._updater());
             }
         };
 
-        this.loadPage(pagesHolder, ctx, ctx.searchQuery.page, true)
+        this._loadPage(pagesHolder, ctx, ctx.searchQuery.page, true)
             .then(pageNode => {
                 if (ctx.searchQuery.page > 1) {
                     window.scroll(0, pageNode.getBoundingClientRect().top);
                 }
-                this.updater();
+                this._updater();
             });
-        window.addEventListener('scroll', this.updater, true);
-        window.addEventListener('unload', this.scrollToTop, true);
+        window.addEventListener('scroll', this._updater, true);
+        window.addEventListener('unload', this._scrollToTop, true);
     }
 
     unrender() {
-        this.active = false;
-        window.removeEventListener('scroll', this.updater, true);
-        window.removeEventListener('unload', this.scrollToTop, true);
+        this._active = false;
+        window.removeEventListener('scroll', this._updater, true);
+        window.removeEventListener('unload', this._scrollToTop, true);
     }
 
-    scrollToTop() {
+    _scrollToTop() {
         window.scroll(0, 0);
     }
 
-    loadPage(pagesHolder, ctx, pageNumber, append) {
-        this.working++;
+    _loadPage(pagesHolder, ctx, pageNumber, append) {
+        this._working++;
         return ctx.requestPage(pageNumber).then(response => {
-            if (!this.active) {
-                this.working--;
+            if (!this._active) {
+                this._working--;
                 return Promise.reject();
             }
             this.totalPages = Math.ceil(response.total / response.pageSize);
             if (response.total) {
-                const pageNode = this.pageTemplate({
+                const pageNode = this._pageTemplate({
                     page: pageNumber,
                     totalPages: this.totalPages,
                 });
@@ -137,17 +137,17 @@ class EndlessPageView {
                         window.scrollX,
                         window.scrollY + pageNode.offsetHeight);
                 }
-                this.working--;
+                this._working--;
                 return Promise.resolve(pageNode);
             }
             if (response.total <= (pageNumber - 1) * response.pageSize) {
                 events.notify(events.Info, 'No data to show');
             }
-            this.working--;
+            this._working--;
             return Promise.reject();
         }, response => {
             events.notify(events.Error, response.description);
-            this.working--;
+            this._working--;
             return Promise.reject();
         });
     }
diff --git a/client/js/views/file_dropper_control.js b/client/js/views/file_dropper_control.js
deleted file mode 100644
index 45b753a..0000000
--- a/client/js/views/file_dropper_control.js
+++ /dev/null
@@ -1,67 +0,0 @@
-'use strict';
-
-const views = require('../util/views.js');
-
-class FileDropperControl {
-    constructor() {
-        this.template = views.getTemplate('file-dropper');
-    }
-
-    render(ctx) {
-        const target = ctx.target;
-        const source = this.template({
-            allowMultiple: ctx.allowMultiple,
-            id: 'file-' + Math.random().toString(36).substring(7),
-        });
-
-        const dropper = source.querySelector('.file-dropper');
-        const fileInput = source.querySelector('input');
-        fileInput.style.display = 'none';
-        fileInput.multiple = ctx.allowMultiple || false;
-
-        const resolve = files => {
-            files = Array.from(files);
-            if (ctx.lock) {
-                dropper.innerText = files.map(file => file.name).join(', ');
-            }
-            ctx.resolve(files);
-        };
-
-        let counter = 0;
-        dropper.addEventListener('dragenter', e => {
-            dropper.classList.add('active');
-            counter++;
-        });
-
-        dropper.addEventListener('dragleave', e => {
-            counter--;
-            if (counter === 0) {
-                dropper.classList.remove('active');
-            }
-        });
-
-        dropper.addEventListener('dragover', e => {
-            e.preventDefault();
-        });
-
-        dropper.addEventListener('drop', e => {
-            dropper.classList.remove('active');
-            e.preventDefault();
-            if (!e.dataTransfer.files.length) {
-                window.alert('Only files are supported.');
-            }
-            if (!ctx.allowMultiple && e.dataTransfer.files.length > 1) {
-                window.alert('Cannot select multiple files.');
-            }
-            resolve(e.dataTransfer.files);
-        });
-
-        fileInput.addEventListener('change', e => {
-            resolve(e.target.files);
-        });
-
-        views.showView(target, source);
-    }
-}
-
-module.exports = FileDropperControl;
diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js
index b4ff278..ffa4afd 100644
--- a/client/js/views/help_view.js
+++ b/client/js/views/help_view.js
@@ -5,14 +5,14 @@ const views = require('../util/views.js');
 
 class HelpView {
     constructor() {
-        this.template = views.getTemplate('help');
-        this.sectionTemplates = {};
+        this._template = views.getTemplate('help');
+        this._sectionTemplates = {};
         const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos'];
         for (let section of sectionKeys) {
             const templateName = 'help-' + section;
-            this.sectionTemplates[section] = views.getTemplate(templateName);
+            this._sectionTemplates[section] = views.getTemplate(templateName);
         }
-        this.subsectionTemplates = {
+        this._subsectionTemplates = {
             'search': {
                 'default': views.getTemplate('help-search-general'),
                 'posts': views.getTemplate('help-search-posts'),
@@ -24,23 +24,23 @@ class HelpView {
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template();
+        const source = this._template();
 
         ctx.section = ctx.section || 'about';
-        if (ctx.section in this.sectionTemplates) {
+        if (ctx.section in this._sectionTemplates) {
             views.showView(
                 source.querySelector('.content'),
-                this.sectionTemplates[ctx.section]({
+                this._sectionTemplates[ctx.section]({
                     name: config.name,
                 }));
         }
 
         ctx.subsection = ctx.subsection || 'default';
-        if (ctx.section in this.subsectionTemplates &&
-                ctx.subsection in this.subsectionTemplates[ctx.section]) {
+        if (ctx.section in this._subsectionTemplates &&
+                ctx.subsection in this._subsectionTemplates[ctx.section]) {
             views.showView(
                 source.querySelector('.subcontent'),
-                this.subsectionTemplates[ctx.section][ctx.subsection]({
+                this._subsectionTemplates[ctx.section][ctx.subsection]({
                     name: config.name,
                 }));
         }
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index c0d5df1..cb19b19 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -5,12 +5,12 @@ const views = require('../util/views.js');
 
 class HomeView {
     constructor() {
-        this.template = views.getTemplate('home');
+        this._template = views.getTemplate('home');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template({
+        const source = this._template({
             name: config.name,
             version: config.meta.version,
             buildDate: config.meta.buildDate,
diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js
index 112cdab..ab07093 100644
--- a/client/js/views/login_view.js
+++ b/client/js/views/login_view.js
@@ -5,12 +5,12 @@ const views = require('../util/views.js');
 
 class LoginView {
     constructor() {
-        this.template = views.getTemplate('login');
+        this._template = views.getTemplate('login');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template({
+        const source = this._template({
             userNamePattern: config.userNameRegex,
             passwordPattern: config.passwordRegex,
             canSendMails: config.canSendMails,
diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js
index 53f0c4b..9e176f8 100644
--- a/client/js/views/manual_page_view.js
+++ b/client/js/views/manual_page_view.js
@@ -6,13 +6,13 @@ const keyboard = require('../util/keyboard.js');
 const misc = require('../util/misc.js');
 const views = require('../util/views.js');
 
-function removeConsecutiveDuplicates(a) {
+function _removeConsecutiveDuplicates(a) {
     return a.filter((item, pos, ary) => {
         return !pos || item != ary[pos - 1];
     });
 }
 
-function getVisiblePageNumbers(currentPage, totalPages) {
+function _getVisiblePageNumbers(currentPage, totalPages) {
     const threshold = 2;
     let pagesVisible = [];
     for (let i = 1; i <= threshold; i++) {
@@ -30,11 +30,11 @@ function getVisiblePageNumbers(currentPage, totalPages) {
         return item >= 1 && item <= totalPages;
     });
     pagesVisible = pagesVisible.sort((a, b) => { return a - b; });
-    pagesVisible = removeConsecutiveDuplicates(pagesVisible);
+    pagesVisible = _removeConsecutiveDuplicates(pagesVisible);
     return pagesVisible;
 }
 
-function getPages(currentPage, pageNumbers, clientUrl) {
+function _getPages(currentPage, pageNumbers, clientUrl) {
     const pages = [];
     let lastPage = 0;
     for (let page of pageNumbers) {
@@ -53,13 +53,13 @@ function getPages(currentPage, pageNumbers, clientUrl) {
 
 class ManualPageView {
     constructor() {
-        this.holderTemplate = views.getTemplate('manual-pager');
-        this.navTemplate = views.getTemplate('manual-pager-nav');
+        this._holderTemplate = views.getTemplate('manual-pager');
+        this._navTemplate = views.getTemplate('manual-pager-nav');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.holderTemplate();
+        const source = this._holderTemplate();
         const pageContentHolder = source.querySelector('.page-content-holder');
         const pageHeaderHolder = source.querySelector('.page-header-holder');
         const pageNav = source.querySelector('.page-nav');
@@ -75,8 +75,8 @@ class ManualPageView {
             ctx.pageRenderer.render(pageRendererCtx);
 
             const totalPages = Math.ceil(response.total / response.pageSize);
-            const pageNumbers = getVisiblePageNumbers(currentPage, totalPages);
-            const pages = getPages(currentPage, pageNumbers, ctx.clientUrl);
+            const pageNumbers = _getVisiblePageNumbers(currentPage, totalPages);
+            const pages = _getPages(currentPage, pageNumbers, ctx.clientUrl);
 
             keyboard.bind(['a', 'left'], () => {
                 if (currentPage > 1) {
@@ -90,7 +90,7 @@ class ManualPageView {
             });
 
             if (response.total) {
-                views.showView(pageNav, this.navTemplate({
+                views.showView(pageNav, this._navTemplate({
                     prevLink: ctx.clientUrl.format({page: currentPage - 1}),
                     nextLink: ctx.clientUrl.format({page: currentPage + 1}),
                     prevLinkActive: currentPage > 1,
diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js
index 67043c5..0d67a19 100644
--- a/client/js/views/password_reset_view.js
+++ b/client/js/views/password_reset_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class PasswordResetView {
     constructor() {
-        this.template = views.getTemplate('password-reset');
+        this._template = views.getTemplate('password-reset');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template();
+        const source = this._template();
 
         const form = source.querySelector('form');
         const userNameOrEmailField = source.querySelector('#user-name');
diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js
index bcd8c74..96e0681 100644
--- a/client/js/views/registration_view.js
+++ b/client/js/views/registration_view.js
@@ -5,7 +5,7 @@ const views = require('../util/views.js');
 
 class RegistrationView {
     constructor() {
-        this.template = views.getTemplate('user-registration');
+        this._template = views.getTemplate('user-registration');
     }
 
     render(ctx) {
@@ -13,7 +13,7 @@ class RegistrationView {
         ctx.passwordPattern = config.passwordRegex;
 
         const target = document.getElementById('content-holder');
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const userNameField = source.querySelector('#user-name');
diff --git a/client/js/views/settings_view.js b/client/js/views/settings_view.js
index 6809ee8..c5487b1 100644
--- a/client/js/views/settings_view.js
+++ b/client/js/views/settings_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class SettingsView {
     constructor() {
-        this.template = views.getTemplate('settings');
+        this._template = views.getTemplate('settings');
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template({browsingSettings: ctx.getSettings()});
+        const source = this._template({browsingSettings: ctx.getSettings()});
 
         const form = source.querySelector('form');
         views.decorateValidator(form);
diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js
index 7dd7115..30e101e 100644
--- a/client/js/views/tag_categories_view.js
+++ b/client/js/views/tag_categories_view.js
@@ -5,7 +5,7 @@ const views = require('../util/views.js');
 
 class TagListHeaderView {
     constructor() {
-        this.template = views.getTemplate('tag-categories');
+        this._template = views.getTemplate('tag-categories');
     }
 
     _saveButtonClickHandler(e, ctx, target) {
@@ -73,7 +73,7 @@ class TagListHeaderView {
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const newRowTemplate = source.querySelector('.add-template');
diff --git a/client/js/views/tag_delete_view.js b/client/js/views/tag_delete_view.js
index e3771d3..4cceb92 100644
--- a/client/js/views/tag_delete_view.js
+++ b/client/js/views/tag_delete_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class TagDeleteView {
     constructor() {
-        this.template = views.getTemplate('tag-delete');
+        this._template = views.getTemplate('tag-delete');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
 
diff --git a/client/js/views/tag_merge_view.js b/client/js/views/tag_merge_view.js
index ee40c45..b18f951 100644
--- a/client/js/views/tag_merge_view.js
+++ b/client/js/views/tag_merge_view.js
@@ -2,18 +2,19 @@
 
 const config = require('../config.js');
 const views = require('../util/views.js');
-const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
+const TagAutoCompleteControl =
+    require('../controls/tag_auto_complete_control.js');
 
 class TagMergeView {
     constructor() {
-        this.template = views.getTemplate('tag-merge');
+        this._template = views.getTemplate('tag-merge');
     }
 
     render(ctx) {
         ctx.tagNamePattern = config.tagNameRegex;
 
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const otherTagField = source.querySelector('.target input');
diff --git a/client/js/views/tag_summary_view.js b/client/js/views/tag_summary_view.js
index 8b5b19d..5fdcfcf 100644
--- a/client/js/views/tag_summary_view.js
+++ b/client/js/views/tag_summary_view.js
@@ -2,7 +2,7 @@
 
 const config = require('../config.js');
 const views = require('../util/views.js');
-const TagInputControl = require('./tag_input_control.js');
+const TagInputControl = require('../controls/tag_input_control.js');
 
 function split(str) {
     return str.split(/\s+/).filter(s => s);
@@ -10,7 +10,7 @@ function split(str) {
 
 class TagSummaryView {
     constructor() {
-        this.template = views.getTemplate('tag-summary');
+        this._template = views.getTemplate('tag-summary');
     }
 
     render(ctx) {
@@ -18,7 +18,7 @@ class TagSummaryView {
         ctx.tagNamesPattern = '^((' + baseRegex + ')\\s+)*(' + baseRegex + ')$';
 
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const namesField = source.querySelector('.names input');
diff --git a/client/js/views/tag_view.js b/client/js/views/tag_view.js
index e022a87..eea7240 100644
--- a/client/js/views/tag_view.js
+++ b/client/js/views/tag_view.js
@@ -7,15 +7,15 @@ const TagDeleteView = require('./tag_delete_view.js');
 
 class TagView {
     constructor() {
-        this.template = views.getTemplate('tag');
-        this.summaryView = new TagSummaryView();
-        this.mergeView = new TagMergeView();
-        this.deleteView = new TagDeleteView();
+        this._template = views.getTemplate('tag');
+        this._summaryView = new TagSummaryView();
+        this._mergeView = new TagMergeView();
+        this._deleteView = new TagDeleteView();
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         ctx.section = ctx.section || 'summary';
 
@@ -29,11 +29,11 @@ class TagView {
 
         let view = null;
         if (ctx.section == 'merge') {
-            view = this.mergeView;
+            view = this._mergeView;
         } else if (ctx.section == 'delete') {
-            view = this.deleteView;
+            view = this._deleteView;
         } else {
-            view = this.summaryView;
+            view = this._summaryView;
         }
         ctx.target = source.querySelector('.tag-content-holder');
         view.render(ctx);
diff --git a/client/js/views/tags_header_view.js b/client/js/views/tags_header_view.js
index 565f7de..ac4dbf5 100644
--- a/client/js/views/tags_header_view.js
+++ b/client/js/views/tags_header_view.js
@@ -4,16 +4,17 @@ const page = require('page');
 const keyboard = require('../util/keyboard.js');
 const misc = require('../util/misc.js');
 const views = require('../util/views.js');
-const TagAutoCompleteControl = require('./tag_auto_complete_control.js');
+const TagAutoCompleteControl =
+    require('../controls/tag_auto_complete_control.js');
 
 class TagsHeaderView {
     constructor() {
-        this.template = views.getTemplate('tags-header');
+        this._template = views.getTemplate('tags-header');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const searchTextInput = form.querySelector('[name=search-text]');
diff --git a/client/js/views/tags_page_view.js b/client/js/views/tags_page_view.js
index 26c0f28..26d3630 100644
--- a/client/js/views/tags_page_view.js
+++ b/client/js/views/tags_page_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class TagsPageView {
     constructor() {
-        this.template = views.getTemplate('tags-page');
+        this._template = views.getTemplate('tags-page');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
         views.showView(target, source);
     }
 }
diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js
index 9a1d279..22973ff 100644
--- a/client/js/views/top_nav_view.js
+++ b/client/js/views/top_nav_view.js
@@ -4,13 +4,13 @@ const views = require('../util/views.js');
 
 class TopNavView {
     constructor() {
-        this.template = views.getTemplate('top-nav');
-        this.navHolder = document.getElementById('top-nav-holder');
+        this._template = views.getTemplate('top-nav');
+        this._navHolder = document.getElementById('top-nav-holder');
     }
 
     render(ctx) {
-        const target = this.navHolder;
-        const source = this.template(ctx);
+        const target = this._navHolder;
+        const source = this._template(ctx);
 
         for (let link of source.querySelectorAll('a')) {
             const regex = new RegExp(
@@ -21,7 +21,7 @@ class TopNavView {
                 '<span class="access-key" data-accesskey="$1">$1</span>');
         }
 
-        views.showView(this.navHolder, source);
+        views.showView(this._navHolder, source);
     }
 
     activate(itemName) {
diff --git a/client/js/views/user_delete_view.js b/client/js/views/user_delete_view.js
index c9e2be7..8fd07cd 100644
--- a/client/js/views/user_delete_view.js
+++ b/client/js/views/user_delete_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class UserDeleteView {
     constructor() {
-        this.template = views.getTemplate('user-delete');
+        this._template = views.getTemplate('user-delete');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
 
diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js
index 0f9d858..ac2ad20 100644
--- a/client/js/views/user_edit_view.js
+++ b/client/js/views/user_edit_view.js
@@ -2,12 +2,11 @@
 
 const config = require('../config.js');
 const views = require('../util/views.js');
-const FileDropperControl = require('./file_dropper_control.js');
+const FileDropperControl = require('../controls/file_dropper_control.js');
 
 class UserEditView {
     constructor() {
-        this.template = views.getTemplate('user-edit');
-        this.fileDropperControl = new FileDropperControl();
+        this._template = views.getTemplate('user-edit');
     }
 
     render(ctx) {
@@ -15,7 +14,7 @@ class UserEditView {
         ctx.passwordPattern = config.passwordRegex + /|^$/.source;
 
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
         const avatarContentField = source.querySelector('#avatar-content');
@@ -23,15 +22,18 @@ class UserEditView {
         views.decorateValidator(form);
 
         let avatarContent = null;
-        this.fileDropperControl.render({
-            target: avatarContentField,
-            lock: true,
-            resolve: files => {
-                source.querySelector(
-                    '[name=avatar-style][value=manual]').checked = true;
-                avatarContent = files[0];
-            },
-        });
+        if (avatarContentField) {
+            new FileDropperControl(
+                avatarContentField,
+                {
+                    lock: true,
+                    resolve: files => {
+                        source.querySelector(
+                            '[name=avatar-style][value=manual]').checked = true;
+                        avatarContent = files[0];
+                    },
+                });
+        }
 
         form.addEventListener('submit', e => {
             const rankField = source.querySelector('#user-rank');
diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js
index c740cce..a5109c8 100644
--- a/client/js/views/user_summary_view.js
+++ b/client/js/views/user_summary_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class UserSummaryView {
     constructor() {
-        this.template = views.getTemplate('user-summary');
+        this._template = views.getTemplate('user-summary');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
         views.listenToMessages(source);
         views.showView(target, source);
     }
diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js
index aac2e08..da94ad4 100644
--- a/client/js/views/user_view.js
+++ b/client/js/views/user_view.js
@@ -7,15 +7,15 @@ const UserEditView = require('./user_edit_view.js');
 
 class UserView {
     constructor() {
-        this.template = views.getTemplate('user');
-        this.deleteView = new UserDeleteView();
-        this.summaryView = new UserSummaryView();
-        this.editView = new UserEditView();
+        this._template = views.getTemplate('user');
+        this._deleteView = new UserDeleteView();
+        this._summaryView = new UserSummaryView();
+        this._editView = new UserEditView();
     }
 
     render(ctx) {
         const target = document.getElementById('content-holder');
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         ctx.section = ctx.section || 'summary';
 
@@ -29,11 +29,11 @@ class UserView {
 
         let view = null;
         if (ctx.section == 'edit') {
-            view = this.editView;
+            view = this._editView;
         } else if (ctx.section == 'delete') {
-            view = this.deleteView;
+            view = this._deleteView;
         } else {
-            view = this.summaryView;
+            view = this._summaryView;
         }
         ctx.target = source.querySelector('#user-content-holder');
         view.render(ctx);
diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js
index d2f4e6c..1f60e97 100644
--- a/client/js/views/users_header_view.js
+++ b/client/js/views/users_header_view.js
@@ -7,12 +7,12 @@ const views = require('../util/views.js');
 
 class UsersHeaderView {
     constructor() {
-        this.template = views.getTemplate('users-header');
+        this._template = views.getTemplate('users-header');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
 
         const form = source.querySelector('form');
 
diff --git a/client/js/views/users_page_view.js b/client/js/views/users_page_view.js
index 1e609af..c7230f5 100644
--- a/client/js/views/users_page_view.js
+++ b/client/js/views/users_page_view.js
@@ -4,12 +4,12 @@ const views = require('../util/views.js');
 
 class UsersPageView {
     constructor() {
-        this.template = views.getTemplate('users-page');
+        this._template = views.getTemplate('users-page');
     }
 
     render(ctx) {
         const target = ctx.target;
-        const source = this.template(ctx);
+        const source = this._template(ctx);
         views.showView(target, source);
     }
 }