├── .editorconfig
├── .gitattributes
├── .gitignore
├── .jshintrc
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── config.xml
├── images
└── safe_ios.png
├── package.json
├── src
├── css
│ ├── safe.android.css
│ └── safe.css
├── html
│ ├── addedit.handlebars
│ ├── changepassword.handlebars
│ ├── changesecurityinfo.handlebars
│ ├── forgotpassword.handlebars
│ ├── home.handlebars
│ ├── info.handlebars
│ ├── login.handlebars
│ ├── photo.handlebars
│ ├── photoitem.handlebars
│ ├── register.handlebars
│ └── settings.handlebars
├── images
│ └── placeholder.png
├── index.android.html
├── index.ios.html
└── js
│ ├── adapters
│ ├── camera.js
│ ├── crypto.js
│ ├── imageresizer.js
│ ├── notification.js
│ └── persistentfilestorage.js
│ ├── app.js
│ ├── backbonelocalstorage.js
│ ├── collections
│ └── photos.js
│ ├── data.js
│ ├── extensions.js
│ ├── fake
│ ├── camera.js
│ ├── crypto.js
│ ├── imageresizer.js
│ ├── keychain.js
│ ├── notification.js
│ └── persistentfilestorage.js
│ ├── keychain.js
│ ├── main.js
│ ├── models
│ ├── credential.js
│ ├── file.js
│ └── photo.js
│ ├── router.js
│ ├── serializer.js
│ ├── session.js
│ ├── settings.js
│ ├── templates.js
│ └── views
│ ├── addedit.js
│ ├── changepassword.js
│ ├── changesecurityinfo.js
│ ├── forgotpassword.js
│ ├── home.js
│ ├── info.js
│ ├── login.js
│ ├── photo.js
│ ├── photoitem.js
│ ├── photos.js
│ ├── register.js
│ └── settings.js
└── tests
├── addeditspec.js
├── changepasswordspec.js
├── changesecurityinfospec.js
├── forgotpasswordspec.js
├── homespec.js
├── loginspec.js
├── main.js
├── photospec.js
└── registerspec.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | # Server and client components
3 | node_modules
4 | bower_components
5 | # Unit-test reports
6 | coverage
7 | # Cordova folders
8 | cordova
9 | # Handlebars template file
10 | src/js/templates.js
11 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "undef": true,
16 | "unused": true,
17 | "strict": true,
18 | "trailing": true,
19 | "smarttabs": true,
20 | "jquery": true,
21 | "jasmine": true,
22 | "globals": {
23 | "define": true,
24 | "cordova": true,
25 | "Camera": true,
26 | "LocalFileSystem": true,
27 | "ImageResizer": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = (function() {
2 |
3 | 'use strict';
4 |
5 | return function (grunt) {
6 |
7 | // Load all the grunt tasks.
8 | require('load-grunt-tasks')(grunt);
9 |
10 | // Time how long tasks take. Can help when optimizing build times.
11 | require('time-grunt')(grunt);
12 |
13 | // Config object.
14 | var config = {
15 | src: 'src', // working directory
16 | tests: 'tests', // unit tests folder
17 | dist: 'cordova', // distribution folder
18 | supported: ['ios', 'android'], // supported platforms
19 | platform: grunt.option('platform') || 'ios' // current target platform
20 | };
21 |
22 | grunt.initConfig({
23 |
24 | config: config,
25 |
26 | // Make sure code styles are up to par and there are no obvious mistakes.
27 | jshint: {
28 | options: {
29 | jshintrc: '.jshintrc',
30 | reporter: require('jshint-stylish')
31 | },
32 | gruntfile: 'Gruntfile.js',
33 | src: [
34 | '<%= config.src %>/js/**/*.js',
35 | '!<%= config.src %>/js/templates.js'
36 | ],
37 | tests: [
38 | '<%= config.tests %>/**/*.js'
39 | ]
40 | },
41 |
42 | // Run jasmine tests.
43 | karma: {
44 | unit: {
45 | basePath: '',
46 | singleRun: true,
47 | frameworks: ['jasmine-jquery', 'jasmine', 'requirejs'],
48 | files: [
49 | {
50 | src: 'bower_components/**/*.js',
51 | included: false
52 | },
53 | {
54 | src: '<%= config.src %>/**/*.js',
55 | included: false
56 | },
57 | {
58 | src: '<%= config.tests %>/*spec.js',
59 | included: false
60 | },
61 | {
62 | src: '<%= config.tests %>/main.js'
63 | }
64 | ],
65 | exclude: ['<%= config.src %>/main.js'],
66 | port: 9002,
67 | reporters: ['mocha', 'coverage'],
68 | preprocessors: {
69 | '<%= config.src %>/js/views/*.js': 'coverage'
70 | },
71 | coverageReporter: {
72 | type: 'text'
73 | },
74 | plugins: [
75 | 'karma-phantomjs-launcher',
76 | 'karma-jasmine-jquery',
77 | 'karma-jasmine',
78 | 'karma-requirejs',
79 | 'karma-coverage',
80 | 'karma-mocha-reporter'
81 | ],
82 | browsers: ['PhantomJS']
83 | }
84 | },
85 |
86 | // Empty the 'www' folder.
87 | clean: {
88 | options: {
89 | force: true
90 | },
91 | dist: ['<%= config.dist %>/www']
92 | },
93 |
94 | // Precompile the handlebar templates.
95 | handlebars: {
96 | compile: {
97 | options: {
98 | amd: true,
99 | processName: function (filepath) {
100 | var pieces = filepath.split('/');
101 | return pieces[pieces.length - 1].split('.')[0];
102 | }
103 | },
104 | src: ['<%= config.src %>/html/{,*/}*.handlebars'],
105 | dest: '<%= config.src %>/js/templates.js'
106 | }
107 | },
108 |
109 | // Optimize the javascript files using r.js tool.
110 | requirejs: {
111 | compile: {
112 | options: {
113 | baseUrl: '<%= config.src %>/js',
114 | mainConfigFile: '<%= config.src %>/js/main.js',
115 | almond: true,
116 | include: ['main'],
117 | out: '<%= config.dist %>/www/js/index.min.js',
118 | optimize: 'uglify'
119 | }
120 | }
121 | },
122 |
123 | // Optimize the CSS files.
124 | cssmin: {
125 | compile: {
126 | files: {
127 | '<%= config.dist %>/www/css/index.min.css': [
128 | 'bower_components/ratchet/dist/css/ratchet.css',
129 | 'bower_components/ratchet/dist/css/ratchet-theme-<%= config.platform %>.css',
130 | '<%= config.src %>/css/safe.css',
131 | '<%= config.src %>/css/safe.<%= config.platform %>.css'
132 | ]
133 | }
134 | }
135 | },
136 |
137 | // Change the script and css references to optimized ones.
138 | processhtml: {
139 | dist: {
140 | files: {
141 | '<%= config.dist %>/www/index.html': ['<%= config.src %>/index.<%= config.platform %>.html']
142 | }
143 | }
144 | },
145 |
146 | // Copy the static resources like fonts, images to the platform specific folder.
147 | copy: {
148 | config: {
149 | expand: true,
150 | dot: true,
151 | src: 'config.xml',
152 | dest: 'cordova'
153 | },
154 | fonts: {
155 | expand: true,
156 | dot: true,
157 | cwd: 'bower_components/ratchet/fonts',
158 | dest: '<%= config.dist %>/www/fonts',
159 | src: ['{,*/}*.*']
160 | },
161 | images: {
162 | expand: true,
163 | dot: true,
164 | cwd: '<%= config.src %>/images',
165 | dest: '<%= config.dist %>/www/images',
166 | src: ['{,*/}*.*']
167 | }
168 | },
169 |
170 | // Grunt server settings
171 | connect: {
172 | options: {
173 | hostname: 'localhost',
174 | open: true,
175 | livereload: true
176 | },
177 | app: {
178 | options: {
179 | middleware: function (connect) {
180 | return [
181 | connect.static(config.src),
182 | connect().use('/bower_components', connect.static('./bower_components'))
183 | ];
184 | },
185 | port: 9000,
186 | open: {
187 | target: 'http://localhost:9000/index.<%= config.platform %>.html'
188 | }
189 | }
190 | },
191 | dist: {
192 | options: {
193 | port: 9001,
194 | base: '<%= config.dist %>/www',
195 | keepalive: true
196 | }
197 | }
198 | },
199 |
200 | // Watch files for changes and runs tasks based on the changed files.
201 | watch: {
202 |
203 | // Watch grunt file.
204 | gruntfile: {
205 | files: ['Gruntfile.js'],
206 | tasks: ['jshint:gruntfile']
207 | },
208 |
209 | // Watch javascript files.
210 | js: {
211 | files: [
212 | '<%= config.src %>/js/**/*.js',
213 | '!<%= config.src %>/js/templates.js'
214 | ],
215 | tasks: ['jshint:src'],
216 | options: {
217 | livereload: true
218 | }
219 | },
220 |
221 | // Watch handlebar templates.
222 | handlebars: {
223 | files: [
224 | '<%= config.src %>/html/{,*/}*.handlebars'
225 | ],
226 | tasks: ['handlebars'],
227 | options: {
228 | livereload: true
229 | }
230 | },
231 |
232 | // Watch html and css files.
233 | livereload: {
234 | options: {
235 | livereload: '<%= connect.options.livereload %>'
236 | },
237 | files: [
238 | '<%= config.src %>/index.<%= config.platform %>.html',
239 | '<%= config.src %>/css/safe.css',
240 | '<%= config.src %>/css/safe.<%= config.platform %>.css'
241 | ]
242 | }
243 | },
244 |
245 | // Task to install platforms and plugins and to build, emulate and deploy the app.
246 | cordovacli: {
247 | options: {
248 | path: './<%= config.dist %>'
249 | },
250 | install: {
251 | options: {
252 | command: ['create', 'platform', 'plugin'],
253 | platforms: '<%= config.supported %>',
254 | plugins: [
255 | 'camera',
256 | 'file',
257 | 'dialogs',
258 | 'https://github.com/VJAI/simple-crypto.git',
259 | 'https://github.com/wymsee/cordova-imageResizer.git'
260 | ],
261 | id: 'com.prideparrot.safe',
262 | name: 'Safe'
263 | }
264 | },
265 | build: {
266 | options: {
267 | command: 'build',
268 | platforms: ['<%= config.platform %>']
269 | }
270 | },
271 | emulate: {
272 | options: {
273 | command: 'emulate',
274 | platforms: ['<%= config.platform %>']
275 | }
276 | },
277 | deploy: {
278 | options: {
279 | command: 'run',
280 | platforms: ['<%= config.platform %>']
281 | }
282 | }
283 | }
284 | });
285 |
286 | // Build the web resources.
287 | grunt.registerTask('buildweb', [
288 | 'jshint',
289 | 'karma',
290 | 'clean',
291 | 'handlebars',
292 | 'requirejs',
293 | 'cssmin',
294 | 'processhtml',
295 | 'copy'
296 | ]);
297 |
298 | // Start the server and watch for changes.
299 | grunt.registerTask('serve', [
300 | 'jshint:src',
301 | 'handlebars',
302 | 'connect:app',
303 | 'watch'
304 | ]);
305 |
306 | // Start the server with distribution code.
307 | grunt.registerTask('dist', [
308 | 'buildweb',
309 | 'connect:dist'
310 | ]);
311 |
312 | // Run karma tests.
313 | grunt.registerTask('tests', [
314 | 'jshint:tests',
315 | 'karma'
316 | ]);
317 |
318 | // Create cordova project, add platforms and plugins.
319 | grunt.registerTask('create', [
320 | 'cordovacli:install'
321 | ]);
322 |
323 | // Build the app.
324 | grunt.registerTask('build', [
325 | 'buildweb',
326 | 'cordovacli:build'
327 | ]);
328 |
329 | // Run the app in emulator.
330 | grunt.registerTask('emulate', [
331 | 'buildweb',
332 | 'cordovacli:emulate'
333 | ]);
334 |
335 | // Deploy the app in device.
336 | grunt.registerTask('deploy', [
337 | 'buildweb',
338 | 'cordovacli:deploy'
339 | ]);
340 |
341 | // Default task.
342 | grunt.registerTask('default', [
343 | 'serve'
344 | ]);
345 | };
346 |
347 | })();
348 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Vijaya Anand, http://www.prideparrot.com
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | * You are not allowed to sell this app in same or different name.
11 |
12 | * You are also not allowed to build and sell apps by copying maximum portion
13 | of the source code.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Safe
2 |
3 | ## What?
4 |
5 | Safe is a hybrid mobile app that helps to keep your photos safe. The stored photos are encrypted using AES-256 algorithm. Safe supports iOS and Android platforms. To know how I built Safe step by step please take a look at this series .
6 |
7 | 
8 |
9 | ## Why?
10 |
11 | Safe is created in the aim to guide developers how to design and develop a hybrid mobile app using client-side technologies.
12 |
13 | ## How?
14 |
15 | Safe is built using Cordova and with plethora of client-side technologies like Backbone, Underscore, RequireJs and Handlebars. Ratchet is used to give the look and feel.
16 |
17 | ## Setting up the machine
18 |
19 | You've to install Node, Grunt, Bower and Cordova. To configure the machine for iOS and Android development please go through the cordova platform docs .
20 |
21 | ## Running Safe
22 |
23 | Once you've installed the necessary tools and downloaded the source-code, you might have to modify couple of configuration properties in the grunt file (Gruntfile.js).
24 |
25 | ### The `supported` property
26 |
27 | The `supported` property is an array that is used to specify the platforms that you've configured in your machine.
28 | If you are using Windows, then you can configure only for the android platform and you should update the `supported` property to `['android']`.
29 |
30 | ### The `platform` property
31 |
32 | At a time you can run Grunt commands for a single platform. For example you cannot build the source-code for both the platforms through a single command. You've to pass the platform parameter along with the Grunt commands. If you don't pass the parameter then what you've specified in the `platform` property is used.
33 |
34 | ### Install node modules and bower components
35 |
36 | Once your machine is setup, you should run the following commands from the terminal.
37 |
38 | > npm install
39 |
40 | This will install all the required node modules locally for the project.
41 |
42 | > bower install
43 |
44 | This will install all the required bower components locally for the project.
45 |
46 | ### Grunt commands
47 |
48 | Once you installed all the necessary components, you should run the below grunt command.
49 |
50 | > grunt create
51 |
52 | This will create the cordova project and install the supported platforms and the needed plugins.
53 |
54 | Assuming you've configured for both the platforms and the default platform is 'ios'. Following are the other important grunt commands you should know.
55 |
56 | | Command | Purpose |
57 | | --------------------------------- | ------------- |
58 | | grunt | Start a web server and run the app for iOS in browser |
59 | | grunt emulate | Run the app in iOS emulator |
60 | | grunt deploy | Deploy the app in iOS device |
61 | | grunt --platform=android | Start a web server and run the app for Android in browser |
62 | | grunt emulate --platform=android | Run the app in Android emulator |
63 | | grunt deploy --platform=android | Deploy the app in Android device |
64 | | grunt tests | Run the Jasmine unit tests |
65 |
66 |
67 | ## Settings
68 |
69 | The key that is used to encrypt the credential is stored in the settings file which exist under src/js. You SHOULD update the `encDecKey` and set a complex base64 string to it.
70 |
71 | ## Known issues
72 |
73 | While testing in couple of Nexus devices I noticed the encryption is taking too much time. I tested in a brand new Moto E device and it was blazing fast. If you are facing too much slowness then you could turn off encryption by overriding the `encrypt` property in settings file to false, which I don't recommend!
74 |
75 | ## Contributions
76 |
77 | Safe supports currently iOS and Android platforms. If you are interested to extend the support to other platforms please contact me . I'll be so glad to help out.
78 |
79 | ## License
80 |
81 | Copyright Vijaya Anand, http://www.prideparrot.com
82 |
83 | Permission is hereby granted, free of charge, to any person obtaining
84 | a copy of this software and associated documentation files (the
85 | "Software"), to deal in the Software without restriction, including
86 | without limitation the rights to use, copy, modify, merge, publish,
87 | distribute, sublicense, and to permit persons to whom the Software is
88 | furnished to do so, subject to the following conditions:
89 |
90 | * You are not allowed to sell this app in same or different name.
91 |
92 | * You are also not allowed to build and sell apps by copying maximum portion
93 | of the source code.
94 |
95 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
96 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
97 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
98 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
99 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
100 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
101 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
102 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Safe",
3 | "description": "Cordova based hybrid mobile app to keep your photos safe",
4 | "version": "0.1.0",
5 | "dependencies": {
6 | "almond": "~0.3.1",
7 | "backbone": "~1.1.2",
8 | "backbone.localStorage": "~1.1.16",
9 | "backbone.stickit": "0.8.0",
10 | "backbone.touch": "~0.4.2",
11 | "backbone.validation": "~0.7.1",
12 | "handlebars": "~3.0.0",
13 | "jquery": "~2.1.3",
14 | "ratchet": "~2.0.2",
15 | "underscore": "~1.8.2"
16 | },
17 | "devDependencies": {
18 | "requirejs": "~2.1.16"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Safe
5 |
6 |
7 | Cordova based mobile app to manage secret photos.
8 |
9 |
10 |
11 | Vijaya Anand
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/images/safe_ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VJAI/Safe/1407464651a49d70f60908731c8821e78f05b55c/images/safe_ios.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Safe",
3 | "description": "Cordova based hybrid mobile app to keep your photos safe",
4 | "version": "0.1.0",
5 | "devDependencies": {
6 | "cca": "~0.5.1",
7 | "cordova": "~4.3.0",
8 | "grunt": "^0.4.5",
9 | "grunt-contrib-clean": "^0.6.0",
10 | "grunt-contrib-connect": "^0.8.0",
11 | "grunt-contrib-copy": "^0.6.0",
12 | "grunt-contrib-cssmin": "^0.10.0",
13 | "grunt-contrib-handlebars": "~0.10.1",
14 | "grunt-contrib-jshint": "^0.10.0",
15 | "grunt-contrib-uglify": "^0.6.0",
16 | "grunt-contrib-watch": "^0.6.1",
17 | "grunt-cordovacli": "~0.7.0",
18 | "grunt-karma": "~0.10.1",
19 | "grunt-processhtml": "^0.3.3",
20 | "grunt-requirejs": "^0.4.2",
21 | "jasmine-core": "~2.2.0",
22 | "jshint-stylish": "^1.0.0",
23 | "karma": "^0.12.32",
24 | "karma-chrome-launcher": "~0.1.7",
25 | "karma-coverage": "~0.2.7",
26 | "karma-jasmine": "^0.3.5",
27 | "karma-jasmine-jquery": "^0.1.1",
28 | "karma-mocha-reporter": "~1.0.0",
29 | "karma-phantomjs-launcher": "^0.1.4",
30 | "karma-requirejs": "~0.2.2",
31 | "load-grunt-tasks": "^0.6.0",
32 | "requirejs": "~2.1.16",
33 | "time-grunt": "^1.0.0"
34 | },
35 | "private": true
36 | }
37 |
--------------------------------------------------------------------------------
/src/css/safe.android.css:
--------------------------------------------------------------------------------
1 | /* Android styles */
2 |
3 | .bar-tab {
4 | bottom: 0;
5 | top: auto;
6 | }
7 |
8 | .bar .bar-nav {
9 | border-top: solid 1px #ccc;
10 | }
11 |
12 | .content {
13 | padding-bottom: 50px !important;
14 | }
15 |
16 | .title {
17 | text-align: center;
18 | }
19 |
--------------------------------------------------------------------------------
/src/css/safe.css:
--------------------------------------------------------------------------------
1 | *:not(input):not(textarea) {
2 | -webkit-user-select: none;
3 | /* disable selection/Copy of UIWebView */
4 | -webkit-touch-callout: none;
5 | /* disable the IOS popup when long-press on a link */
6 | }
7 |
8 | /* Common styles */
9 | .img-responsive {
10 | width: 100%;
11 | height: auto;
12 | }
13 | .text-center {
14 | text-align: center;
15 | }
16 | .text-left {
17 | text-align: left;
18 | }
19 | .text-right {
20 | text-align: right;
21 | }
22 | .bold {
23 | font-weight: bold;
24 | }
25 | .field-label {
26 | margin: 10px 0 10px 0;
27 | display: inline-block;
28 | }
29 |
30 | /* Login Page */
31 | .login-page .content {
32 | padding-top: 65px !important;
33 | }
34 | .login-page form {
35 | margin-top: 15px;
36 | }
37 | .forgot-password {
38 | display: block;
39 | }
40 | #search-form {
41 | margin-bottom: 0px;
42 | }
43 | .description {
44 | margin: 10px;
45 | }
46 | #total {
47 | margin-top: 10px;
48 | }
--------------------------------------------------------------------------------
/src/html/addedit.handlebars:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 | Home
25 |
26 |
27 |
28 | Settings
29 |
30 |
31 |
32 | Info
33 |
34 |
35 |
36 |
51 |
--------------------------------------------------------------------------------
/src/html/changepassword.handlebars:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Change Password
6 |
7 |
8 |
9 | {{#if isAuthenticated}}
10 |
11 |
12 | Home
13 |
14 |
15 |
16 | Settings
17 |
18 |
19 |
20 | Info
21 |
22 | {{else}}
23 |
24 |
25 | Info
26 |
27 | {{/if}}
28 |
29 |
30 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/src/html/changesecurityinfo.handlebars:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Change Security Info
6 |
7 |
8 |
9 |
10 |
11 | Home
12 |
13 |
14 |
15 | Settings
16 |
17 |
18 |
19 | Info
20 |
21 |
22 |
23 |
24 |
34 |
35 |
--------------------------------------------------------------------------------
/src/html/forgotpassword.handlebars:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Verify yourself
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 | Info
20 |
21 |
--------------------------------------------------------------------------------
/src/html/home.handlebars:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Home
15 |
16 |
17 |
18 | Settings
19 |
20 |
21 |
22 | Info
23 |
24 |
25 |
26 |
27 | {{#if total}}
28 |
29 |
30 |
31 | {{/if}}
32 |
33 |
34 | {{#if total}} {{total}} photo(s) found {{else}} No photos found {{/if}}
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/html/info.handlebars:
--------------------------------------------------------------------------------
1 |
2 | {{#if backToUrl}}
3 |
4 |
5 |
6 | {{/if}}
7 | Info
8 |
9 |
10 |
11 | {{#if isAuthenticated}}
12 |
13 |
14 | Home
15 |
16 |
17 |
18 | Settings
19 |
20 | {{/if}}
21 |
22 |
23 | Info
24 |
25 |
26 |
27 |
28 |
29 |
Safe
30 |
31 | A simple app that helps to preserve your photos safe! The persisted photos are encrypted using the powerful AES 256 encryption algorithm.
32 |
33 |
34 | This app is an experiment done to demonstrate how we can architect and develop a pure hybrid application that runs across mobile platforms using Cordova, Backbone and other UI technologies.
35 |
36 |
37 | This application is developed purely for educational purpose. You can download the complete source code from Github . You are free to use the source code as a reference material to architect your mobile apps for personal or commercial purposes.
38 |
39 |
You are not allowed to sell this app in same or different name. You are also not allowed to build and sell apps by copying maximum portion of the source code. For more information about license please visit this page .
40 |
41 |
42 |
43 | As I said earlier, this is an experiment! If you run into any issues of using this app or source code the complete responsibility is yours! For any questions please contact me here .
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/html/login.handlebars:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Info
9 |
10 |
11 |
12 |
13 |
14 | Keep your photos safe!
15 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/src/html/photo.handlebars:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | Home
15 |
16 |
17 |
18 | Settings
19 |
20 |
21 |
22 | Info
23 |
24 |
25 |
26 |
27 |
28 |
{{description}}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/html/photoitem.handlebars:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{description}}
5 |
6 |
--------------------------------------------------------------------------------
/src/html/register.handlebars:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Info
9 |
10 |
11 |
12 |
13 |
14 | Keep your photos safe!
15 |
16 |
17 |
28 |
29 |
--------------------------------------------------------------------------------
/src/html/settings.handlebars:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Home
9 |
10 |
11 |
12 | Settings
13 |
14 |
15 |
16 | Info
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/images/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VJAI/Safe/1407464651a49d70f60908731c8821e78f05b55c/src/images/placeholder.png
--------------------------------------------------------------------------------
/src/index.android.html:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Safe
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/index.ios.html:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Safe
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/js/adapters/camera.js:
--------------------------------------------------------------------------------
1 | // Camera module.
2 | // A wrapper to the cordova camera plugin.
3 | // Contains methods to access the built-in camera, album and library.
4 | define(['jquery', 'underscore', 'settings'], function ($, _, settings) {
5 |
6 | 'use strict';
7 |
8 | return {
9 |
10 | getPicture: function (options) {
11 |
12 | var d = $.Deferred(),
13 | cameraOptions = _.extend({
14 | encodingType: navigator.camera.EncodingType.JPEG,
15 | destinationType: navigator.camera.DestinationType.DATA_URL,
16 | targetWidth: settings.targetWidth,
17 | targetHeight: settings.targetHeight
18 | }, options);
19 |
20 | navigator.camera.getPicture(d.resolve, d.reject, cameraOptions);
21 |
22 | return d.promise();
23 | },
24 |
25 | capturePicture: function () {
26 | return this.getPicture({
27 | sourceType: navigator.camera.PictureSourceType.CAMERA
28 | });
29 | },
30 |
31 | getPictureFromAlbum: function () {
32 | return this.getPicture({
33 | sourceType: navigator.camera.PictureSourceType.SAVEDPHOTOALBUM
34 | });
35 | },
36 |
37 | getPictureFromLibrary: function () {
38 | return this.getPicture({
39 | sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY
40 | });
41 | }
42 | };
43 | });
--------------------------------------------------------------------------------
/src/js/adapters/crypto.js:
--------------------------------------------------------------------------------
1 | // Crypto module.
2 | // A wrapper for the cordova crypto plugin.
3 | // Contains method to encrypt and decrypt data.
4 | define(['jquery'], function ($) {
5 |
6 | 'use strict';
7 |
8 | var getSimpleCrypto = function() {
9 | return cordova.require('com.disusered.simplecrypto.SimpleCrypto');
10 | };
11 |
12 | return {
13 | encrypt: function (key, data) {
14 | var d = $.Deferred();
15 | getSimpleCrypto().encrypt(key, data, d.resolve, d.reject);
16 | return d.promise();
17 | },
18 |
19 | decrypt: function (key, data) {
20 | var d = $.Deferred();
21 | getSimpleCrypto().decrypt(key, data, d.resolve, d.reject);
22 | return d.promise();
23 | }
24 | };
25 | });
--------------------------------------------------------------------------------
/src/js/adapters/imageresizer.js:
--------------------------------------------------------------------------------
1 | // Cordova Image Resizer Module.
2 | define(['jquery'], function ($) {
3 |
4 | 'use strict';
5 |
6 | return {
7 | resize: function (imageBase64, width, height) {
8 | var d = $.Deferred();
9 |
10 | window.imageResizer.resizeImage(function (data) {
11 | d.resolve(data);
12 | }, function (error) {
13 | d.reject(error);
14 | }, imageBase64, width, height, {
15 | imageType: ImageResizer.IMAGE_DATA_TYPE_BASE64,
16 | resizeType: ImageResizer.RESIZE_TYPE_PIXEL,
17 | storeImage: false,
18 | pixelDensity: true,
19 | photoAlbum: false
20 | });
21 |
22 | return d.promise();
23 | }
24 | };
25 | });
--------------------------------------------------------------------------------
/src/js/adapters/notification.js:
--------------------------------------------------------------------------------
1 | // Notification module.
2 | // A wrapper to the cordova notification plugin.
3 | // Contains methods to alert, vibrate, beep etc.
4 | define(['jquery'], function ($) {
5 |
6 | 'use strict';
7 |
8 | return {
9 |
10 | alert: function (message, title, buttonName) {
11 | var d = $.Deferred();
12 | navigator.notification.alert(message, d.resolve, title, buttonName);
13 | return d.promise();
14 | },
15 |
16 | confirm: function (message, title, buttonLabels) {
17 | var d = $.Deferred();
18 | navigator.notification.confirm(message, d.resolve, title, buttonLabels);
19 | return d.promise();
20 | },
21 |
22 | prompt: function (message, title, buttonLabels, defaultText) {
23 | var d = $.Deferred();
24 | navigator.notification.prompt(message, d.resolve, title, buttonLabels, defaultText);
25 | return d.promise();
26 | },
27 |
28 | beep: function (times) {
29 | navigator.notification.beep(times);
30 | },
31 |
32 | vibrate: function (milliseconds) {
33 | navigator.notification.vibrate(milliseconds);
34 | }
35 | };
36 | });
--------------------------------------------------------------------------------
/src/js/adapters/persistentfilestorage.js:
--------------------------------------------------------------------------------
1 | // FileStorage module.
2 | // A wrapper to the cordova file plugin.
3 | // Contains methods to read/write folders and files into the persistent file storage.
4 | // Ref: https://github.com/tonyhursh/gapfile/blob/master/www/gapfile.js
5 | define(['jquery'], function ($) {
6 |
7 | 'use strict';
8 |
9 | var root = '/';
10 |
11 | var extractDirectory = function (path) {
12 | var dirPath,
13 | lastSlash = path.lastIndexOf('/');
14 |
15 | /*jslint eqeqeq:true*/
16 | if (lastSlash == -1) {
17 | dirPath = root;
18 | } else {
19 | dirPath = path.substring(0, lastSlash);
20 |
21 | if (dirPath === '') {
22 | dirPath = root;
23 | }
24 | }
25 |
26 | return dirPath;
27 | };
28 |
29 | var extractFile = function (path) {
30 | var lastSlash = path.lastIndexOf('/');
31 |
32 | /*jslint eqeqeq:true*/
33 | if (lastSlash == -1) {
34 | return path;
35 | }
36 |
37 | var filename = path.substring(lastSlash + 1);
38 |
39 | return filename;
40 | };
41 |
42 | return {
43 | getFileSystem: function () {
44 | var d = $.Deferred();
45 | window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, d.resolve, d.reject);
46 | return d.promise();
47 | },
48 |
49 | getDirectory: function (name, options) {
50 | var d = $.Deferred();
51 |
52 | this.getFileSystem().then(function (fS) {
53 | fS.root.getDirectory(name, options, d.resolve, d.reject);
54 | }, d.reject);
55 |
56 | return d.promise();
57 | },
58 |
59 | createDirectory: function (name) {
60 | return this.getDirectory(name, {
61 | create: true,
62 | exclusive: false
63 | });
64 | },
65 |
66 | removeDirectory: function (name) {
67 | var d = $.Deferred();
68 |
69 | this.getDirectory(name, {
70 | create: false,
71 | exclusive: false
72 | }).then(function (dirEntry) {
73 | dirEntry.removeRecursively(d.resolve, d.reject);
74 | }, d.reject);
75 |
76 | return d.promise();
77 | },
78 |
79 | getFile: function (path, dirOptions, fileOptions) {
80 | var d = $.Deferred();
81 |
82 | this.getDirectory(extractDirectory(path), dirOptions).then(function (dirEntry) {
83 | dirEntry.getFile(extractFile(path), fileOptions, d.resolve, d.reject);
84 | }, d.reject);
85 |
86 | return d.promise();
87 | },
88 |
89 | writeToFile: function (path, data, append) {
90 | var d = $.Deferred();
91 |
92 | this.getFile(path, {
93 | create: true,
94 | exclusive: false
95 | }, {
96 | create: true
97 | }).then(function (fileEntry) {
98 | var fileURL = fileEntry.toURL();
99 | fileEntry.createWriter(
100 | function (writer) {
101 |
102 | writer.onwrite = function () {
103 | d.resolve(fileURL);
104 | };
105 |
106 | writer.onerror = d.reject;
107 |
108 | if (append === true) {
109 | writer.seek(writer.length);
110 | }
111 |
112 | writer.write(data);
113 | }, d.reject);
114 | }, d.reject);
115 |
116 | return d.promise();
117 | },
118 |
119 | readFromFile: function (path, asText) {
120 | var d = $.Deferred();
121 |
122 | this.getFile(path, {
123 | create: false,
124 | exclusive: false
125 | }, {
126 | create: false
127 | }).then(function (fileEntry) {
128 | fileEntry.file(function (file) {
129 |
130 | var reader = new FileReader();
131 |
132 | reader.onloadend = function (evt) {
133 | d.resolve(evt.target.result);
134 | };
135 |
136 | reader.onerror = d.reject;
137 |
138 | if (asText === true) {
139 | reader.readAsText(file);
140 | } else {
141 | reader.readAsDataURL(file);
142 | }
143 | }, d.reject);
144 | });
145 |
146 | return d.promise();
147 | },
148 |
149 | deleteFile: function (path) {
150 | var d = $.Deferred();
151 |
152 | this.getFile(path, {
153 | create: false,
154 | exclusive: false
155 | }, {
156 | create: false
157 | }).then(function (fileEntry) {
158 | fileEntry.remove(d.resolve, d.reject);
159 | }, d.reject);
160 |
161 | return d.promise();
162 | }
163 | };
164 | });
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | // Startup module.
2 | define(['backbone', 'extensions', 'session'], function (Backbone, session) {
3 |
4 | 'use strict';
5 |
6 | // Starts the backbone routing.
7 | function startHistory() {
8 | Backbone.history.start();
9 | }
10 |
11 | return {
12 |
13 | // Add extension methods and start app on device ready.
14 | start: function (isDevice) {
15 |
16 | // If the app is running in device, run the startup code in the 'deviceready' event else on document ready.
17 | if (isDevice) {
18 | document.addEventListener('deviceready', function() {
19 |
20 | // Clear the session when the app moves to background.
21 | document.addEventListener('pause', function() {
22 | session.clear();
23 | }, false);
24 |
25 | startHistory();
26 | }, false);
27 | } else {
28 | $(startHistory);
29 | }
30 | }
31 | };
32 | });
--------------------------------------------------------------------------------
/src/js/backbonelocalstorage.js:
--------------------------------------------------------------------------------
1 | // Customized localstorage based persistent store for backbone.
2 | // Ref: https://github.com/jeromegn/Backbone.localStorage/blob/master/backbone.localStorage.js
3 | define([
4 | 'jquery',
5 | 'backbone',
6 | 'underscore',
7 | 'serializer'
8 | ], function (
9 | $,
10 | Backbone,
11 | _,
12 | serializer
13 | ) {
14 |
15 | 'use strict';
16 |
17 | Backbone.LocalStorage = function (name) {
18 | this.name = name;
19 | var store = this.localStorage().getItem(this.name);
20 | this.records = (store && store.split(',')) || [];
21 | };
22 |
23 | _.extend(Backbone.LocalStorage.prototype, {
24 |
25 | // Save the data to localstorage.
26 | save: function () {
27 | this.localStorage().setItem(this.name, this.records.join(','));
28 | },
29 |
30 | // Serialize the newly created model and persist it.
31 | // Invoked by 'collection.create'.
32 | create: function (model) {
33 | var d = $.Deferred();
34 |
35 | if (!model.id && model.id !== 0) {
36 | model.id = _.guid();
37 | model.set(model.idAttribute, model.id);
38 | }
39 |
40 | serializer.serialize(model)
41 | .done(_.bind(function (data) {
42 | this.localStorage().setItem(this.itemName(model.id), data);
43 | this.records.push(model.id.toString());
44 | this.save();
45 | d.resolve(this.find(model));
46 | }, this))
47 | .fail(d.reject);
48 |
49 | return d.promise();
50 | },
51 |
52 | // Update the persisted model.
53 | update: function (model) {
54 | var d = $.Deferred();
55 |
56 | serializer.serialize(model)
57 | .done(_.bind(function (data) {
58 | this.localStorage().setItem(this.itemName(model.id), data);
59 | var modelId = model.id.toString();
60 | if (!_.contains(this.records, modelId)) {
61 | this.records.push(modelId);
62 | this.save();
63 | }
64 | d.resolve(this.find(model));
65 | }, this))
66 | .fail(d.reject);
67 |
68 | return d.promise();
69 | },
70 |
71 | find: function (model) {
72 | return serializer.deserialize(this.localStorage().getItem(this.itemName(model.id)));
73 | },
74 |
75 | // http://stackoverflow.com/questions/27100664/exit-from-for-loop-when-any-jquery-deferred-fails/27118636#27118636
76 | findAll: function () {
77 | var d = $.Deferred();
78 |
79 | var promises = this.records.map(_.bind(function (record) {
80 | return serializer.deserialize(this.localStorage().getItem(this.itemName(record)));
81 | }, this));
82 |
83 | $.when.apply($, promises).done(function () {
84 | d.resolve(Array.prototype.slice.apply(arguments));
85 | }).fail(d.reject);
86 |
87 | return d.promise();
88 | },
89 |
90 | destroy: function (model) {
91 | var d = $.Deferred();
92 | this.localStorage().removeItem(this.itemName(model.id));
93 | var modelId = model.id.toString();
94 |
95 | for (var i = 0; i < this.records.length; i++) {
96 | if (this.records[i] === modelId) {
97 | this.records.splice(i, 1);
98 | }
99 | }
100 |
101 | this.save();
102 | d.resolve(model);
103 |
104 | return d.promise();
105 | },
106 |
107 | clear: function () {
108 | var local = this.localStorage(),
109 | itemRe = new RegExp('^' + this.name + '-');
110 |
111 | local.removeItem(this.name);
112 |
113 | for (var k in local) {
114 | if (itemRe.test(k)) {
115 | local.removeItem(k);
116 | }
117 | }
118 |
119 | this.records.length = 0;
120 | },
121 |
122 | storageSize: function () {
123 | return this.localStorage().length;
124 | },
125 |
126 | itemName: function (id) {
127 | return this.name + '-' + id;
128 | },
129 |
130 | localStorage: function () {
131 | return window.localStorage;
132 | }
133 | });
134 |
135 | // TODO: need to eliminate this.
136 | function result(object, property) {
137 | if (object === null) {
138 | return void 0;
139 | }
140 | var value = object[property];
141 | return (typeof value === 'function') ? object[property]() : value;
142 | }
143 |
144 | // Localstorage sync.
145 | Backbone.LocalStorage.sync = function (method, model, options) {
146 | var d = $.Deferred(),
147 | resp,
148 | store = result(model, 'localStorage') || result(model.collection, 'localStorage');
149 |
150 | switch (method) {
151 | case 'read':
152 | resp = (model.id !== undefined ? store.find(model) : store.findAll());
153 | break;
154 | case 'create':
155 | resp = store.create(model);
156 | break;
157 | case 'update':
158 | resp = store.update(model);
159 | break;
160 | case 'delete':
161 | resp = store.destroy(model);
162 | break;
163 | }
164 |
165 | resp.done(function (result) {
166 | if (options && options.success) {
167 | options.success(result);
168 | }
169 | d.resolve(result);
170 | }).fail(function (error) {
171 | if (options && options.error) {
172 | options.error(error);
173 | }
174 | d.reject(error);
175 | });
176 |
177 | if (options && options.complete) {
178 | options.complete(resp);
179 | }
180 |
181 | return d.promise();
182 | };
183 |
184 | // Override the backbone sync with localstorage sync.
185 | Backbone.sync = function (method, model, options) {
186 | return Backbone.LocalStorage.sync.apply(this, [method, model, options]);
187 | };
188 |
189 | return Backbone.LocalStorage;
190 | });
--------------------------------------------------------------------------------
/src/js/collections/photos.js:
--------------------------------------------------------------------------------
1 | define(['backbone', 'models/photo', 'backbonelocalstorage'], function (Backbone, Photo) {
2 |
3 | 'use strict';
4 |
5 | // Represent collection of photos.
6 | var Photos = Backbone.Collection.extend({
7 |
8 | // Use localstorage for persistence.
9 | localStorage: new Backbone.LocalStorage('secret-photos'),
10 |
11 | model: Photo,
12 |
13 | // Sort the photos in descending order by 'lastSaved' date.
14 | comparator: function(photo) {
15 | var lastSavedDate = new Date(photo.get('lastSaved'));
16 | return -lastSavedDate.getTime();
17 | },
18 |
19 | // Text search by 'description'.
20 | search: function (text) {
21 | if(!text) {
22 | return this.models;
23 | }
24 |
25 | return this.filter(function (photo) {
26 | return photo.get('description').toLowerCase().indexOf(text.toLowerCase()) > -1;
27 | });
28 | }
29 | });
30 |
31 | return Photos;
32 | });
33 |
--------------------------------------------------------------------------------
/src/js/data.js:
--------------------------------------------------------------------------------
1 | // Contains all the static data.
2 | define(function () {
3 |
4 | 'use strict';
5 |
6 | return {
7 | securityQuestions: [
8 | 'Which movie you love the most',
9 | 'Which country you\'ll love to go for vacation'
10 | ]
11 | };
12 | });
--------------------------------------------------------------------------------
/src/js/extensions.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'backbone',
5 | 'router',
6 | 'validation',
7 | 'stickit',
8 | 'touch',
9 | 'backbonelocalstorage'
10 | ], function (
11 | $,
12 | _,
13 | Backbone,
14 | router
15 | ) {
16 |
17 | 'use strict';
18 |
19 | function S4() {
20 | /*jslint bitwise: true */
21 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
22 | }
23 |
24 | // Add custom methods to underscore.
25 | _.mixin({
26 | // extension method to create GUID.
27 | guid: function () {
28 | return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
29 | },
30 |
31 | // return a promise object with resolve.
32 | resolve: function () {
33 | var d = $.Deferred();
34 | d.resolve.apply(null, arguments);
35 | return d.promise();
36 | },
37 |
38 | // return a promise object with reject.
39 | reject: function () {
40 | var d = $.Deferred();
41 | d.reject.apply(null, arguments);
42 | return d.promise();
43 | }
44 | });
45 |
46 | // Extend backbone model to perform custom validation.
47 | _.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
48 |
49 | // Extend backbone views with custom methods.
50 | _.extend(Backbone.View.prototype, {
51 |
52 | // Stop listening to events and remove the children.
53 | ghost: function () {
54 | this.unstickit();
55 | this.stopListening();
56 | this.undelegateEvents();
57 | this.$el.html('');
58 | return this;
59 | },
60 |
61 | // A delegate to router's navigate method.
62 | navigateTo: function (page, trigger) {
63 | router.navigate(page, trigger || true);
64 | }
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/js/fake/crypto.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'underscore'], function ($, _) {
2 |
3 | 'use strict';
4 |
5 | return {
6 | encrypt: function (key, data) {
7 | return _.resolve(data);
8 | },
9 |
10 | decrypt: function (key, data) {
11 | return _.resolve(data);
12 | }
13 | };
14 | });
15 |
--------------------------------------------------------------------------------
/src/js/fake/imageresizer.js:
--------------------------------------------------------------------------------
1 | define(['jquery'], function ($) {
2 |
3 | 'use strict';
4 |
5 | return {
6 | /* jslint unused: false */
7 | resize: function (imageBase64, width, height) {
8 | var deferred = $.Deferred();
9 |
10 | var resizedImageBase64 =
11 | 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAUCAIAAAB00TjRAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAASAAAAEgARslrPgAABntJREFUSMfFlltsXEcZx+d2rrvnnF2vvU7WGzv1JTZJHSeo4SJVpEoRVZMmQpF4KFRCgFCAqoISURGpAsFbHyCRKpSKh0IkLi+lKFBUEWhEColwkiq4abK4tvc4Xttr733PZc/ZOWdmeFg1LU1TCC/M42i++X3/b77LQCEE+P8t8r+ZCSHAnX7/+w6EECD04ffAe1LfOwwhvIfznEOEwF1MyD2xe2AeRZ1G06vVOrV65DggCLgAiDMt8ADtcs6lZNLM55Nj4zC/DWAMhACcf2AkyH9J7klubWwslUoLhcLy4mJlY9N33U6r6QRBrl55OPJvJtOerJgDA7qiMqdtMbZ7W/7+T38m+dmjQNc/0IP/FHwhBAAQQk7p4lvXl1bXYoRkWaaUMs6zAwMXLl364+/OfrVT//vknhUzPWwae2Z2p1IpWVF++9JLWjccsouPWsbe7/9Q7NkL7/CA3FUuAADCnuig3b755tx62+nLZjVNgwgRQiCEYRheX1g6Urml7ZrelRvMr6x0+9O35uereuLgY4cmxyfcIFAeOvDqfGHzW08d+N4PpAMPA8YgxndR38vndxyMOWdxHDjtzfXyar1BXS/2/dB1Q8+J43ijXLZXV8ySPbq5erLe2acTmjDHHzloZfpBGN6X29pcX/eazUEjqfRlSn+98HkEBp87iWdm3vsKd+AhZJx3NiqdcjlsNGLXizwfRRQ4rWqpVKlstquVsF7lXsugXpbgoUTykuNfazaHjdSAqm7T1SRCMoSIc0yIJMmKJGNMVCu13KjLYx8Z/+4Ja2K8B3o/vhOG3StXa394pWnbvtPqek6347HAbfj+ApJxKt3vNtOAYwiSqi6ribjjD5umme7XJSyrmsA4hhBgLDBUNH3JXuBcjE3txKpaLC6W/vHG+GAumx7QvnTMPHq0F4N38QFj1TNn0C/PVIeH/XoV/XOOSiigoS3Qr5GV3r33/k89NHT2Fx9dLvRv30ETZpPDQQm5NCYSlOIuoAGPujyiIo4EiwBjgjHOAYEiZrwd49EtW+eY2Gi1jhx4ZPDHpyHBQAgCABCcQ4RaNwvOn89BQ0fZrN6sglQmiGjoOkF+NDG8y4R8X3lpa702PLX396538upb9bA7LuipB6bjtuutLRJZ4QIACASAEEIAIYKQQ8Qg6AKyLbPlN+XNH4VSPQjP/ezFFyYmMk99BwoBBecAwrBaaT9/anWp2KJd5ro0jiljgdvUOCWE0FTGYlGeiWQq/Ze1+adtN5PLKUKUvM5Mc/25HTm/VVcJYlyIdzojAAABwCGCMU3kpgRCx0qbgZmxdG3pzbmf7P/Y5145LzgngnOIcbO08refP58fHOgjWJIkggkiUqSDLkMAITlyoSSjpFENN18sN7ZunxpKG2YqndqsFDvujdXVCUOLaSTebfYQQAgggBAyhBUVzTUa1Mw8/fVjjuefKhaXlQQAgAtBejUgjdz3TWa0rt3SCUacYSAQEH39WYax4/m58bFcc/VZ5DQi5siDZkJ9/AtPjI6NvXD6tL2yUqH+MJI6qskhYgiHMQtiFgkQcUEj3uHy0M3CLOVkYsuyveyFoSxJ2U882Cs00qsBFaHtmCwwrkiSpKkxYwJAKABotrIQKEvFB/oMJ8bX235Nj0cxee38+dnLV1rtNsbo7UZH1awShQCCDg2sTMbKZpCsyImEput9lnXj7YVfnfvTx2e0G4VCJwi+/cwzX3zySQAAJoRAhAAAyXT6p2fPijh69sSJV187P6Br3TAgRuIbX/nycqHgNpt2HFfNkZJWW51ffHD//lqtbtv2jsnJ4Xx+ciinyvLcyy9fuHLV0PWvPf6EOTzChJAURVYULZHYue+TyhvXLr7++mOHDyuyPLVzp9/pCCEURYGu61JKwzBknMeMz16ePX78+HqpBAB49MiRQ4cPl0qlWr3utNoIQtsuXr54MZvNHjx0iBByeXa2aNuyqvq+H8cxiyJJlgVjQghFlhVF0TRN17RUOtU/kAUQpixrbGxsenp6ZGRkaGjINE24sbHhOE65XLZtu1gs1uv1dqu1trYWBIHneZVKRQjRS2bBuaIoCcNgjFmWlc1mNU3r6+szDSOVSpmWaVmpZCJhmKZhGIlEQtd1VVVVVZVlGWOsKIokSb2RffvH8P6JJ4SIoogxRimllHLO4zjmnAMAEEIYY4SQJEkIY4kQRVHwe+bHh39SboMghLfx/wLg1oT6h+nc9AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wMS0xN1QwODoxMjo1Mi0wNTowMD3Ny5MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDEtMTdUMDg6MTI6NTItMDU6MDBMkHMvAAAAAElFTkSuQmCC';
12 |
13 | deferred.resolve({
14 | imageData: resizedImageBase64
15 | });
16 |
17 | return deferred.promise();
18 | }
19 | };
20 | });
--------------------------------------------------------------------------------
/src/js/fake/keychain.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'adapters/crypto'], function ($, crypto) {
2 |
3 | 'use strict';
4 |
5 | var encDecKey = 'TEST';
6 |
7 | /* jslint unused: false */
8 | return {
9 |
10 | containsKey: function (key, serviceName) {
11 | var d = $.Deferred();
12 |
13 | if (window.localStorage.getItem(key)) {
14 | d.resolve(true);
15 | } else {
16 | d.resolve(false);
17 | }
18 |
19 | return d.promise();
20 | },
21 |
22 | getForKey: function (key, serviceName) {
23 | var result = window.localStorage.getItem(key);
24 | return crypto.decrypt(encDecKey, result);
25 | },
26 |
27 | setForKey: function (key, serviceName, value) {
28 | var d = $.Deferred();
29 |
30 | crypto.encrypt(encDecKey, value)
31 | .done(function (encValue) {
32 | window.localStorage.setItem(key, encValue);
33 | d.resolve();
34 | }).fail(d.reject);
35 |
36 | return d.promise();
37 | },
38 |
39 | removeForKey: function (key, serviceName) {
40 | var d = $.Deferred();
41 | window.localStorage.removeItem(key);
42 | d.resolve();
43 | return d.promise();
44 | }
45 | };
46 | });
--------------------------------------------------------------------------------
/src/js/fake/notification.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'underscore'], function ($, _) {
2 |
3 | 'use strict';
4 |
5 | return {
6 |
7 | alert: function (message) {
8 | var d = $.Deferred();
9 | window.alert(message);
10 | d.resolve();
11 | return d.promise();
12 | },
13 |
14 | confirm: function (message) {
15 | var d = $.Deferred();
16 | var result = window.confirm(message);
17 |
18 | if(result) {
19 | d.resolve(1);
20 | } else {
21 | d.reject();
22 | }
23 |
24 | return d.promise();
25 | },
26 |
27 | prompt: function (message) {
28 | var d = $.Deferred();
29 | window.prompt(message);
30 | d.resolve();
31 | return d.promise();
32 | },
33 |
34 | beep: _.noop,
35 |
36 | vibrate: _.noop
37 | };
38 | });
39 |
--------------------------------------------------------------------------------
/src/js/keychain.js:
--------------------------------------------------------------------------------
1 | // A simple 'localstorage' based keychain.
2 | define(['jquery', 'adapters/crypto', 'settings'], function ($, crypto, settings) {
3 |
4 | 'use strict';
5 |
6 | return {
7 |
8 | containsKey: function (key) {
9 | return window.localStorage.getItem(key) ? true : false;
10 | },
11 |
12 | getForKey: function (key) {
13 | var result = window.localStorage.getItem(key);
14 | return crypto.decrypt(settings.encDecKey, result);
15 | },
16 |
17 | setForKey: function (key, value) {
18 | var d = $.Deferred();
19 |
20 | crypto.encrypt(settings.encDecKey, value)
21 | .done(function (encValue) {
22 | window.localStorage.setItem(key, encValue);
23 | d.resolve();
24 | }).fail(d.reject);
25 |
26 | return d.promise();
27 | },
28 |
29 | removeForKey: function (key) {
30 | window.localStorage.removeItem(key);
31 | }
32 | };
33 | });
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | (function () {
2 |
3 | 'use strict';
4 |
5 | require.config({
6 |
7 | baseUrl: 'js',
8 |
9 | paths: {
10 | jquery: '../../bower_components/jquery/dist/jquery',
11 | underscore: '../../bower_components/underscore/underscore',
12 | backbone: '../../bower_components/backbone/backbone',
13 | validation: '../../bower_components/backbone.validation/dist/backbone-validation-amd',
14 | stickit: '../../bower_components/backbone.stickit/backbone.stickit',
15 | touch: '../../bower_components/backbone.touch/backbone.touch',
16 | handlebars: '../../bower_components/handlebars/handlebars.runtime',
17 | almond: '../../bower_components/almond/almond'
18 | },
19 |
20 | shim: {
21 | jquery: {
22 | exports: '$'
23 | },
24 |
25 | underscore: {
26 | deps: ['jquery'],
27 | exports: '_'
28 | },
29 |
30 | backbone: {
31 | deps: ['underscore', 'jquery'],
32 | exports: 'Backbone'
33 | },
34 |
35 | handlebars: {
36 | exports: 'Handlebars'
37 | }
38 | }
39 | });
40 |
41 | var isDevice = navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|IEMobile)/);
42 |
43 | if(!isDevice) {
44 | require.config({
45 | paths: {
46 | adapters: 'fake'
47 | }
48 | });
49 | }
50 |
51 | require(['app'], function (app) {
52 | app.start(isDevice);
53 | });
54 | })();
55 |
--------------------------------------------------------------------------------
/src/js/models/credential.js:
--------------------------------------------------------------------------------
1 | // Credential model.
2 | define(['jquery', 'underscore', 'backbone', 'keychain'], function ($, _, Backbone, keychain) {
3 |
4 | 'use strict';
5 |
6 | // Model that represents credential.
7 | var Credential = Backbone.Model.extend({
8 |
9 | defaults: {
10 | password: null,
11 | key: null, // encryption-decryption key for photos
12 | securityQuestion: null,
13 | securityAnswer: null
14 | },
15 |
16 | // Validations. All properties are required!
17 | validation: {
18 | password: {
19 | required: true
20 | },
21 | key: {
22 | required: true
23 | },
24 | securityQuestion: {
25 | required: true
26 | },
27 | securityAnswer: {
28 | required: true
29 | }
30 | },
31 |
32 | // Override the sync method to persist the credential in keychain.
33 | sync: function (method, model, options) {
34 | var d = $.Deferred(),
35 | resp;
36 |
37 | switch (method) {
38 | case 'read':
39 | resp = this.findModel();
40 | break;
41 | case 'update':
42 | resp = this.createOrUpdate();
43 | break;
44 | case 'delete':
45 | throw new Error('Not implemented');
46 | }
47 |
48 | resp.done(function (result) {
49 | if (options && options.success) {
50 | options.success(result);
51 | }
52 | d.resolve(result);
53 | }).fail(function (error) {
54 | if (options && options.error) {
55 | options.error(error);
56 | }
57 | d.reject(error);
58 | });
59 |
60 | if (options && options.complete) {
61 | options.complete(resp);
62 | }
63 |
64 | return d.promise();
65 | },
66 |
67 | // Returns the persisted model as JSON.
68 | findModel: function () {
69 | var d = $.Deferred();
70 |
71 | keychain.getForKey(this.id)
72 | .done(_.bind(function (persistedData) {
73 | d.resolve(JSON.parse(persistedData));
74 | }, this))
75 | .fail(d.reject);
76 |
77 | return d.promise();
78 | },
79 |
80 | // Save or update the persisted model.
81 | createOrUpdate: function () {
82 | return keychain.setForKey(this.id, JSON.stringify(this.toJSON()));
83 | }
84 | });
85 |
86 | return Credential;
87 | });
--------------------------------------------------------------------------------
/src/js/models/file.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'backbone',
5 | 'adapters/persistentfilestorage',
6 | 'session',
7 | 'serializer'
8 | ], function (
9 | $,
10 | _,
11 | Backbone,
12 | persistentfilestorage,
13 | session,
14 | serializer
15 | ) {
16 |
17 | 'use strict';
18 |
19 | // Model used to represent the actual image file.
20 | var File = Backbone.Model.extend({
21 |
22 | defaults: {
23 | data: null // file content
24 | },
25 |
26 | // Validation rules.
27 | validation: {
28 | data: {
29 | required: true
30 | }
31 | },
32 |
33 | // Override the sync method to persist the photo in filesystem.
34 | sync: function (method, model, options) {
35 | var d = $.Deferred(),
36 | resp;
37 |
38 | switch (method) {
39 | case 'read':
40 | resp = this.findFile();
41 | break;
42 | case 'update':
43 | resp = this.writeFile(model);
44 | break;
45 | case 'delete':
46 | resp = this.deleteFile();
47 | break;
48 | }
49 |
50 | resp.done(function (result) {
51 | if (options && options.success) {
52 | options.success(result);
53 | }
54 | d.resolve(result);
55 | }).fail(function (error) {
56 | if (options && options.error) {
57 | options.error(error);
58 | }
59 | d.reject(error);
60 | });
61 |
62 | if (options && options.complete) {
63 | options.complete(resp);
64 | }
65 |
66 | return d.promise();
67 | },
68 |
69 | findFile: function () {
70 | var d = $.Deferred();
71 |
72 | persistentfilestorage.readFromFile(this.getPhotoPath(), true)
73 | .then(_.bind(function (persistedImage) {
74 | return serializer.deserialize(persistedImage);
75 | }, this), d.reject)
76 | .then(_.bind(function (image) {
77 | d.resolve(image);
78 | }, this), d.reject);
79 |
80 | return d.promise();
81 | },
82 |
83 | writeFile: function (model) {
84 | var d = $.Deferred();
85 |
86 | serializer.serialize(model)
87 | .done(_.bind(function (encImage) {
88 | persistentfilestorage.writeToFile(this.getPhotoPath(), encImage);
89 | d.resolve();
90 | }, this))
91 | .fail(d.reject);
92 |
93 | return d.promise();
94 | },
95 |
96 | deleteFile: function () {
97 | return persistentfilestorage.deleteFile(this.getPhotoPath());
98 | },
99 |
100 | getPhotoPath: function () {
101 | return 'Photos/' + this.id + '.dat';
102 | }
103 | });
104 |
105 | return File;
106 | });
107 |
--------------------------------------------------------------------------------
/src/js/models/photo.js:
--------------------------------------------------------------------------------
1 | define(['backbone'], function (Backbone) {
2 |
3 | 'use strict';
4 |
5 | // Model that represents the meta-data information of the secret photo.
6 | // Contains properties to represent description, thumbnail and last-saved-date.
7 | var Photo = Backbone.Model.extend({
8 |
9 | // Properties (attributes)
10 | defaults: {
11 | description: null,
12 | thumbnail: null,
13 | lastSaved: Date()
14 | },
15 |
16 | // Validation rules.
17 | validation: {
18 | description: {
19 | required: true
20 | },
21 | thumbnail: {
22 | required: true
23 | }
24 | }
25 | });
26 |
27 | return Photo;
28 | });
--------------------------------------------------------------------------------
/src/js/router.js:
--------------------------------------------------------------------------------
1 | // Custom backbone router.
2 | // Define all routes and methods to handle them.
3 | // **I wish backbone should have CONTROLLERS!
4 | define([
5 | 'jquery',
6 | 'underscore',
7 | 'backbone',
8 | 'keychain',
9 | 'adapters/notification',
10 | 'adapters/crypto',
11 | 'adapters/persistentfilestorage',
12 | 'data',
13 | 'models/credential',
14 | 'models/photo',
15 | 'models/file',
16 | 'collections/photos',
17 | 'session',
18 | 'views/register',
19 | 'views/login',
20 | 'views/forgotpassword',
21 | 'views/changepassword',
22 | 'views/changesecurityinfo',
23 | 'views/home',
24 | 'views/addedit',
25 | 'views/photo',
26 | 'views/info',
27 | 'views/settings'
28 | ], function (
29 | $,
30 | _,
31 | Backbone,
32 | keychain,
33 | notification,
34 | crypto,
35 | persistentfilestorage,
36 | Data,
37 | Credential,
38 | Photo,
39 | File,
40 | Photos,
41 | session,
42 | RegisterView,
43 | LoginView,
44 | ForgotPasswordView,
45 | ChangePasswordView,
46 | ChangeSecurityInfoView,
47 | HomeView,
48 | AddEditView,
49 | PhotoView,
50 | InfoView,
51 | SettingsView
52 | ) {
53 |
54 | 'use strict';
55 |
56 | // Array that maps the user state to allowed/not-allowed routes and default one.
57 | var stateRouteMapArray = [
58 | {
59 | state: 'NOT_REGISTERED',
60 | allowed: 'register|info',
61 | default: 'register'
62 | },
63 | {
64 | state: 'LOGGED_OUT',
65 | allowed: 'login|forgotpassword|info',
66 | default: 'login'
67 | },
68 | {
69 | state: 'VERIFIED',
70 | allowed: 'changepassword|info|login',
71 | default: 'changepassword'
72 | },
73 | {
74 | state: 'LOGGED_IN',
75 | notallowed: 'register|login|forgotpassword',
76 | default: 'photos'
77 | }
78 | ];
79 |
80 | // Returns the route that the user should be directed to.
81 | var whereToGo = function (state, route) {
82 |
83 | // Get the map object for the user state.
84 | var stateRouteMap = _.find(stateRouteMapArray, {
85 | state: state
86 | });
87 |
88 | var routes = (stateRouteMap.allowed || stateRouteMap.notallowed).split('|'),
89 | toGo;
90 | var containsRoute = _.contains(routes, route);
91 |
92 | if (stateRouteMap.allowed) {
93 | toGo = containsRoute ? route : stateRouteMap.default;
94 | } else {
95 | toGo = containsRoute ? stateRouteMap.default : route;
96 | }
97 |
98 | return toGo;
99 | };
100 |
101 | var currentView, // Represents the current view
102 | photos; // Collection of photos
103 |
104 | // The router.
105 | var Router = Backbone.Router.extend({
106 |
107 | routes: {
108 | '': 'photos',
109 | 'photos': 'photos',
110 | 'login': 'login',
111 | 'logout': 'logout',
112 | 'register': 'register',
113 | 'forgotpassword': 'forgotPassword',
114 | 'changepassword': 'changePassword',
115 | 'changesecurityinfo': 'changesecurityinfo',
116 | 'photos/:id': 'photo',
117 | 'add': 'add',
118 | 'edit/:id': 'edit',
119 | 'info': 'info',
120 | 'settings': 'settings'
121 | },
122 |
123 | // Fetch the photos collection and render the view.
124 | photos: function () {
125 | this.getPhotos()
126 | .done(_.bind(function () {
127 | this.renderView(new HomeView({
128 | model: new Backbone.Model({
129 | search: '',
130 | total: photos.length
131 | }),
132 | collection: photos
133 | }));
134 | }, this))
135 | .fail(function () {
136 | notification.alert('Failed to retrieve photos. Please try again.', 'Error', 'Ok');
137 | });
138 | },
139 |
140 | // Renders the login view.
141 | login: function () {
142 | this.renderView(new LoginView({
143 | model: new Credential(),
144 | credential: new Credential({
145 | id: 'Safe-Credential'
146 | })
147 | }));
148 | },
149 |
150 | // Logs out the user and redirect to login.
151 | logout: function () {
152 | session.clear();
153 | this.navigate('#login', true);
154 | },
155 |
156 | // Renders the register view.
157 | register: function () {
158 | this.renderView(new RegisterView({
159 | model: new Credential({
160 | id: 'Safe-Credential'
161 | }),
162 | securityQuestions: Data.securityQuestions
163 | }));
164 | },
165 |
166 | // Renders the forgot password view.
167 | forgotPassword: function () {
168 | var credential = new Credential({
169 | id: 'Safe-Credential'
170 | });
171 |
172 | credential.fetch()
173 | .done(_.bind(function () {
174 | this.renderView(new ForgotPasswordView({
175 | model: new Credential({
176 | securityQuestion: credential.get('securityQuestion')
177 | }),
178 | credential: credential
179 | }));
180 | }, this))
181 | .fail(function () {
182 | notification.alert('Failed to retrieve security info. Please try again.', 'Error', 'Ok');
183 | });
184 | },
185 |
186 | // Renders the change password view.
187 | changePassword: function () {
188 | this.renderView(new ChangePasswordView({
189 | model: new Credential(),
190 | credential: new Credential({
191 | id: 'Safe-Credential'
192 | })
193 | }));
194 | },
195 |
196 | changesecurityinfo: function () {
197 | this.renderView(new ChangeSecurityInfoView({
198 | model: new Credential(),
199 | credential: new Credential({
200 | id: 'Safe-Credential'
201 | }),
202 | securityQuestions: Data.securityQuestions
203 | }));
204 | },
205 |
206 | // Renders the photo view.
207 | photo: function (id) {
208 | this.getPhotos()
209 | .done(_.bind(function () {
210 | var photo = photos.get(id);
211 |
212 | if (!photo) {
213 | notification.alert('Photo not exists.', 'Error', 'Ok');
214 | return;
215 | }
216 |
217 | this.renderView(new PhotoView({
218 | model: photo,
219 | file: new File({
220 | id: id
221 | })
222 | }));
223 | }, this))
224 | .fail(function () {
225 | notification.alert('Failed to retrieve photo. Please try again.', 'Error', 'Ok');
226 | });
227 | },
228 |
229 | // Renders the view to add photo.
230 | add: function () {
231 | this.getPhotos()
232 | .done(_.bind(function () {
233 | this.renderView(new AddEditView({
234 | model: new Photo(),
235 | file: new File(),
236 | collection: photos
237 | }));
238 | }, this))
239 | .fail(function () {
240 | notification.alert('Error occured while retrieving the associated collection. Please try again.', 'Error', 'Ok');
241 | });
242 | },
243 |
244 | // Gets the photo model and renders the view to edit photo.
245 | edit: function (id) {
246 | this.getPhotos()
247 | .done(_.bind(function () {
248 | var photo = photos.get(id);
249 |
250 | if (!photo) {
251 | notification.alert('Photo not exists.', 'Error', 'Ok');
252 | return;
253 | }
254 |
255 | this.renderView(new AddEditView({
256 | model: photo,
257 | file: new File({
258 | id: id
259 | })
260 | }));
261 | }, this))
262 | .fail(function () {
263 | notification.alert('Failed to retrieve photo. Please try again.', 'Error', 'Ok');
264 | });
265 | },
266 |
267 | // Renders the info view.
268 | info: function () {
269 | this.renderView(new InfoView());
270 | },
271 |
272 | // Renders the settings view.
273 | settings: function () {
274 | this.renderView(new SettingsView());
275 | },
276 |
277 | getPhotos: function () {
278 | var d = $.Deferred();
279 |
280 | if (photos) {
281 | photos.sort();
282 | d.resolve(photos);
283 | } else {
284 | photos = new Photos();
285 | photos.fetch().done(d.resolve).fail(d.reject);
286 | }
287 |
288 | return d.promise();
289 | },
290 |
291 | renderView: function (view) {
292 | if (currentView) {
293 | currentView.ghost();
294 | }
295 |
296 | currentView = view;
297 | currentView.setElement($('body'));
298 | currentView.render();
299 | },
300 |
301 | // Override the 'route' method to prevent unauthorized access.
302 | // before directing the routes to their corresponding methods.
303 | // Ref: http://stackoverflow.com/questions/13970699/backbone-js-force-to-login-route
304 | route: function (route, name, callback) {
305 |
306 | if (!_.isRegExp(route)) {
307 | route = this._routeToRegExp(route);
308 | }
309 |
310 | if (_.isFunction(name)) {
311 | callback = name;
312 | name = '';
313 | }
314 |
315 | if (!callback) {
316 | callback = this[name];
317 | }
318 |
319 | // here my custom code
320 | callback = _.wrap(callback, _.bind(function (cb, args) {
321 |
322 | var state = session.retrieve('state'),
323 | fn = function () {
324 | if (state === 'VERIFIED') {
325 | session.store('state', 'LOGGED_OUT');
326 | }
327 |
328 | // Get the page the user should navigate to.
329 | // 'name' is the handler of the particular route.
330 | var goto = whereToGo(state, name.toLowerCase());
331 |
332 | if (goto.toLowerCase() === name.toLowerCase()) {
333 | _.bind(cb, this)(args);
334 | } else {
335 | this.navigate(goto, true);
336 | }
337 | };
338 |
339 | if (!state) {
340 | state = keychain.containsKey('Safe-Credential') ? 'LOGGED_OUT' : 'NOT_REGISTERED';
341 | session.store('state', state);
342 | }
343 |
344 | fn.call(this);
345 | }, this));
346 |
347 | var router = this;
348 |
349 | Backbone.history.route(route, function (fragment) {
350 | var args = router._extractParameters(route, fragment);
351 |
352 | if (router.execute(callback, args, name) !== false) {
353 | router.trigger.apply(router, ['route:' + name].concat(args));
354 | router.trigger('route', name, args);
355 | Backbone.history.trigger('route', router, name, args);
356 | }
357 | });
358 |
359 | return this;
360 | }
361 | });
362 |
363 | return new Router();
364 | });
365 |
--------------------------------------------------------------------------------
/src/js/serializer.js:
--------------------------------------------------------------------------------
1 | // Serializes javascript object to string and vice-versa.
2 | define([
3 | 'jquery',
4 | 'underscore',
5 | 'settings',
6 | 'session',
7 | 'adapters/crypto'
8 | ], function (
9 | $,
10 | _,
11 | settings,
12 | session,
13 | crypto
14 | ) {
15 |
16 | 'use strict';
17 |
18 | return {
19 | serialize: function (item) {
20 | var result = JSON.stringify(item);
21 |
22 | if (settings.encrypt) {
23 | return crypto.encrypt(session.retrieve('key'), result);
24 | }
25 |
26 | return _.resolve(result);
27 | },
28 |
29 | deserialize: function (data) {
30 | var d = $.Deferred();
31 |
32 | if (settings.encrypt) {
33 | crypto.decrypt(session.retrieve('key'), data)
34 | .done(function (result) {
35 | d.resolve(JSON.parse(result));
36 | })
37 | .fail(d.reject);
38 | } else {
39 | d.resolve(JSON.parse(data));
40 | }
41 |
42 | return d.promise();
43 | }
44 | };
45 | });
46 |
--------------------------------------------------------------------------------
/src/js/session.js:
--------------------------------------------------------------------------------
1 | // Session module based on HTML5 sessionStorage.
2 | define(function() {
3 |
4 | 'use strict';
5 |
6 | return {
7 | retrieve: function(key) {
8 | return window.sessionStorage.getItem(key);
9 | },
10 |
11 | store: function(key, value) {
12 | window.sessionStorage.setItem(key, value);
13 | },
14 |
15 | remove: function(key) {
16 | window.sessionStorage.removeItem(key);
17 | },
18 |
19 | clear: function() {
20 | window.sessionStorage.clear();
21 | }
22 | };
23 | });
24 |
--------------------------------------------------------------------------------
/src/js/settings.js:
--------------------------------------------------------------------------------
1 | // Contains all the configuration settings of the app.
2 | define(function() {
3 |
4 | 'use strict';
5 |
6 | return {
7 | encrypt: true, // Settings this to false will not encrypt the photos, not a good idea!
8 | encDecKey: 'WW91clNlY3JldElzU2FmZVdpdGhNZQ==', // Encryption key for credential. Don't reveal this to anyone!
9 | targetWidth: 320, // Width in pixels to scale image. Must be used with targetHeight. Aspect ratio remains constant. (Number)
10 | targetHeight: 480 // Height in pixels to scale image. Must be used with targetWidth. Aspect ratio remains constant. (Number)
11 | };
12 | });
13 |
--------------------------------------------------------------------------------
/src/js/templates.js:
--------------------------------------------------------------------------------
1 | define(['handlebars'], function(Handlebars) {
2 |
3 | this["JST"] = this["JST"] || {};
4 |
5 | this["JST"]["addedit"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
6 | var helper;
7 |
8 | return " \n";
11 | },"3":function(depth0,helpers,partials,data) {
12 | return " \n";
13 | },"5":function(depth0,helpers,partials,data) {
14 | return " Edit Photo\n";
15 | },"7":function(depth0,helpers,partials,data) {
16 | return " Add Photo\n";
17 | },"9":function(depth0,helpers,partials,data) {
18 | var helper;
19 |
20 | return " \n";
23 | },"11":function(depth0,helpers,partials,data) {
24 | return " \n";
25 | },"13":function(depth0,helpers,partials,data) {
26 | return " \n";
27 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
28 | var stack1;
29 |
30 | return "\n"
31 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.id : depth0),{"name":"if","hash":{},"fn":this.program(1, data, 0),"inverse":this.program(3, data, 0),"data":data})) != null ? stack1 : "")
32 | + " \n \n \n \n \n \n"
33 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.id : depth0),{"name":"if","hash":{},"fn":this.program(5, data, 0),"inverse":this.program(7, data, 0),"data":data})) != null ? stack1 : "")
34 | + " \n\n\n\n \n \n Home \n \n \n \n Settings \n \n \n \n Info \n \n \n\n\n";
39 | },"useData":true});
40 |
41 | this["JST"]["changepassword"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
42 | return " \n \n Home \n \n \n \n Settings \n \n \n \n Info \n \n";
43 | },"3":function(depth0,helpers,partials,data) {
44 | return " \n \n Info \n \n";
45 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
46 | var stack1, helper;
47 |
48 | return "\n \n \n \n Change Password \n \n\n\n"
51 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.isAuthenticated : depth0),{"name":"if","hash":{},"fn":this.program(1, data, 0),"inverse":this.program(3, data, 0),"data":data})) != null ? stack1 : "")
52 | + " \n\n\n \n
\n";
53 | },"useData":true});
54 |
55 | this["JST"]["changesecurityinfo"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
56 | return " "
57 | + this.escapeExpression(this.lambda(depth0, depth0))
58 | + " \n";
59 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
60 | var stack1;
61 |
62 | return "\n \n \n \n Change Security Info \n \n\n\n \n \n Home \n \n \n \n Settings \n \n \n \n Info \n \n \n\n\n \n
\n";
65 | },"useData":true});
66 |
67 | this["JST"]["forgotpassword"] = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
68 | var helper;
69 |
70 | return "\r\n\r\n\r\n \r\n
\r\n\r\n\r\n \r\n \r\n Info \r\n \r\n ";
73 | },"useData":true});
74 |
75 | this["JST"]["home"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
76 | return " \r\n \r\n
\r\n";
77 | },"3":function(depth0,helpers,partials,data) {
78 | var helper;
79 |
80 | return " "
81 | + this.escapeExpression(((helper = (helper = helpers.total || (depth0 != null ? depth0.total : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0,{"name":"total","hash":{},"data":data}) : helper)))
82 | + " photo(s) found ";
83 | },"5":function(depth0,helpers,partials,data) {
84 | return " No photos found ";
85 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
86 | var stack1;
87 |
88 | return "\r\n\r\n\r\n \r\n \r\n Home \r\n \r\n \r\n \r\n Settings \r\n \r\n \r\n \r\n Info \r\n \r\n \r\n\r\n\r\n"
89 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.total : depth0),{"name":"if","hash":{},"fn":this.program(1, data, 0),"inverse":this.noop,"data":data})) != null ? stack1 : "")
90 | + "\r\n
\r\n "
91 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.total : depth0),{"name":"if","hash":{},"fn":this.program(3, data, 0),"inverse":this.program(5, data, 0),"data":data})) != null ? stack1 : "")
92 | + "\r\n
\r\n\r\n
\r\n
\r\n
\r\n";
93 | },"useData":true});
94 |
95 | this["JST"]["info"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
96 | var helper;
97 |
98 | return " \r\n \r\n \r\n";
101 | },"3":function(depth0,helpers,partials,data) {
102 | return " \r\n \r\n Home \r\n \r\n \r\n \r\n Settings \r\n \r\n";
103 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
104 | var stack1;
105 |
106 | return "\r\n"
107 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.backToUrl : depth0),{"name":"if","hash":{},"fn":this.program(1, data, 0),"inverse":this.noop,"data":data})) != null ? stack1 : "")
108 | + " Info \r\n \r\n\r\n\r\n"
109 | + ((stack1 = helpers['if'].call(depth0,(depth0 != null ? depth0.isAuthenticated : depth0),{"name":"if","hash":{},"fn":this.program(3, data, 0),"inverse":this.noop,"data":data})) != null ? stack1 : "")
110 | + " \r\n \r\n Info \r\n \r\n \r\n\r\n\r\n
\r\n
Safe \r\n
\r\n A simple app that helps to preserve your photos safe! The persisted photos are encrypted using the powerful AES 256 encryption algorithm.\r\n
\r\n
\r\n This app is an experiment done to demonstrate how we can architect and develop a pure hybrid application that runs across mobile platforms using Cordova, Backbone and other UI technologies.\r\n
\r\n
\r\n This application is developed purely for educational purpose. You can download the complete source code from Github . You are free to use the source code as a reference material to architect your mobile apps for personal or commercial purposes.
\r\n\r\n
You are not allowed to sell this app in same or different name. You are also not allowed to build and sell apps by copying maximum portion of the source code. For more information about license please visit this page .\r\n
\r\n\r\n
\r\n As I said earlier, this is an experiment! If you run into any issues of using this app or source code the complete responsibility is yours! For any questions please contact me here .\r\n
\r\n
\r\n
\r\n";
111 | },"useData":true});
112 |
113 | this["JST"]["login"] = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
114 | return "\r\n\r\n\r\n \r\n \r\n Info \r\n \r\n \r\n\r\n\r\n
\r\n Keep your photos safe!\r\n
\r\n\r\n
\r\n
";
115 | },"useData":true});
116 |
117 | this["JST"]["photo"] = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
118 | var helper, alias1=helpers.helperMissing, alias2="function", alias3=this.escapeExpression;
119 |
120 | return "\n\n\n \n \n Home \n \n \n \n Settings \n \n \n \n Info \n \n \n\n\n
\n
"
123 | + alias3(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : alias1),(typeof helper === alias2 ? helper.call(depth0,{"name":"description","hash":{},"data":data}) : helper)))
124 | + "
\n
\n
\n
\n";
127 | },"useData":true});
128 |
129 | this["JST"]["photoitem"] = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
130 | var helper, alias1=helpers.helperMissing, alias2="function", alias3=this.escapeExpression;
131 |
132 | return "\r\n \r\n \r\n "
137 | + alias3(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : alias1),(typeof helper === alias2 ? helper.call(depth0,{"name":"description","hash":{},"data":data}) : helper)))
138 | + "\r\n
\r\n ";
139 | },"useData":true});
140 |
141 | this["JST"]["register"] = Handlebars.template({"1":function(depth0,helpers,partials,data) {
142 | return " "
143 | + this.escapeExpression(this.lambda(depth0, depth0))
144 | + " \n";
145 | },"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
146 | var stack1;
147 |
148 | return "\n\n\n \n \n Info \n \n \n\n\n
\n Keep your photos safe!\n
\n\n
\n
\n";
151 | },"useData":true});
152 |
153 | this["JST"]["settings"] = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
154 | return "\r\n\r\n\r\n \r\n \r\n Home \r\n \r\n \r\n \r\n Settings \r\n \r\n \r\n \r\n Info \r\n \r\n \r\n\r\n";
155 | },"useData":true});
156 |
157 | return this["JST"];
158 |
159 | });
--------------------------------------------------------------------------------
/src/js/views/addedit.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'underscore',
3 | 'backbone',
4 | 'adapters/camera',
5 | 'adapters/imageresizer',
6 | 'adapters/crypto',
7 | 'adapters/persistentfilestorage',
8 | 'adapters/notification',
9 | 'templates'
10 | ], function (
11 | _,
12 | Backbone,
13 | camera,
14 | imageresizer,
15 | crypto,
16 | persistentfilestorage,
17 | notification,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | // Add/edit view.
24 | // Renders the view to add/edit photo. Handles all the UI logic associated with it.
25 | var AddEdit = Backbone.View.extend({
26 |
27 | // Set the template.
28 | template: templates.addedit,
29 |
30 | // Hook handlers to events.
31 | events: {
32 | 'click #addphoto': 'addPhoto',
33 | 'click #save': 'submit',
34 | 'submit #addedit-form': 'save',
35 | 'click #delete': 'delete'
36 | },
37 |
38 | // Input elements-model bindings.
39 | bindings: {
40 | '#description': 'description'
41 | },
42 |
43 | initialize: function (options) {
44 |
45 | // If model is not passed throw error.
46 | if (!this.model) {
47 | throw new Error('model is required');
48 | }
49 |
50 | // If file model is not passed throw error.
51 | if (!(options && options.file)) {
52 | throw new Error('file is required');
53 | }
54 |
55 | this.file = options.file;
56 |
57 | // A flag to store whether the view is rendered to add or edit photo.
58 | this.isEdit = this.model.id ? true : false;
59 |
60 | // If collection is not passed for add throw error.
61 | if(!this.isEdit && !this.collection) {
62 | throw new Error('collection is required');
63 | }
64 |
65 | // Call the enable 'done' button handler when user selects a photo.
66 | this.listenTo(this.file, 'change:data', this.updateImage);
67 |
68 | // On change of the model, validate the model and enable the 'done' button.
69 | this.listenTo(this.model, 'change', this.enableDone);
70 | },
71 |
72 | render: function () {
73 | // Render the view.
74 | this.$el.html(this.template(this.model.toJSON()));
75 |
76 | // Set the bindings.
77 | this.stickit();
78 |
79 | // Cache the DOM elements for easy access/manipulation.
80 | this.$save = this.$('#save');
81 | this.$photo = this.$('#photo');
82 | this.$delete = this.$('#delete');
83 | this.$form = this.$('#addedit-form');
84 |
85 | // If edit, load the image!
86 | if (this.isEdit) {
87 | this.file.fetch()
88 | .fail(function () {
89 | notification.alert('Failed to load image. Please try again.', 'Error', 'Ok');
90 | });
91 | }
92 |
93 | return this;
94 | },
95 |
96 | // add photo event handler.
97 | addPhoto: function (e) {
98 | e.preventDefault();
99 |
100 | camera.getPictureFromLibrary()
101 | .done(_.bind(function (base64image) {
102 | this.file.set('data', base64image);
103 | this.createThumbnail();
104 | }, this));
105 | },
106 |
107 | submit: function () {
108 | this.$form.submit();
109 | },
110 |
111 | // Save the photo model.
112 | save: function (e) {
113 | e.preventDefault();
114 |
115 | // If the models are not valid display error.
116 | if (!(this.model.isValid(true) && this.file.isValid(true))) {
117 | notification.alert('Some of the required fields are missing.', 'Error', 'Ok');
118 | return;
119 | }
120 |
121 | // Disable the button to avoid saving multiple times.
122 | this.$save.attr('disabled', 'disabled');
123 |
124 | // Update the last saved date.
125 | this.model.set('lastSaved', Date(), {
126 | silent: true
127 | });
128 |
129 | // Function to save the file in file-system.
130 | var saveFile = function () {
131 |
132 | // If it's in edit model set the 'id' parameter.
133 | if (!this.isEdit) {
134 | this.file.set('id', this.model.id, {
135 | silent: true
136 | });
137 | }
138 |
139 | // Write the base64 content into the file system.
140 | this.file.save()
141 | .done(_.bind(function () {
142 | this.navigateTo(this.isEdit ? '#photos/' + this.model.id : '#photos');
143 | }, this))
144 | .fail(_.bind(error, this));
145 | };
146 |
147 | // Common error function.
148 | var error = function () {
149 | notification.alert('Failed to save photo. Please try again.', 'Error', 'Ok')
150 | .done(_.bind(function () {
151 | this.saving = false;
152 | this.$save.removeAttr('disabled');
153 | }, this));
154 | };
155 |
156 | this.saving = true;
157 |
158 | // If it's edit save else create.
159 | if (this.model.id) {
160 | this.model.save()
161 | .done(_.bind(saveFile, this))
162 | .fail(_.bind(error, this));
163 | } else {
164 | this.model.localStorage = this.collection.localStorage;
165 | this.collection.create(this.model, {
166 | wait: true,
167 | success: _.bind(saveFile, this),
168 | error: _.bind(error, this)
169 | });
170 | }
171 | },
172 |
173 | // Delete the model and the file.
174 | delete: function () {
175 | this.$delete.attr('disabled', 'disabled');
176 |
177 | notification.confirm('Are you sure want to delete this photo?')
178 | .done(_.bind(function (r) {
179 |
180 | if (r === 1) {
181 | var deleteError = function () {
182 | notification.alert('Failed to delete. Please try again.', 'Error', 'Ok')
183 | .done(_.bind(function () {
184 | this.$delete.removeAttr('disabled');
185 | }, this));
186 | };
187 |
188 | this.file.destroy()
189 | .then(_.bind(function () {
190 | return this.model.destroy();
191 | }, this), _.bind(deleteError, this))
192 | .then(_.bind(function () {
193 | this.navigateTo('#photos');
194 | }, this), _.bind(deleteError, this));
195 | } else {
196 | this.$delete.removeAttr('disabled');
197 | }
198 | }, this));
199 | },
200 |
201 | // Update the image tag.
202 | updateImage: function () {
203 | this.$photo.attr('src', 'data:image/png;base64,' + this.file.get('data'));
204 | },
205 |
206 | // Resize the image and create thumbnail.
207 | createThumbnail: function () {
208 | var imageData = this.file.get('data'),
209 | resizeWidth = 42;
210 |
211 | imageresizer.resize(imageData, resizeWidth, 0)
212 | .done(_.bind(function (resizedImage) {
213 | this.model.set('thumbnail', resizedImage.imageData);
214 | }, this))
215 | .fail(function () {
216 | notification.alert('Failed to create thumbnail.', 'Error', 'Ok');
217 | });
218 | },
219 |
220 | // Enable the 'done' button if the models are valid.
221 | enableDone: function () {
222 | if (!this.saving && this.model.isValid(true) && this.file.isValid(true)) {
223 | this.$save.removeAttr('disabled');
224 | } else {
225 | this.$save.attr('disabled', 'disabled');
226 | }
227 | }
228 | });
229 |
230 | return AddEdit;
231 | });
--------------------------------------------------------------------------------
/src/js/views/changepassword.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'backbone',
3 | 'underscore',
4 | 'adapters/notification',
5 | 'session',
6 | 'templates'
7 | ], function (
8 | Backbone,
9 | _,
10 | notification,
11 | session,
12 | templates
13 | ) {
14 |
15 | 'use strict';
16 |
17 | // Change password view.
18 | // Renders the view to change password. Handles all the UI logic associated with it.
19 | var ChangePasswordView = Backbone.View.extend({
20 |
21 | // Set the template.
22 | template: templates.changepassword,
23 |
24 | // Wire-up the handlers for DOM events.
25 | events: {
26 | 'submit #changepassword-form': 'changePassword'
27 | },
28 |
29 | // Set the bindings.
30 | bindings: {
31 | '#password': 'password'
32 | },
33 |
34 | initialize: function (options) {
35 |
36 | // If model is not passed throw error.
37 | if (!this.model) {
38 | throw new Error('model is required');
39 | }
40 |
41 | // If credential is not passed throw error.
42 | if(!options.credential) {
43 | throw new Error('credential is required');
44 | }
45 |
46 | // Store the passed credential in a property.
47 | this.credential = options.credential;
48 |
49 | // Enable/disable the change button whenever password changes.
50 | this.listenTo(this.model, 'change:password', this.enableChange);
51 | },
52 |
53 | render: function () {
54 | var state = session.retrieve('state');
55 |
56 | this.$el.html(this.template({
57 | isAuthenticated: state === 'LOGGED_IN',
58 | backToUrl: state === 'LOGGED_IN' ? '#settings' : '#login'
59 | }));
60 |
61 | this.stickit();
62 |
63 | this.$change = this.$('#change');
64 |
65 | return this;
66 | },
67 |
68 | // Enable the change button when password text is valid.
69 | enableChange: function () {
70 | if (this.model.isValid('password')) {
71 | this.$change.removeAttr('disabled');
72 | } else {
73 | this.$change.attr('disabled', 'disabled');
74 | }
75 | },
76 |
77 | // Change button event handler.
78 | changePassword: function (e) {
79 | e.preventDefault();
80 |
81 | // Alert the user if the form is submitted with empty password.
82 | if (!this.model.isValid('password')) {
83 | notification.alert('Enter the password.', 'Error', 'Ok');
84 | return;
85 | }
86 |
87 | // Disable the change button.
88 | this.$change.attr('disabled', 'disabled');
89 |
90 | var error = function () {
91 | notification.alert('Failed to change password. Please try again.', 'Error', 'Ok')
92 | .done(_.bind(function () {
93 | this.$change.removeAttr('disabled');
94 | }, this));
95 | };
96 |
97 | // Fetch the credential, set the new password, save and navigate to login page.
98 | this.credential.fetch()
99 | .then(_.bind(function () {
100 | this.credential.set('password', this.model.get('password'));
101 | return this.credential.save();
102 | }, this), _.bind(error, this))
103 | .then(_.bind(function () {
104 | session.store('state', 'LOGGED_OUT');
105 | session.remove('key');
106 | this.navigateTo('#login');
107 | }, this), _.bind(error, this));
108 | }
109 | });
110 |
111 | return ChangePasswordView;
112 | });
--------------------------------------------------------------------------------
/src/js/views/changesecurityinfo.js:
--------------------------------------------------------------------------------
1 | define(['backbone', 'underscore', 'adapters/notification', 'templates'], function (Backbone, _, notification, templates) {
2 |
3 | 'use strict';
4 |
5 | // Change security info view.
6 | // Renders the view to change the security question and answer.
7 | // Handles all the UI logic.
8 | var ChangeSecurityInfoView = Backbone.View.extend({
9 |
10 | // Set the template.
11 | template: templates.changesecurityinfo,
12 |
13 | // Wire-up the handlers for DOM events.
14 | events: {
15 | 'submit #changesecurityinfo-form': 'changeSecurityInfo'
16 | },
17 |
18 | // Set the bindings.
19 | bindings: {
20 | '#security-question': 'securityQuestion',
21 | '#security-answer': 'securityAnswer'
22 | },
23 |
24 | initialize: function (options) {
25 |
26 | // If model is not passes throw error.
27 | if (!this.model) {
28 | throw new Error('model is required');
29 | }
30 |
31 | // If credential is not passed throw error.
32 | if (!options.credential) {
33 | throw new Error('credential is required');
34 | }
35 |
36 | // If not security questions passed throw error.
37 | if (!(options && options.securityQuestions && options.securityQuestions.length)) {
38 | throw new Error('security questions required');
39 | }
40 |
41 | // Store the passed credential in a property.
42 | this.credential = options.credential;
43 |
44 | // Store the passed questions in a property.
45 | this.securityQuestions = options.securityQuestions;
46 |
47 | // Listen to model change events to enable/disable controls.
48 | this.listenTo(this.model, 'change:securityQuestion', this.enableAnswer);
49 | this.listenTo(this.model, 'change:securityAnswer', this.enableSave);
50 | },
51 |
52 | render: function () {
53 | // We need to pass the security questions also to the view, so let's create a viewmodel.
54 | var viewModel = this.model.toJSON();
55 | viewModel.questions = this.securityQuestions;
56 |
57 | this.$el.html(this.template(viewModel));
58 | this.stickit();
59 |
60 | this.$securityAnswer = this.$('#security-answer');
61 | this.$save = this.$('#save');
62 |
63 | return this;
64 | },
65 |
66 | enableAnswer: function () {
67 | this.model.set('securityAnswer', null);
68 |
69 | if (this.model.get('securityQuestion') && this.model.get('securityQuestion') !== 'Select a question') {
70 | this.$securityAnswer.removeAttr('disabled');
71 | } else {
72 | this.$securityAnswer.attr('disabled', 'disabled');
73 | }
74 | },
75 |
76 | enableSave: function () {
77 | if (this.model.isValid('securityQuestion') && this.model.isValid('securityAnswer')) {
78 | this.$save.removeAttr('disabled');
79 | } else {
80 | this.$save.attr('disabled', 'disabled');
81 | }
82 | },
83 |
84 | // Save button event handler.
85 | changeSecurityInfo: function (e) {
86 |
87 | e.preventDefault();
88 |
89 | // Alert the user if the form is submitted with empty question or answer.
90 | if (!(this.model.isValid('securityQuestion') && this.model.isValid('securityAnswer'))) {
91 | notification.alert('Submit security question with answer.', 'Error', 'Ok');
92 | return;
93 | }
94 |
95 | // Disable the save button to avoid multiple submits.
96 | this.$save.attr('disabled', 'disabled');
97 |
98 | var error = function () {
99 | notification.alert('Failed to change security info. Please try again.', 'Error', 'Ok')
100 | .done(_.bind(function () {
101 | this.$save.removeAttr('disabled');
102 | }, this));
103 | };
104 |
105 | // Fetch the credential, set the security question/answer, save and navigate to settings page.
106 | this.credential.fetch()
107 | .then(_.bind(function () {
108 | this.credential.set({
109 | securityQuestion: this.model.get('securityQuestion'),
110 | securityAnswer: this.model.get('securityAnswer')
111 | });
112 | return this.credential.save();
113 | }, this), _.bind(error, this))
114 | .then(_.bind(function () {
115 | this.navigateTo('#settings');
116 | }, this), _.bind(error, this));
117 | }
118 | });
119 |
120 | return ChangeSecurityInfoView;
121 | });
--------------------------------------------------------------------------------
/src/js/views/forgotpassword.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'backbone',
3 | 'underscore',
4 | 'adapters/notification',
5 | 'session',
6 | 'templates'
7 | ], function (
8 | Backbone,
9 | _,
10 | notification,
11 | session,
12 | templates
13 | ) {
14 |
15 | 'use strict';
16 |
17 | // Forgot password view.
18 | // Renders the view for forgot password and handles the UI actions.
19 | var ForgotPasswordView = Backbone.View.extend({
20 |
21 | // Set the template.
22 | template: templates.forgotpassword,
23 |
24 | // Wire handlers for DOM events.
25 | events: {
26 | 'submit #forgotpassword-form': 'verifyAnswer'
27 | },
28 |
29 | // Set the bindings.
30 | bindings: {
31 | '#security-answer': 'securityAnswer'
32 | },
33 |
34 | initialize: function (options) {
35 | // if model is not passed throw error.
36 | if (!this.model) {
37 | throw new Error('model is required');
38 | }
39 |
40 | // if the persisted credential is not passed throw error.
41 | if (!(options && options.credential)) {
42 | throw new Error('credential is required');
43 | }
44 |
45 | // Set the passed credential to a property.
46 | this.credential = options.credential;
47 |
48 | // Hook the handler to enable/disable submit button when security answer changes.
49 | this.listenTo(this.model, 'change:securityAnswer', this.enableSubmit);
50 | },
51 |
52 | // Get the security question and render the view.
53 | render: function () {
54 | this.$el.html(this.template(this.model.toJSON()));
55 | this.stickit();
56 |
57 | this.$verify = this.$('#verify');
58 |
59 | return this;
60 | },
61 |
62 | // Clear the answer textbox and enable it if the selected question is valid.
63 | enableSubmit: function () {
64 | if (this.model.isValid('securityAnswer')) {
65 | this.$verify.removeAttr('disabled');
66 | } else {
67 | this.$verify.attr('disabled', 'disabled');
68 | }
69 | },
70 |
71 | // Verify button event handler.
72 | verifyAnswer: function (e) {
73 | e.preventDefault();
74 |
75 | // Alert the user if the form is submitted with empty answer.
76 | if (!this.model.isValid('securityAnswer')) {
77 | notification.alert('Enter the answer.', 'Error', 'Ok');
78 | return;
79 | }
80 |
81 | // Disable the button to prevent multiple clicks.
82 | this.$verify.attr('disabled', 'disabled');
83 |
84 | // Verify the answer and on success set the state variable to 'VERIFIED' and navigate to change password page.
85 | if (this.model.get('securityAnswer') === this.credential.get('securityAnswer')) {
86 | session.store('state', 'VERIFIED');
87 | this.navigateTo('#changepassword');
88 | } else {
89 | notification.alert('Invalid answer.', 'Error', 'Ok')
90 | .done(_.bind(function () {
91 | this.$verify.removeAttr('disabled');
92 | }, this));
93 | }
94 | }
95 | });
96 |
97 | return ForgotPasswordView;
98 | });
--------------------------------------------------------------------------------
/src/js/views/home.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'underscore',
3 | 'backbone',
4 | 'adapters/notification',
5 | 'views/photos',
6 | 'templates'
7 | ], function (
8 | _,
9 | Backbone,
10 | notification,
11 | PhotosView,
12 | templates
13 | ) {
14 |
15 | 'use strict';
16 |
17 | // Home view.
18 | // Renders the home view that displays the list of all secrets and handles the UI logic associated with it.
19 | var Home = Backbone.View.extend({
20 |
21 | // Set the template.
22 | template: templates.home,
23 |
24 | // Set the bindings.
25 | bindings: {
26 | '#search': 'search'
27 | },
28 |
29 | initialize: function () {
30 |
31 | // Throw error if photos collection not passed.
32 | if(!this.collection) {
33 | throw new Error('collection is required');
34 | }
35 |
36 | // Throw error if model is not passed.
37 | if(!this.model) {
38 | throw new Error('model is required');
39 | }
40 |
41 | // We need this collection because of search.
42 | this.filtered = new Backbone.Collection(this.collection.models);
43 |
44 | // Instantiate the child view that displays the photos list passing the collection.
45 | this.listView = new PhotosView({
46 | collection: this.filtered
47 | });
48 |
49 | // Run search whenever user type or change text in the search textbox.
50 | this.listenTo(this.model, 'change:search', this.searchPhotos);
51 |
52 | // Update the count label.
53 | this.listenTo(this.model, 'change:total', this.updateLabel);
54 | },
55 |
56 | render: function () {
57 | // Render the outer container.
58 | this.$el.html(this.template(this.model.toJSON()));
59 |
60 | // Bind the model props to DOM.
61 | this.stickit();
62 |
63 | // Render the child list view.
64 | this.$('#list-container').append(this.listView.render().el);
65 |
66 | // Cache the DOM els for quick access.
67 | this.$total = this.$('#total');
68 |
69 | return this;
70 | },
71 |
72 | searchPhotos: function () {
73 | var filteredCollection = this.collection.search((this.model.get('search') || '').trim());
74 | this.model.set('total', filteredCollection.length);
75 | this.filtered.reset(filteredCollection);
76 | },
77 |
78 | updateLabel: function () {
79 | var total = this.model.get('total'),
80 | message = '';
81 |
82 | if (total > 0) {
83 | message = total + ' photo(s) found';
84 | } else {
85 | message = 'No photos found';
86 | }
87 |
88 | this.$total.html(message);
89 | }
90 | });
91 |
92 | return Home;
93 | });
--------------------------------------------------------------------------------
/src/js/views/info.js:
--------------------------------------------------------------------------------
1 | define(['underscore', 'backbone', 'session', 'templates'], function (_, Backbone, session, templates) {
2 |
3 | 'use strict';
4 |
5 | // Info view.
6 | var InfoView = Backbone.View.extend({
7 |
8 | template: templates.info,
9 |
10 | render: function () {
11 | var isAuthenticated = session.retrieve('state') === 'LOGGED_IN';
12 |
13 | this.$el.html(this.template({
14 | isAuthenticated: isAuthenticated,
15 | backToUrl: !isAuthenticated ? '#login' : null
16 | }));
17 |
18 | return this;
19 | }
20 | });
21 |
22 | return InfoView;
23 | });
--------------------------------------------------------------------------------
/src/js/views/login.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'backbone',
3 | 'underscore',
4 | 'adapters/notification',
5 | 'session',
6 | 'templates'
7 | ], function (
8 | Backbone,
9 | _,
10 | notification,
11 | session,
12 | templates
13 | ) {
14 |
15 | 'use strict';
16 |
17 | // Login view.
18 | // Renders the login view and handles the UI logic.
19 | var LoginView = Backbone.View.extend({
20 |
21 | // Set the template to render the markup.
22 | template: templates.login,
23 |
24 | // Wire-up handlers for DOM events.
25 | events: {
26 | 'submit #login-form': 'login'
27 | },
28 |
29 | // Set the bindings for input elements with model properties.
30 | bindings: {
31 | '#password': 'password'
32 | },
33 |
34 | initialize: function (options) {
35 |
36 | // if model is not passed throw error.
37 | if (!this.model) {
38 | throw new Error('model is required');
39 | }
40 |
41 | // if the persisted credential is not passed throw error.
42 | if (!(options && options.credential)) {
43 | throw new Error('credential is required');
44 | }
45 |
46 | // Store the actual credential in some property for easy access later.
47 | this.credential = options.credential;
48 |
49 | // Enable/disable the login button whenever the password changes.
50 | this.listenTo(this.model, 'change:password', this.enableLogin);
51 | },
52 |
53 | render: function () {
54 | // Render the view.
55 | this.$el.html(this.template());
56 |
57 | // Bind input elements with model properties.
58 | this.stickit();
59 |
60 | // Cache DOM elements for easy access.
61 | this.$login = this.$('#login');
62 |
63 | return this;
64 | },
65 |
66 | // Enable the login button only if the 'password' property has some text.
67 | enableLogin: function () {
68 | if (this.model.isValid('password')) {
69 | this.$login.removeAttr('disabled');
70 | } else {
71 | this.$login.attr('disabled', 'disabled');
72 | }
73 | },
74 |
75 | // Login form submit handler.
76 | login: function (e) {
77 |
78 | // Prevent the form's default action.
79 | e.preventDefault();
80 |
81 | // Alert the user and return if the form is submitted with empty password.
82 | if (!this.model.isValid('password')) {
83 | notification.alert('Enter the password.', 'Error', 'Ok');
84 | return;
85 | }
86 |
87 | // Disable the login button to prevent multiple submits.
88 | // We'll enable it back after authentication is over.
89 | this.$login.attr('disabled', 'disabled');
90 |
91 | // Fetch the persisted credential and authenticate.
92 | this.credential.fetch()
93 | .done(_.bind(function () {
94 |
95 | // Authenticate the user.
96 | // On success, update the session vars and redirect him to '#photos', else show respective error message.
97 | if (this.model.get('password') === this.credential.get('password')) {
98 |
99 | // Update the 'state' to 'LOGGED_IN' and store the encryption-decryption key in session.
100 | session.store('state', 'LOGGED_IN');
101 | session.store('key', this.credential.get('key'));
102 |
103 | this.navigateTo('#photos');
104 | } else {
105 | // Show error and re-enable the login button.
106 | notification.alert('Invalid password.', 'Error', 'Ok')
107 | .done(_.bind(function () {
108 | this.$login.removeAttr('disabled');
109 | }, this));
110 | }
111 | }, this))
112 | .fail(_.bind(function () {
113 | notification.alert('Failed to retrieve credential. Please try again.', 'Error', 'Ok')
114 | .done(_.bind(function () {
115 | this.$login.removeAttr('disabled');
116 | }, this));
117 | }, this));
118 | }
119 | });
120 |
121 | return LoginView;
122 | });
--------------------------------------------------------------------------------
/src/js/views/photo.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'underscore',
3 | 'backbone',
4 | 'templates',
5 | 'adapters/crypto',
6 | 'adapters/persistentfilestorage',
7 | 'adapters/notification'
8 | ], function (
9 | _,
10 | Backbone,
11 | templates,
12 | crypto,
13 | persistentfilestorage,
14 | notification
15 | ) {
16 |
17 | 'use strict';
18 |
19 | // Photo view.
20 | // Renders the photo view and handles the UI logic.
21 | var PhotoView = Backbone.View.extend({
22 |
23 | template: templates.photo,
24 |
25 | initialize: function(options) {
26 |
27 | // If model is not passed throw error.
28 | if(!this.model) {
29 | throw new Error('model is required');
30 | }
31 |
32 | // If file model is not passed throw error.
33 | if(!(options && options.file)) {
34 | throw new Error('file is required');
35 | }
36 |
37 | // Set the passed file model to a property.
38 | this.file = options.file;
39 | },
40 |
41 | render: function () {
42 | this.$el.html(this.template(this.model.toJSON()));
43 | this.$photo = this.$('#photo');
44 | this.loadImage();
45 |
46 | return this;
47 | },
48 |
49 | loadImage: function () {
50 | this.file.fetch()
51 | .done(_.bind(function () {
52 | this.$photo.attr('src', 'data:image/png;base64,' + this.file.get('data'));
53 | }, this))
54 | .fail(function () {
55 | notification.alert('Failed to load image. Please try again.', 'Error', 'Ok');
56 | });
57 | }
58 | });
59 |
60 | return PhotoView;
61 | });
--------------------------------------------------------------------------------
/src/js/views/photoitem.js:
--------------------------------------------------------------------------------
1 | define(['backbone', 'templates'], function(Backbone, templates) {
2 |
3 | 'use strict';
4 |
5 | // Photo item view.
6 | var PhotoItemView = Backbone.View.extend({
7 |
8 | tagName: 'li',
9 |
10 | className: 'table-view-cell media',
11 |
12 | template: templates.photoitem,
13 |
14 | render: function() {
15 | this.$el.html(this.template(this.model.toJSON()));
16 | return this;
17 | }
18 | });
19 |
20 | return PhotoItemView;
21 | });
--------------------------------------------------------------------------------
/src/js/views/photos.js:
--------------------------------------------------------------------------------
1 | define(['underscore', 'backbone', 'views/photoitem'], function (_, Backbone, PhotoItemView) {
2 |
3 | 'use strict';
4 |
5 | // Photos list view.
6 | var Photos = Backbone.View.extend({
7 |
8 | tagName: 'ul',
9 |
10 | className: 'table-view',
11 |
12 | initialize: function () {
13 | this.childViews = [];
14 | this.listenTo(this.collection, 'reset', this.refresh);
15 | },
16 |
17 | render: function () {
18 | // Iterate through the collection, construct and render the photoitemview for each item.
19 | this.collection.each(_.bind(function (model) {
20 | var photoItemView = new PhotoItemView({
21 | model: model
22 | });
23 |
24 | this.childViews.push(photoItemView);
25 | this.$el.append(photoItemView.render().el);
26 | }, this));
27 |
28 | return this;
29 | },
30 |
31 | // Remove the child views and re-render the collection.
32 | refresh: function () {
33 | _.each(this.childViews, function(childView) {
34 | childView.remove();
35 | });
36 |
37 | this.render();
38 | }
39 | });
40 |
41 | return Photos;
42 | });
--------------------------------------------------------------------------------
/src/js/views/register.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'underscore',
3 | 'backbone',
4 | 'adapters/notification',
5 | 'session',
6 | 'templates'
7 | ], function (
8 | _,
9 | Backbone,
10 | notification,
11 | session,
12 | templates
13 | ) {
14 |
15 | 'use strict';
16 |
17 | // Registration view.
18 | // Renders the registration view and handles the UI logic.
19 | var RegisterView = Backbone.View.extend({
20 |
21 | // Set the template.
22 | template: templates.register,
23 |
24 | // Wire-up handlers for DOM events.
25 | events: {
26 | 'submit #register-form': 'register'
27 | },
28 |
29 | // Set the bindings for input elements with model properties.
30 | bindings: {
31 | '#password': 'password',
32 | '#security-question': 'securityQuestion',
33 | '#security-answer': 'securityAnswer'
34 | },
35 |
36 | initialize: function (options) {
37 |
38 | // If model is not passed through error.
39 | if (!this.model) {
40 | throw new Error('model is required');
41 | }
42 |
43 | // If security questions not passed or collection empty throw error.
44 | if (!(options && options.securityQuestions && options.securityQuestions.length)) {
45 | throw new Error('security questions required');
46 | }
47 |
48 | // Store the passed security questions in some property.
49 | this.securityQuestions = options.securityQuestions;
50 |
51 | // Create a new guid and set it as the encryption/decryption key for the model.
52 | this.model.set('key', _.guid());
53 |
54 | // Hook the handler to enable/disable security question whenever the password changes.
55 | this.listenTo(this.model, 'change:password', this.enableQuestion);
56 |
57 | // Hook the handler to enable/disable answer textbox when the selected security question changes.
58 | this.listenTo(this.model, 'change:securityQuestion', this.enableAnswer);
59 |
60 | // Hook the handler to enable/disable the register button whenever the model changes.
61 | this.listenTo(this.model, 'change', this.enableRegister);
62 | },
63 |
64 | // Render the view, bind the controls and cache the elements.
65 | render: function () {
66 | this.$el.html(this.template({
67 | questions: this.securityQuestions
68 | }));
69 |
70 | this.stickit();
71 |
72 | this.$securityQuestion = this.$('#security-question');
73 | this.$securityAnswer = this.$('#security-answer');
74 | this.$register = this.$('#register');
75 |
76 | return this;
77 | },
78 |
79 | // Enable the security question dropdown when the password is valid.
80 | enableQuestion: function () {
81 | if (this.model.isValid('password')) {
82 | this.$securityQuestion.removeAttr('disabled');
83 | } else {
84 | this.$securityQuestion.attr('disabled', 'disabled');
85 | }
86 | },
87 |
88 | // Clear the answer textbox, enable it if the selected question is valid.
89 | enableAnswer: function () {
90 | this.model.set('securityAnswer', null);
91 |
92 | if (this.model.isValid('securityQuestion') && this.model.get('securityQuestion') !== 'Select a question') {
93 | this.$securityAnswer.removeAttr('disabled');
94 | } else {
95 | this.$securityAnswer.attr('disabled', 'disabled');
96 | }
97 | },
98 |
99 | // Enable the register button when the model is valid.
100 | enableRegister: function () {
101 | if (this.model.isValid(true)) {
102 | this.$register.removeAttr('disabled');
103 | } else {
104 | this.$register.attr('disabled', 'disabled');
105 | }
106 | },
107 |
108 | // Register button event handler.
109 | register: function (e) {
110 | e.preventDefault();
111 |
112 | // Alert the user if all the fields are not filled.
113 | if (!this.model.isValid(true)) {
114 | notification.alert('Fill all the fields.', 'Error', 'Ok');
115 | return;
116 | }
117 |
118 | // Disable the register button to prevent multiple submits.
119 | this.$register.attr('disabled', 'disabled');
120 |
121 | // Save the credential, security info and redirect him to '#login'.
122 | this.model.save()
123 | .done(_.bind(function () {
124 | // Update the state session variable and navigate to login page.
125 | session.store('state', 'LOGGED_OUT');
126 | this.navigateTo('#login');
127 | }, this))
128 | .fail(_.bind(function () {
129 | notification.alert('Registration failed. Please try again.', 'Error', 'Ok')
130 | .done(_.bind(function () {
131 | this.$register.removeAttr('disabled');
132 | }, this));
133 | }, this));
134 | }
135 | });
136 |
137 | return RegisterView;
138 | });
139 |
--------------------------------------------------------------------------------
/src/js/views/settings.js:
--------------------------------------------------------------------------------
1 | define(['backbone', 'templates'], function(Backbone, templates) {
2 |
3 | 'use strict';
4 |
5 | // Settings view.
6 | var SettingsView = Backbone.View.extend({
7 |
8 | template: templates.settings,
9 |
10 | render: function() {
11 | this.$el.html(this.template());
12 | return this;
13 | }
14 | });
15 |
16 | return SettingsView;
17 | });
--------------------------------------------------------------------------------
/tests/addeditspec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'adapters/camera',
6 | 'adapters/imageresizer',
7 | 'adapters/notification',
8 | 'models/photo',
9 | 'models/file',
10 | 'collections/photos',
11 | 'views/addedit',
12 | 'templates'
13 | ], function (
14 | $,
15 | _,
16 | extensions,
17 | camera,
18 | imageresizer,
19 | notification,
20 | Photo,
21 | File,
22 | Photos,
23 | AddEditView,
24 | templates
25 | ) {
26 |
27 | 'use strict';
28 |
29 | describe('addedit view', function () {
30 |
31 | var view, model, collection, file;
32 |
33 | beforeEach(function () {
34 | spyOn(notification, 'alert').and.callFake(function () {
35 | return _.resolve();
36 | });
37 |
38 | model = new Photo();
39 |
40 | file = new File();
41 |
42 | collection = new Photos();
43 |
44 | view = new AddEditView({
45 | model: model,
46 | file: file,
47 | collection: collection
48 | });
49 | });
50 |
51 | afterEach(function () {
52 | if (view) {
53 | view.remove();
54 | }
55 | });
56 |
57 | describe('when constructed', function () {
58 |
59 | describe('without model', function () {
60 | it('should throw error', function () {
61 | expect(function () {
62 | new AddEditView();
63 | }).toThrow(new Error('model is required'));
64 | });
65 | });
66 |
67 | describe('without file model', function () {
68 | it('should throw error', function () {
69 | expect(function () {
70 | new AddEditView({
71 | model: new Photo()
72 | });
73 | }).toThrow(new Error('file is required'));
74 | });
75 | });
76 |
77 | describe('without collection', function () {
78 | it('should throw error', function () {
79 | expect(function () {
80 | new AddEditView({
81 | model: new Photo(),
82 | file: new File()
83 | });
84 | }).toThrow(new Error('collection is required'));
85 | });
86 | });
87 |
88 | describe('with required arguments', function () {
89 | it('should exist with properties initialized', function () {
90 | expect(view).toBeDefined();
91 | expect(view.model).toBe(model);
92 | expect(view.file).toBe(file);
93 | expect(view.template).toBe(templates.addedit);
94 | });
95 | });
96 |
97 | describe('for add a new photo', function () {
98 | it('should have the isEdit property as false', function () {
99 | expect(view.isEdit).toBe(false);
100 | });
101 | });
102 |
103 | describe('for edit an existing photo', function () {
104 |
105 | beforeEach(function () {
106 | view = new AddEditView({
107 | model: new Photo({
108 | id: '1'
109 | }),
110 | file: new File({
111 | id: '1'
112 | })
113 | });
114 | });
115 |
116 | it('should have the isEdit property as true', function () {
117 | expect(view.isEdit).toBe(true);
118 | });
119 | });
120 | });
121 |
122 | describe('when rendered for add photo', function () {
123 |
124 | beforeEach(function () {
125 | view.render();
126 | });
127 |
128 | it('should contain the title as Add Photo', function () {
129 | expect(view.$el.find('.title')).toHaveText('Add Photo');
130 | });
131 |
132 | it('should contain description textbox empty with maxlength 25', function () {
133 | var $description = view.$el.find('#description');
134 |
135 | expect($description).toHaveValue('');
136 | expect($description).toHaveAttr('maxlength', '25');
137 | });
138 |
139 | it('should contain the image pointing to placeholder.png', function () {
140 | expect(view.$el.find('#photo')).toHaveAttr('src', 'images/placeholder.png');
141 | });
142 |
143 | it('should not contain the delete button', function () {
144 | expect(view.$el).not.toContainElement('#delete');
145 | });
146 | });
147 |
148 | describe('when rendered to edit photo', function () {
149 |
150 | beforeEach(function () {
151 | model = new Photo({
152 | id: '1',
153 | description: 'River-side property document',
154 | thumbnail: '[some crazy thumbnail]',
155 | lastSaved: 'Fri Mar 06 2015 18:10:23 GMT+0530 (India Standard Time)'
156 | });
157 |
158 | file = new File({
159 | id: model.id
160 | });
161 |
162 | view = new AddEditView({
163 | model: model,
164 | file: file
165 | });
166 | });
167 |
168 | describe('with file fetch failed', function () {
169 |
170 | beforeEach(function () {
171 | spyOn(file, 'fetch').and.callFake(function () {
172 | return _.reject();
173 | });
174 |
175 | view.render();
176 | });
177 |
178 | it('should contain the title as Edit Photo', function () {
179 | expect(view.$el.find('.title')).toHaveText('Edit Photo');
180 | });
181 |
182 | it('should contain description textbox filled', function () {
183 | expect(view.$el.find('#description')).toHaveValue(model.get('description'));
184 | });
185 |
186 | it('should contain the delete button', function () {
187 | expect(view.$el).toContainElement('#delete');
188 | });
189 |
190 | it('should contain the image src set to thumbnail', function () {
191 | expect(view.$el.find('#photo')).toHaveAttr('src', 'data:image/png;base64,' + model.get('thumbnail'));
192 | });
193 |
194 | it('should notify the user about the failure', function () {
195 | expect(notification.alert).toHaveBeenCalledWith('Failed to load image. Please try again.', 'Error', 'Ok');
196 | });
197 | });
198 |
199 | describe('with file fetch succeeded', function () {
200 |
201 | var base64Image = '[SOME CRAZY IMAGE]';
202 |
203 | beforeEach(function () {
204 | spyOn(file, 'fetch').and.callFake(function () {
205 | file.set('data', base64Image);
206 | return _.resolve();
207 | });
208 |
209 | view.render();
210 | });
211 |
212 | it('should contain the image src set to the actual image\'s base64', function () {
213 | expect(view.$el.find('#photo')).toHaveAttr('src', 'data:image/png;base64,' + base64Image);
214 | });
215 | });
216 | });
217 |
218 | describe('when changes', function () {
219 |
220 | beforeEach(function () {
221 | view.render();
222 | });
223 |
224 | describe('by user selecting a new photo from library', function () {
225 |
226 | beforeEach(function () {
227 |
228 | spyOn(camera, 'getPictureFromLibrary').and.callFake(function () {
229 | return _.resolve('[SOME CRAZY BASE64 STRING]');
230 | });
231 |
232 | spyOn(imageresizer, 'resize').and.callFake(function () {
233 | return _.resolve({
234 | imageData: '[some crazy base64 thumbnail]'
235 | });
236 | });
237 |
238 | view.addPhoto({
239 | preventDefault: $.noop
240 | });
241 | });
242 |
243 | it('should update the data property of file model', function () {
244 | expect(view.file.get('data')).toBe('[SOME CRAZY BASE64 STRING]');
245 | });
246 |
247 | it('should update the image tag', function () {
248 | expect(view.$el.find('#photo')).toHaveAttr('src', 'data:image/png;base64,[SOME CRAZY BASE64 STRING]');
249 | });
250 |
251 | it('should resize the image', function () {
252 | expect(imageresizer.resize).toHaveBeenCalledWith('[SOME CRAZY BASE64 STRING]', 42, 0);
253 | });
254 |
255 | it('should update the thumbnail property of photo model', function () {
256 | expect(view.model.get('thumbnail')).toBe('[some crazy base64 thumbnail]');
257 | });
258 |
259 | it('the save button should stay disabled', function () {
260 | expect(view.$el.find('#save')).toBeDisabled();
261 | });
262 | });
263 |
264 | describe('by adding text to description textbox', function () {
265 |
266 | beforeEach(function () {
267 | view.$el.find('#description').val('HDFC CreditCard').trigger('change');
268 | });
269 |
270 | it('should update the description property', function () {
271 | expect(view.model.get('description')).toBe('HDFC CreditCard');
272 | });
273 |
274 | it('the save button should stay disabled', function () {
275 | expect(view.$el.find('#save')).toBeDisabled();
276 | });
277 | });
278 |
279 | describe('by filling all the fields', function () {
280 |
281 | beforeEach(function () {
282 |
283 | spyOn(camera, 'getPictureFromLibrary').and.callFake(function () {
284 | return _.resolve('[SOME CRAZY BASE64 STRING]');
285 | });
286 |
287 | spyOn(imageresizer, 'resize').and.callFake(function () {
288 | return _.resolve({
289 | imageData: '[some crazy base64 thumbnail]'
290 | });
291 | });
292 |
293 | view.addPhoto({
294 | preventDefault: $.noop
295 | });
296 |
297 | view.$el.find('#description').val('HDFC CreditCard').trigger('change');
298 | });
299 |
300 | it('should enable the done button', function () {
301 | expect(view.$el.find('#save')).not.toBeDisabled();
302 | });
303 | });
304 |
305 | describe('by submitting form', function () {
306 |
307 | describe('without all fields filled', function () {
308 |
309 | beforeEach(function () {
310 | spyOn(view.model, 'save');
311 | spyOn(view.collection, 'create');
312 |
313 | view.$el.find('#description').val('HDFC CreditCard').trigger('change');
314 | view.$el.find('#addedit-form').trigger('submit');
315 | });
316 |
317 | it('should alert error', function () {
318 | expect(notification.alert).toHaveBeenCalledWith('Some of the required fields are missing.', 'Error', 'Ok');
319 | });
320 |
321 | it('should not call model.save', function () {
322 | expect(view.model.save).not.toHaveBeenCalled();
323 | });
324 |
325 | it('should not call collection.create', function () {
326 | expect(view.collection.create).not.toHaveBeenCalled();
327 | });
328 | });
329 |
330 | describe('with all fields filled', function () {
331 |
332 | beforeEach(function () {
333 | spyOn(camera, 'getPictureFromLibrary').and.callFake(function () {
334 | return _.resolve('[SOME CRAZY BASE64 STRING]');
335 | });
336 |
337 | spyOn(imageresizer, 'resize').and.callFake(function () {
338 | return _.resolve({
339 | imageData: '[some crazy base64 thumbnail]'
340 | });
341 | });
342 |
343 | view.addPhoto({
344 | preventDefault: $.noop
345 | });
346 |
347 | view.$el.find('#description').val('HDFC CreditCard').trigger('change');
348 | });
349 |
350 | describe('and failed to create', function () {
351 |
352 | beforeEach(function () {
353 | spyOn(view.collection, 'create').and.callFake(function (model, options) {
354 | options.error();
355 | });
356 |
357 | view.$el.find('#addedit-form').trigger('submit');
358 | });
359 |
360 | it('should alert error', function () {
361 | expect(notification.alert).toHaveBeenCalledWith('Failed to save photo. Please try again.', 'Error', 'Ok');
362 | });
363 |
364 | it('should enable the done button', function () {
365 | expect(view.$el.find('#save')).not.toBeDisabled();
366 | });
367 | });
368 |
369 | describe('and failed to persist file', function () {
370 |
371 | beforeEach(function () {
372 | spyOn(view.collection, 'create').and.callFake(function (model, options) {
373 | options.success();
374 | });
375 |
376 | spyOn(view.file, 'save').and.callFake(function () {
377 | return _.reject();
378 | });
379 |
380 | view.$el.find('#addedit-form').trigger('submit');
381 | });
382 |
383 | it('should alert error', function () {
384 | expect(notification.alert).toHaveBeenCalledWith('Failed to save photo. Please try again.', 'Error', 'Ok');
385 | });
386 |
387 | it('should enable the done button', function () {
388 | expect(view.$el.find('#save')).not.toBeDisabled();
389 | });
390 | });
391 |
392 | describe('and succeeded to create', function () {
393 |
394 | beforeEach(function () {
395 | spyOn(view.collection, 'create').and.callFake(function (model, options) {
396 | options.success();
397 | });
398 |
399 | spyOn(view.file, 'save').and.callFake(function () {
400 | return _.resolve();
401 | });
402 |
403 | spyOn(view, 'navigateTo');
404 |
405 | view.$el.find('#addedit-form').trigger('submit');
406 | });
407 |
408 | it('should rnavigate back to photos page', function () {
409 | expect(view.navigateTo).toHaveBeenCalledWith('#photos');
410 | });
411 | });
412 |
413 | });
414 | });
415 | });
416 |
417 | describe('when changes in edit', function () {
418 |
419 | beforeEach(function () {
420 | model = new Photo({
421 | id: '1',
422 | description: 'River-side property document',
423 | thumbnail: '[thumbnail]',
424 | lastSaved: 'Fri Mar 06 2015 18:10:23 GMT+0530 (India Standard Time)'
425 | });
426 |
427 | file = new File({
428 | id: model.id
429 | });
430 |
431 | view = new AddEditView({
432 | model: model,
433 | file: file
434 | });
435 |
436 | spyOn(view.file, 'fetch').and.callFake(function () {
437 | view.file.set('data', '[persisted base64 image]');
438 | return _.resolve();
439 | });
440 |
441 | view.render();
442 | });
443 |
444 | describe('by submitting form', function () {
445 |
446 | describe('and failed to save', function () {
447 |
448 | beforeEach(function () {
449 | spyOn(view.model, 'save').and.callFake(function () {
450 | return _.reject();
451 | });
452 |
453 | view.$el.find('#addedit-form').trigger('submit');
454 | });
455 |
456 | it('should alert error', function () {
457 | expect(notification.alert).toHaveBeenCalledWith('Failed to save photo. Please try again.', 'Error', 'Ok');
458 | });
459 |
460 | it('should enable the done button', function () {
461 | expect(view.$el.find('#save')).not.toBeDisabled();
462 | });
463 | });
464 |
465 | describe('and succeeded to save', function () {
466 |
467 | beforeEach(function () {
468 | spyOn(view.model, 'save').and.callFake(function () {
469 | return _.resolve();
470 | });
471 |
472 | spyOn(view.file, 'save').and.callFake(function () {
473 | return _.resolve();
474 | });
475 |
476 | spyOn(view, 'navigateTo');
477 |
478 | view.$el.find('#addedit-form').trigger('submit');
479 | });
480 |
481 | it('should navigate back to the photo page', function () {
482 | expect(view.navigateTo).toHaveBeenCalledWith('#photos/' + model.id);
483 | });
484 | });
485 | });
486 |
487 | describe('by clicking the delete button', function () {
488 |
489 | describe('and not confirm', function () {
490 |
491 | beforeEach(function () {
492 | spyOn(notification, 'confirm').and.callFake(function () {
493 | return _.resolve(0);
494 | });
495 |
496 | spyOn(view.file, 'destroy');
497 | spyOn(view.model, 'destroy');
498 |
499 | view.delete();
500 | });
501 |
502 | it('should not call destroy file', function () {
503 | expect(view.file.destroy).not.toHaveBeenCalled();
504 | });
505 |
506 | it('should not call destroy model', function () {
507 | expect(view.model.destroy).not.toHaveBeenCalled();
508 | });
509 |
510 | it('should enable back the delete button', function () {
511 | expect(view.$el.find('#delete')).not.toBeDisabled();
512 | });
513 | });
514 |
515 | describe('and confirm', function () {
516 |
517 | beforeEach(function () {
518 | spyOn(notification, 'confirm').and.callFake(function () {
519 | return _.resolve(1);
520 | });
521 | });
522 |
523 | describe('and on failed to delete file', function () {
524 |
525 | beforeEach(function () {
526 | spyOn(view.file, 'destroy').and.callFake(function () {
527 | return _.reject();
528 | });
529 |
530 | spyOn(view.model, 'destroy');
531 |
532 | view.delete();
533 | });
534 |
535 | it('should alert notification', function () {
536 | expect(notification.alert).toHaveBeenCalledWith('Failed to delete. Please try again.', 'Error', 'Ok');
537 | });
538 |
539 | it('should enable back the delete button', function () {
540 | expect(view.$el.find('#delete')).not.toBeDisabled();
541 | });
542 |
543 | it('should not call destroy model', function () {
544 | expect(view.model.destroy).not.toHaveBeenCalled();
545 | });
546 | });
547 |
548 | describe('and on failed to delete model', function () {
549 |
550 | beforeEach(function () {
551 | spyOn(view.file, 'destroy').and.callFake(function () {
552 | return _.resolve();
553 | });
554 |
555 | spyOn(view.model, 'destroy').and.callFake(function () {
556 | return _.reject();
557 | });
558 |
559 | view.delete();
560 | });
561 |
562 | it('should alert notification', function () {
563 | expect(notification.alert).toHaveBeenCalledWith('Failed to delete. Please try again.', 'Error', 'Ok');
564 | });
565 |
566 | it('should enable back the delete button', function () {
567 | expect(view.$el.find('#delete')).not.toBeDisabled();
568 | });
569 | });
570 |
571 | describe('and on success delete', function () {
572 |
573 | beforeEach(function () {
574 | spyOn(view.file, 'destroy').and.callFake(function () {
575 | return _.resolve();
576 | });
577 |
578 | spyOn(view.model, 'destroy').and.callFake(function () {
579 | return _.resolve();
580 | });
581 |
582 | spyOn(view, 'navigateTo');
583 |
584 | view.delete();
585 | });
586 |
587 | it('should call destroy file', function () {
588 | expect(view.file.destroy).toHaveBeenCalled();
589 | });
590 |
591 | it('should call destroy model', function () {
592 | expect(view.model.destroy).toHaveBeenCalled();
593 | });
594 |
595 | it('should navigate back to photos page', function () {
596 | expect(view.navigateTo).toHaveBeenCalledWith('#photos');
597 | });
598 | });
599 |
600 | });
601 |
602 | });
603 | });
604 | });
605 | });
606 |
--------------------------------------------------------------------------------
/tests/changepasswordspec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'session',
6 | 'adapters/notification',
7 | 'models/credential',
8 | 'views/changepassword',
9 | 'templates'
10 | ], function (
11 | $,
12 | _,
13 | extensions,
14 | session,
15 | notification,
16 | Credential,
17 | ChangePasswordView,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | describe('changepassword view', function () {
24 |
25 | var view, model, credential;
26 |
27 | beforeEach(function () {
28 | spyOn(notification, 'alert').and.callFake(function () {
29 | return _.resolve();
30 | });
31 |
32 | model = new Credential();
33 |
34 | credential = new Credential({
35 | id: 'Safe-Credential'
36 | });
37 |
38 | view = new ChangePasswordView({
39 | model: model,
40 | credential: credential
41 | });
42 | });
43 |
44 | afterEach(function () {
45 | if (view) {
46 | view.remove();
47 | }
48 | });
49 |
50 | describe('when constructed', function () {
51 | describe('without model', function () {
52 | it('should throw error', function () {
53 | expect(function () {
54 | new ChangePasswordView();
55 | }).toThrow(new Error('model is required'));
56 | });
57 | });
58 |
59 | describe('without credential', function () {
60 | it('should throw error', function () {
61 | expect(function () {
62 | new ChangePasswordView({
63 | model: new Credential()
64 | });
65 | }).toThrow(new Error('credential is required'));
66 | });
67 | });
68 |
69 | describe('with required arguments', function () {
70 | it('should exist with properties initialized', function () {
71 | expect(view).toBeDefined();
72 | expect(view.model).toBe(model);
73 | expect(view.credential).toBe(credential);
74 | expect(view.template).toBe(templates.changepassword);
75 | });
76 | });
77 | });
78 |
79 | describe('when rendered', function () {
80 |
81 | beforeEach(function () {
82 | view.render();
83 | });
84 |
85 | it('should contain password textbox empty, maxlength 15 and autocomplete off', function () {
86 | var $password = view.$el.find('#password');
87 |
88 | expect($password).toHaveValue('');
89 | expect($password).toHaveAttr('maxlength', '15');
90 | expect($password).toHaveAttr('autocomplete', 'off');
91 | });
92 |
93 | it('should contain submit button as disabled', function () {
94 | expect(view.$el.find('#change')).toBeDisabled();
95 | });
96 | });
97 |
98 | describe('when changes', function () {
99 |
100 | beforeEach(function () {
101 | view.render();
102 | });
103 |
104 | describe('with the password textbox empty', function () {
105 |
106 | beforeEach(function () {
107 | view.$el.find('#password').val('').trigger('change');
108 | });
109 |
110 | it('then password property of the model should be empty', function () {
111 | expect(view.model.get('password')).toBe('');
112 | });
113 |
114 | it('then the submit button should be disabled', function () {
115 | expect(view.$el.find('#change')).toBeDisabled();
116 | });
117 | });
118 |
119 | describe('with the password textbox not empty', function () {
120 |
121 | beforeEach(function () {
122 | view.$el.find('#password').val('Ch@ng3d').trigger('change');
123 | });
124 |
125 | it('then the password property of the model should not be empty', function () {
126 | expect(view.model.get('password')).toBe('Ch@ng3d');
127 | });
128 |
129 | it('then the submit button should be enabled', function () {
130 | expect(view.$el.find('#change')).not.toBeDisabled();
131 | });
132 | });
133 |
134 | describe('when form is submitted', function () {
135 |
136 | describe('without any password', function () {
137 |
138 | beforeEach(function () {
139 | view.$el.find('#password').val('').trigger('change');
140 | view.$el.find('#changepassword-form').trigger('submit');
141 | });
142 |
143 | it('should alert an error', function () {
144 | expect(notification.alert).toHaveBeenCalledWith('Enter the password.', 'Error', 'Ok');
145 | });
146 | });
147 |
148 | describe('with password', function () {
149 |
150 | beforeEach(function () {
151 | view.$el.find('#password').val('Ch@ng3d').trigger('change');
152 | });
153 |
154 | describe('and failed to fetch', function () {
155 |
156 | beforeEach(function () {
157 | spyOn(view.credential, 'fetch').and.callFake(function () {
158 | return _.reject();
159 | });
160 |
161 | view.$el.find('#changepassword-form').trigger('submit');
162 | });
163 |
164 | it('should display notification', function () {
165 | expect(notification.alert).toHaveBeenCalledWith('Failed to change password. Please try again.', 'Error', 'Ok');
166 | });
167 |
168 | it('should enable back the change button', function () {
169 | expect(view.$el.find('#change')).not.toBeDisabled();
170 | });
171 | });
172 |
173 | describe('and fetch succeeded', function () {
174 |
175 | beforeEach(function () {
176 | spyOn(view.credential, 'fetch').and.callFake(function () {
177 | view.credential.set({
178 | password: 'S@f3',
179 | key: '007',
180 | securityQuestion: 'CODE WELCOME',
181 | securityAnswer: 'CODE GET LOST'
182 | });
183 |
184 | return _.resolve();
185 | });
186 | });
187 |
188 | describe('and failed to save', function () {
189 |
190 | beforeEach(function () {
191 | spyOn(view.credential, 'save').and.callFake(function () {
192 | return _.reject();
193 | });
194 |
195 | view.$el.find('#changepassword-form').trigger('submit');
196 | });
197 |
198 | it('should display notification', function () {
199 | expect(notification.alert).toHaveBeenCalledWith('Failed to change password. Please try again.', 'Error', 'Ok');
200 | });
201 |
202 | it('should enable back the change button', function () {
203 | expect(view.$el.find('#change')).not.toBeDisabled();
204 | });
205 | });
206 |
207 | describe('and save succeeded', function () {
208 |
209 | beforeEach(function () {
210 | spyOn(view.credential, 'save').and.callFake(function () {
211 | return _.resolve();
212 | });
213 |
214 | spyOn(session, 'store');
215 | spyOn(session, 'remove');
216 | spyOn(view, 'navigateTo');
217 |
218 | view.$el.find('#changepassword-form').trigger('submit');
219 | });
220 |
221 | it('should update the state session variable to LOGGED_OUT', function () {
222 | expect(session.store).toHaveBeenCalledWith('state', 'LOGGED_OUT');
223 | });
224 |
225 | it('should removed the key from session', function () {
226 | expect(session.remove).toHaveBeenCalled();
227 | });
228 |
229 | it('should navigate to login page', function () {
230 | expect(view.navigateTo).toHaveBeenCalledWith('#login');
231 | });
232 | });
233 |
234 | });
235 |
236 | });
237 | });
238 | });
239 | });
240 | });
--------------------------------------------------------------------------------
/tests/changesecurityinfospec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'adapters/notification',
6 | 'models/credential',
7 | 'views/changesecurityinfo',
8 | 'templates'
9 | ], function (
10 | $,
11 | _,
12 | extensions,
13 | notification,
14 | Credential,
15 | ChangeSecurityInfoView,
16 | templates
17 | ) {
18 |
19 | 'use strict';
20 |
21 | describe('changesecurityinfo view', function () {
22 |
23 | var view,
24 | model,
25 | credential,
26 | securityQuestions = ['Where is eiffel tower?', 'Who directed Titanic?', 'Test'];
27 |
28 | beforeEach(function () {
29 | spyOn(notification, 'alert').and.callFake(function () {
30 | return _.resolve();
31 | });
32 |
33 | model = new Credential();
34 |
35 | credential = new Credential({
36 | id: 'Safe-Credential'
37 | });
38 |
39 | view = new ChangeSecurityInfoView({
40 | model: model,
41 | credential: credential,
42 | securityQuestions: securityQuestions
43 | });
44 | });
45 |
46 | afterEach(function () {
47 | if (view) {
48 | view.remove();
49 | }
50 | });
51 |
52 | describe('when constructed', function () {
53 |
54 | describe('without model', function () {
55 | it('should throw error', function () {
56 | expect(function () {
57 | new ChangeSecurityInfoView();
58 | }).toThrow(new Error('model is required'));
59 | });
60 | });
61 |
62 | describe('without credential', function () {
63 | it('should throw error', function () {
64 | expect(function () {
65 | new ChangeSecurityInfoView({
66 | model: new Credential()
67 | });
68 | }).toThrow(new Error('credential is required'));
69 | });
70 | });
71 |
72 | describe('without security questions', function () {
73 | it('should throw error', function () {
74 | expect(function () {
75 | new ChangeSecurityInfoView({
76 | model: model,
77 | credential: new Credential()
78 | });
79 | }).toThrow(new Error('security questions required'));
80 | });
81 | });
82 |
83 | describe('with required arguments', function () {
84 | it('should exist with properties initialized', function () {
85 | expect(view).toBeDefined();
86 | expect(view.model).toBe(model);
87 | expect(view.credential).toBe(credential);
88 | expect(view.securityQuestions).toBe(securityQuestions);
89 | expect(view.template).toBe(templates.changesecurityinfo);
90 | });
91 | });
92 |
93 | });
94 |
95 | describe('when rendered', function () {
96 |
97 | beforeEach(function () {
98 | view.render();
99 | });
100 |
101 | it('should contain the security question dropdown selected', function () {
102 | expect(view.$el.find('#security-question option:eq(0)')).toBeSelected();
103 | });
104 |
105 | it('should contain the security answer filled', function () {
106 | expect(view.$el.find('#security-answer')).toHaveValue('');
107 | });
108 |
109 | it('should contain the change button disabled', function () {
110 | expect(view.$el.find('#save')).toBeDisabled();
111 | });
112 | });
113 |
114 | describe('when changes', function () {
115 |
116 | beforeEach(function () {
117 | view.render();
118 | });
119 |
120 | describe('with no security question is selected', function () {
121 |
122 | beforeEach(function () {
123 | view.$el.find('#security-question').val('Select a question').trigger('change');
124 | });
125 |
126 | it('then the answer textbox should be empty and disabled', function () {
127 | var $securityanswer = view.$el.find('#security-answer');
128 |
129 | expect($securityanswer).toHaveValue('');
130 | expect($securityanswer).toBeDisabled();
131 | });
132 | });
133 |
134 | describe('with security question is selected', function () {
135 |
136 | beforeEach(function () {
137 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
138 | });
139 |
140 | it('then the answer textbox should be enabled', function () {
141 | expect(view.$el.find('#security-answer')).not.toBeDisabled();
142 | });
143 | });
144 |
145 | describe('with all fields not filled', function () {
146 | beforeEach(function () {
147 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
148 | });
149 |
150 | it('then the save button should be disabled', function () {
151 | expect(view.$el.find('#save')).toBeDisabled();
152 | });
153 | });
154 |
155 | describe('with all fields filled', function () {
156 |
157 | beforeEach(function () {
158 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
159 | view.$el.find('#security-answer').val('France').trigger('change');
160 | });
161 |
162 | it('then the save button should be enabled', function () {
163 | expect(view.$el.find('#save')).not.toBeDisabled();
164 | });
165 | });
166 |
167 | describe('when form is submitted', function () {
168 |
169 | describe('without all fields filled', function () {
170 |
171 | beforeEach(function () {
172 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
173 | view.$el.find('#security-answer').val('').trigger('change');
174 | view.$el.find('#changesecurityinfo-form').trigger('submit');
175 | });
176 |
177 | it('should alert an error', function () {
178 | expect(notification.alert).toHaveBeenCalledWith('Submit security question with answer.', 'Error', 'Ok');
179 | });
180 | });
181 |
182 | describe('with all the fields filled', function () {
183 |
184 | beforeEach(function () {
185 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
186 | view.$el.find('#security-answer').val('France').trigger('change');
187 | });
188 |
189 | describe('and failed to fetch', function () {
190 |
191 | beforeEach(function () {
192 | spyOn(view.credential, 'fetch').and.callFake(function () {
193 | return _.reject();
194 | });
195 |
196 | view.$el.find('#changesecurityinfo-form').trigger('submit');
197 | });
198 |
199 | it('should display notification', function () {
200 | expect(notification.alert).toHaveBeenCalledWith('Failed to change security info. Please try again.', 'Error', 'Ok');
201 | });
202 |
203 | it('should enable back the save button', function () {
204 | expect(view.$el.find('#save')).not.toBeDisabled();
205 | });
206 | });
207 |
208 | describe('and fetch succeeded', function () {
209 |
210 | beforeEach(function () {
211 | spyOn(view.credential, 'fetch').and.callFake(function () {
212 | view.credential.set({
213 | password: 'S@f3',
214 | key: '007',
215 | securityQuestion: 'CODE WELCOME',
216 | securityAnswer: 'CODE GET LOST'
217 | });
218 |
219 | return _.resolve();
220 | });
221 | });
222 |
223 | describe('and failed to save', function () {
224 |
225 | beforeEach(function () {
226 | spyOn(view.credential, 'save').and.callFake(function () {
227 | return _.reject();
228 | });
229 |
230 | view.$el.find('#changesecurityinfo-form').trigger('submit');
231 | });
232 |
233 | it('should display notification', function () {
234 | expect(notification.alert).toHaveBeenCalledWith('Failed to change security info. Please try again.', 'Error', 'Ok');
235 | });
236 |
237 | it('should enable back the save button', function () {
238 | expect(view.$el.find('#save')).not.toBeDisabled();
239 | });
240 | });
241 |
242 | describe('and save succeeded', function () {
243 |
244 | beforeEach(function () {
245 | spyOn(view.credential, 'save').and.callFake(function () {
246 | return _.resolve();
247 | });
248 |
249 | spyOn(view, 'navigateTo');
250 |
251 | view.$el.find('#changesecurityinfo-form').trigger('submit');
252 | });
253 |
254 | it('should navigate to settings page', function () {
255 | expect(view.navigateTo).toHaveBeenCalledWith('#settings');
256 | });
257 | });
258 |
259 | });
260 |
261 | });
262 | });
263 | });
264 | });
265 | });
--------------------------------------------------------------------------------
/tests/forgotpasswordspec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'underscore',
3 | 'extensions',
4 | 'session',
5 | 'adapters/notification',
6 | 'models/credential',
7 | 'views/forgotpassword',
8 | 'templates'
9 | ], function (
10 | _,
11 | extensions,
12 | session,
13 | notification,
14 | Credential,
15 | ForgotPasswordView,
16 | templates
17 | ) {
18 |
19 | 'use strict';
20 |
21 | describe('forgotpassword view', function () {
22 |
23 | var view,
24 | model,
25 | persistedModel = new Credential({
26 | id: 'Safe-Credential',
27 | password: 'S@f3',
28 | key: '007',
29 | securityQuestion: 'CODE WELCOME',
30 | securityAnswer: 'CODE GET LOST'
31 | });
32 |
33 | beforeEach(function () {
34 | session.clear();
35 |
36 | spyOn(notification, 'alert').and.callFake(function () {
37 | return _.resolve();
38 | });
39 |
40 | model = new Credential({
41 | securityQuestion: persistedModel.get('securityQuestion')
42 | });
43 |
44 | view = new ForgotPasswordView({
45 | model: model,
46 | credential: persistedModel
47 | });
48 | });
49 |
50 | afterEach(function () {
51 | if (view) {
52 | view.remove();
53 | }
54 | });
55 |
56 | describe('when constructed', function () {
57 |
58 | describe('without model', function () {
59 | it('should throw error', function () {
60 | expect(function () {
61 | new ForgotPasswordView();
62 | }).toThrow(new Error('model is required'));
63 | });
64 | });
65 |
66 | describe('without credential', function () {
67 | it('should throw error', function () {
68 | expect(function () {
69 | new ForgotPasswordView({
70 | model: model
71 | });
72 | }).toThrow(new Error('credential is required'));
73 | });
74 | });
75 |
76 | describe('with required arguments', function () {
77 | it('should exist with properties initialized', function () {
78 | expect(view).toBeDefined();
79 | expect(view.model).toBe(model);
80 | expect(view.credential).toBe(persistedModel);
81 | expect(view.template).toBe(templates.forgotpassword);
82 | });
83 | });
84 | });
85 |
86 | describe('when rendered', function () {
87 |
88 | beforeEach(function () {
89 | view.render();
90 | });
91 |
92 | it('should display the security question', function () {
93 | expect(view.$el.find('#security-question')).toHaveText(persistedModel.get('securityQuestion') + '?');
94 | });
95 |
96 | it('should contain security answer textbox empty, maxlength 15 and autocomplete off', function () {
97 | var $securityanswer = view.$el.find('#security-answer');
98 |
99 | expect($securityanswer).toHaveValue('');
100 | expect($securityanswer).toHaveAttr('maxlength', '15');
101 | expect($securityanswer).toHaveAttr('autocomplete', 'off');
102 | });
103 |
104 | it('should contain submit button as disabled', function () {
105 | expect(view.$el.find('#verify')).toBeDisabled();
106 | });
107 | });
108 |
109 | describe('when changes', function () {
110 |
111 | beforeEach(function () {
112 | view.render();
113 | });
114 |
115 | describe('with the security answer textbox empty', function () {
116 |
117 | beforeEach(function () {
118 | view.$el.find('#security-answer').val('').trigger('change');
119 | });
120 |
121 | it('then security answer property of the model should be empty', function () {
122 | expect(view.model.get('securityAnswer')).toBe('');
123 | });
124 |
125 | it('then the submit button should be disabled', function () {
126 | expect(view.$el.find('#verify')).toBeDisabled();
127 | });
128 | });
129 |
130 | describe('with the security answer textbox not empty', function () {
131 |
132 | beforeEach(function () {
133 | view.$el.find('#security-answer').val('S@f3').trigger('change');
134 | });
135 |
136 | it('then the security security answer property of the model should be empty', function () {
137 | expect(view.model.get('securityAnswer')).toBe('S@f3');
138 | });
139 |
140 | it('then the submit button should be disabled', function () {
141 | expect(view.$el.find('#verify')).not.toBeDisabled();
142 | });
143 | });
144 |
145 | describe('when form is submitted', function () {
146 |
147 | describe('without any security answer', function () {
148 |
149 | beforeEach(function () {
150 | view.$el.find('#security-answer').val('').trigger('change');
151 | view.$el.find('#forgotpassword-form').trigger('submit');
152 | });
153 |
154 | it('should alert an error', function () {
155 | expect(notification.alert).toHaveBeenCalledWith('Enter the answer.', 'Error', 'Ok');
156 | });
157 | });
158 |
159 | describe('with invalid security answer', function () {
160 |
161 | beforeEach(function () {
162 | view.$el.find('#security-answer').val('CODE WELCOME YOU').trigger('change');
163 | view.$el.find('#forgotpassword-form').trigger('submit');
164 | });
165 |
166 | it('should alert an error', function () {
167 | expect(notification.alert).toHaveBeenCalledWith('Invalid answer.', 'Error', 'Ok');
168 | });
169 |
170 | it('the verify button should be enabled', function () {
171 | expect(view.$el.find('#verify')).not.toBeDisabled();
172 | });
173 | });
174 |
175 | describe('with valid security answer', function () {
176 |
177 | beforeEach(function () {
178 | spyOn(session, 'store');
179 | spyOn(view, 'navigateTo').and.callThrough();
180 |
181 | view.$el.find('#security-answer').val(persistedModel.get('securityAnswer')).trigger('change');
182 | view.$el.find('#forgotpassword-form').trigger('submit');
183 | });
184 |
185 | it('the submit button should be disabled', function () {
186 | expect(view.$el.find('#verify')).toBeDisabled();
187 | });
188 |
189 | it('should update the state session variable as VERIFIED', function () {
190 | expect(session.store).toHaveBeenCalledWith('state', 'VERIFIED');
191 | });
192 |
193 | it('should navigate to chnagepassword page', function () {
194 | expect(view.navigateTo).toHaveBeenCalledWith('#changepassword');
195 | });
196 | });
197 | });
198 |
199 | });
200 |
201 | });
202 | });
--------------------------------------------------------------------------------
/tests/homespec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'backbone',
4 | 'extensions',
5 | 'adapters/notification',
6 | 'models/photo',
7 | 'collections/photos',
8 | 'views/home',
9 | 'templates'
10 | ], function (
11 | $,
12 | Backbone,
13 | extensions,
14 | notification,
15 | Photo,
16 | Photos,
17 | HomeView,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | describe('home view', function () {
24 |
25 | var view, collection, model;
26 |
27 | beforeEach(function () {
28 | collection = new Photos([
29 | new Photo({
30 | description: 'HDFC Bank CreditCard',
31 | thumbnail: 'somecrazybase64string1...',
32 | lastSaved: 'Wed Mar 10 2015 13:15:34 GMT+0530 (India Standard Time)'
33 | }),
34 | new Photo({
35 | description: 'ICICI CreditCard',
36 | thumbnail: 'somecrazybase64string2...',
37 | lastSaved: 'Wed Mar 11 2015 13:15:34 GMT+0530 (India Standard Time)'
38 | })
39 | ]);
40 |
41 | model = new Backbone.Model({
42 | search: '',
43 | total: collection.length
44 | });
45 |
46 | view = new HomeView({
47 | collection: collection,
48 | model: model
49 | });
50 | });
51 |
52 | afterEach(function () {
53 | if (view) {
54 | view.remove();
55 | }
56 | });
57 |
58 | describe('when constructed', function () {
59 |
60 | describe('without collection', function () {
61 | it('should throw error', function () {
62 | expect(function () {
63 | new HomeView();
64 | }).toThrow(new Error('collection is required'));
65 | });
66 | });
67 |
68 | describe('without model', function () {
69 | it('should throw error', function () {
70 | expect(function () {
71 | new HomeView({
72 | collection: new Photos()
73 | });
74 | }).toThrow(new Error('model is required'));
75 | });
76 | });
77 |
78 | describe('with required arguments', function () {
79 | it('should exist with properties initialized', function () {
80 | expect(view).toBeDefined();
81 | expect(view.collection).toBe(collection);
82 | expect(view.model).toBe(model);
83 | expect(view.template).toBe(templates.home);
84 | });
85 | });
86 |
87 | });
88 |
89 | describe('when rendered', function () {
90 |
91 | describe('with empty collection', function () {
92 |
93 | beforeEach(function () {
94 | view = new HomeView({
95 | collection: new Photos(),
96 | model: new Backbone.Model({
97 | search: '',
98 | total: 0
99 | })
100 | });
101 |
102 | view.render();
103 | });
104 |
105 | it('should not display search form', function () {
106 | expect(view.$el).not.toContainElement('#search-form');
107 | });
108 |
109 | it('should display total photos count', function () {
110 | expect(view.$el.find('#total')).toHaveText('No photos found');
111 | });
112 |
113 | it('should contain the list empty', function () {
114 | expect(view.$el.find('#list-container ul')).toBeEmpty();
115 | });
116 | });
117 |
118 | describe('with non-empty collection', function () {
119 |
120 | beforeEach(function () {
121 | view.render();
122 | });
123 |
124 | it('should display search form', function () {
125 | expect(view.$el).toContainElement('#search-form');
126 | });
127 |
128 | it('should display total photos count', function () {
129 | expect(view.$el.find('#total')).toHaveText('2 photo(s) found');
130 | });
131 |
132 | it('should contain the list with the items', function () {
133 | expect(view.$el.find('#list-container ul')).not.toBeEmpty();
134 | expect(view.$el.find('#list-container ul li').length).toBe(2);
135 | });
136 | });
137 | });
138 |
139 | describe('when changes', function () {
140 |
141 | beforeEach(function () {
142 | view.render();
143 | });
144 |
145 | describe('with search text that not matches', function () {
146 |
147 | beforeEach(function () {
148 | view.$el.find('#search').val('photo').trigger('change');
149 | });
150 |
151 | it('should clear the list', function () {
152 | expect(view.$el.find('#list-container ul')).toBeEmpty();
153 | });
154 |
155 | it('should update the total text', function () {
156 | expect(view.$el.find('#total')).toHaveText('No photos found');
157 | });
158 | });
159 |
160 | describe('with search text that matches', function () {
161 |
162 | beforeEach(function () {
163 | view.$el.find('#search').val('hdfc').trigger('change');
164 | });
165 |
166 | it('should update the list', function () {
167 | expect(view.$el.find('#list-container ul li').length).toBe(1);
168 | });
169 |
170 | it('should update the total text', function () {
171 | expect(view.$el.find('#total')).toHaveText('1 photo(s) found');
172 | });
173 | });
174 |
175 | describe('with empty search text', function () {
176 |
177 | beforeEach(function () {
178 | view.$el.find('#search').val('').trigger('change');
179 | });
180 |
181 | it('should re-render the list with all items', function () {
182 | expect(view.$el.find('#list-container ul li').length).toBe(2);
183 | });
184 |
185 | it('should update the total text', function () {
186 | expect(view.$el.find('#total')).toHaveText('2 photo(s) found');
187 | });
188 | });
189 | });
190 | });
191 |
192 | });
--------------------------------------------------------------------------------
/tests/loginspec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'session',
6 | 'adapters/notification',
7 | 'models/credential',
8 | 'views/login',
9 | 'templates'
10 | ], function (
11 | $,
12 | _,
13 | extensions,
14 | session,
15 | notification,
16 | Credential,
17 | LoginView,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | describe('login view', function () {
24 |
25 | var view,
26 | model,
27 | persistedModel;
28 |
29 | beforeEach(function () {
30 |
31 | spyOn(notification, 'alert').and.callFake(function () {
32 | return _.resolve();
33 | });
34 |
35 | model = new Credential();
36 |
37 | persistedModel = new Credential({
38 | id: 'Safe-Credential'
39 | });
40 |
41 | view = new LoginView({
42 | model: model,
43 | credential: persistedModel
44 | });
45 | });
46 |
47 | afterEach(function () {
48 | if (view) {
49 | view.remove();
50 | }
51 | });
52 |
53 | describe('when constructed', function () {
54 |
55 | describe('without model', function () {
56 | it('should throw error', function () {
57 | expect(function () {
58 | new LoginView();
59 | }).toThrow(new Error('model is required'));
60 | });
61 | });
62 |
63 | describe('without credential', function () {
64 | it('should throw error', function () {
65 | expect(function () {
66 | new LoginView({
67 | model: model
68 | });
69 | }).toThrow(new Error('credential is required'));
70 | });
71 | });
72 |
73 | describe('with required arguments', function () {
74 | it('should exist with properties initialized', function () {
75 | expect(view).toBeDefined();
76 | expect(view.model).toBe(model);
77 | expect(view.credential).toBe(persistedModel);
78 | expect(view.template).toBe(templates.login);
79 | });
80 | });
81 | });
82 |
83 | describe('when rendered', function () {
84 |
85 | beforeEach(function () {
86 | view.render();
87 | });
88 |
89 | it('should contain password textbox empty, maxlength 15 and autocomplete off', function () {
90 | var $password = view.$el.find('#password');
91 |
92 | expect($password).toHaveValue('');
93 | expect($password).toHaveAttr('maxlength', '15');
94 | expect($password).toHaveAttr('autocomplete', 'off');
95 | });
96 |
97 | it('should contain submit button as disabled', function () {
98 | expect(view.$el.find('#login')).toBeDisabled();
99 | });
100 |
101 | it('should contain the forgot password link', function () {
102 | expect(view.$el.find('.forgot-password')).toHaveAttr('href', '#forgotpassword');
103 | });
104 | });
105 |
106 | describe('when changes', function () {
107 |
108 | beforeEach(function () {
109 | view.render();
110 | });
111 |
112 | describe('with the password textbox empty', function () {
113 |
114 | beforeEach(function () {
115 | view.$el.find('#password').val('').trigger('change');
116 | });
117 |
118 | it('then password property of the model should be empty', function () {
119 | expect(view.model.get('password')).toBe('');
120 | });
121 |
122 | it('then the submit button should be disabled', function () {
123 | expect(view.$el.find('#login')).toBeDisabled();
124 | });
125 | });
126 |
127 | describe('with the password textbox not empty', function () {
128 |
129 | beforeEach(function () {
130 | view.$el.find('#password').val('S@f3').trigger('change');
131 | });
132 |
133 | it('then the password property of the model should not be empty', function () {
134 | expect(view.model.get('password')).toBe('S@f3');
135 | });
136 |
137 | it('then the submit button should be enabled', function () {
138 | expect(view.$el.find('#login')).not.toBeDisabled();
139 | });
140 | });
141 |
142 | describe('when form is submitted', function () {
143 |
144 | describe('without any password', function () {
145 |
146 | beforeEach(function () {
147 | view.$el.find('#password').val('').trigger('change');
148 | view.$el.find('#login-form').trigger('submit');
149 | });
150 |
151 | it('should alert an error', function () {
152 | expect(notification.alert).toHaveBeenCalledWith('Enter the password.', 'Error', 'Ok');
153 | });
154 | });
155 |
156 | describe('with invalid password', function () {
157 |
158 | describe('and fetch failed', function () {
159 |
160 | beforeEach(function () {
161 | spyOn(persistedModel, 'fetch').and.callFake(function () {
162 | return _.reject();
163 | });
164 |
165 | view.$el.find('#password').val('pass123').trigger('change');
166 | view.$el.find('#login-form').trigger('submit');
167 | });
168 |
169 | it('should alert an error', function () {
170 | expect(notification.alert).toHaveBeenCalledWith('Failed to retrieve credential. Please try again.', 'Error', 'Ok');
171 | });
172 |
173 | it('the submit button should be enabled', function () {
174 | expect(view.$el.find('#login')).not.toBeDisabled();
175 | });
176 | });
177 |
178 | describe('and fetch succeeded', function () {
179 |
180 | beforeEach(function () {
181 | spyOn(persistedModel, 'fetch').and.callFake(function () {
182 | var d = $.Deferred();
183 | persistedModel.set({
184 | password: 'S@f3',
185 | key: '007',
186 | securityQuestion: 'CODE WELCOME',
187 | securityAnswer: 'CODE GET LOST'
188 | });
189 | d.resolve();
190 | return d.promise();
191 | });
192 |
193 | view.$el.find('#password').val('pass123').trigger('change');
194 | view.$el.find('#login-form').trigger('submit');
195 | });
196 |
197 | it('should alert an error', function () {
198 | expect(notification.alert).toHaveBeenCalledWith('Invalid password.', 'Error', 'Ok');
199 | });
200 |
201 | it('the submit button should be enabled', function () {
202 | expect(view.$el.find('#login')).not.toBeDisabled();
203 | });
204 | });
205 | });
206 |
207 | describe('with valid password and fetch suceeded', function () {
208 |
209 | beforeEach(function () {
210 | spyOn(persistedModel, 'fetch').and.callFake(function () {
211 | var d = $.Deferred();
212 | persistedModel.set({
213 | password: 'S@f3',
214 | key: '007',
215 | securityQuestion: 'CODE WELCOME',
216 | securityAnswer: 'CODE GET LOST'
217 | });
218 | d.resolve();
219 | return d.promise();
220 | });
221 |
222 | spyOn(session, 'store');
223 | spyOn(view, 'navigateTo');
224 |
225 | view.$el.find('#password').val('S@f3').trigger('change');
226 | view.$el.find('#login-form').trigger('submit');
227 | });
228 |
229 | it('the submit button should be disabled', function () {
230 | expect(view.$el.find('#login')).toBeDisabled();
231 | });
232 |
233 | it('should update the state session variable as LOGGED_IN', function () {
234 | expect(session.store).toHaveBeenCalledWith('state', 'LOGGED_IN');
235 | });
236 |
237 | it('should store the encryption/decryption key in session', function () {
238 | expect(session.store).toHaveBeenCalledWith('key', '007');
239 | });
240 |
241 | it('should return back to photos page', function () {
242 | expect(view.navigateTo).toHaveBeenCalledWith('#photos');
243 | });
244 | });
245 | });
246 |
247 | });
248 |
249 | });
250 | });
--------------------------------------------------------------------------------
/tests/main.js:
--------------------------------------------------------------------------------
1 | (function () {
2 |
3 | 'use strict';
4 |
5 | var allTestFiles = [];
6 | var TEST_REGEXP = /(spec|test)\.js$/i;
7 |
8 | var pathToModule = function (path) {
9 | return '../../' + path.replace(/^\/base\//, '').replace(/\.js$/, '');
10 | };
11 |
12 | Object.keys(window.__karma__.files).forEach(function (file) {
13 | if (TEST_REGEXP.test(file)) {
14 | // Normalize paths to RequireJS module names.
15 | allTestFiles.push(pathToModule(file));
16 | }
17 | });
18 |
19 | require.config({
20 | baseUrl: '/base/src/js',
21 |
22 | paths: {
23 | jquery: '../../bower_components/jquery/dist/jquery',
24 | underscore: '../../bower_components/underscore/underscore',
25 | backbone: '../../bower_components/backbone/backbone',
26 | validation: '../../bower_components/backbone.validation/dist/backbone-validation-amd',
27 | stickit: '../../bower_components/backbone.stickit/backbone.stickit',
28 | touch: '../../bower_components/backbone.touch/backbone.touch',
29 | handlebars: '../../bower_components/handlebars/handlebars.runtime',
30 | almond: '../../bower_components/almond/almond'
31 | },
32 |
33 | shim: {
34 | jquery: {
35 | exports: '$'
36 | },
37 |
38 | underscore: {
39 | deps: ['jquery'],
40 | exports: '_'
41 | },
42 |
43 | backbone: {
44 | deps: ['underscore', 'jquery'],
45 | exports: 'Backbone'
46 | },
47 |
48 | handlebars: {
49 | exports: 'Handlebars'
50 | }
51 | },
52 |
53 | // dynamically load all test files
54 | deps: allTestFiles,
55 |
56 | // we have to kickoff jasmine, as it is asynchronous
57 | callback: window.__karma__.start
58 | });
59 | })();
60 |
--------------------------------------------------------------------------------
/tests/photospec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'adapters/notification',
6 | 'models/photo',
7 | 'models/file',
8 | 'views/photo',
9 | 'templates'
10 | ], function (
11 | $,
12 | _,
13 | extensions,
14 | notification,
15 | Photo,
16 | File,
17 | PhotoView,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | describe('photo view', function () {
24 |
25 | var view, model, file;
26 |
27 | beforeEach(function () {
28 |
29 | spyOn(notification, 'alert').and.callFake(function () {
30 | return _.resolve();
31 | });
32 |
33 | model = new Photo({
34 | id: '1',
35 | description: 'River-side property document',
36 | thumbnail: '[thumbnail]',
37 | lastSaved: 'Fri Mar 06 2015 18:10:23 GMT+0530 (India Standard Time)'
38 | });
39 |
40 | file = new File({
41 | id: model.id
42 | });
43 |
44 | view = new PhotoView({
45 | model: model,
46 | file: file
47 | });
48 | });
49 |
50 | afterEach(function () {
51 | if (view) {
52 | view.remove();
53 | }
54 | });
55 |
56 | describe('when constructed', function () {
57 |
58 | describe('without model', function () {
59 | it('should throw error', function () {
60 | expect(function () {
61 | new PhotoView();
62 | }).toThrow(new Error('model is required'));
63 | });
64 | });
65 |
66 | describe('without file model', function () {
67 | it('should throw error', function () {
68 | expect(function () {
69 | new PhotoView({
70 | model: model
71 | });
72 | }).toThrow(new Error('file is required'));
73 | });
74 | });
75 |
76 | describe('with required arguments', function () {
77 | it('should exist with properties initialized', function () {
78 | expect(view).toBeDefined();
79 | expect(view.model).toBe(model);
80 | expect(view.file).toBe(file);
81 | expect(view.template).toBe(templates.photo);
82 | });
83 | });
84 | });
85 |
86 | describe('when rendered', function () {
87 |
88 | beforeEach(function () {
89 | spyOn(file, 'fetch').and.callFake(function () {
90 | return _.resolve();
91 | });
92 | view.render();
93 | });
94 |
95 | it('should display description', function () {
96 | expect(view.$el.find('.description')).toHaveText(model.get('description'));
97 | });
98 | });
99 |
100 | describe('when failed to load the image', function () {
101 |
102 | beforeEach(function () {
103 | spyOn(file, 'fetch').and.callFake(function () {
104 | var d = $.Deferred();
105 | d.reject();
106 | return d.promise();
107 | });
108 |
109 | view.render();
110 | });
111 |
112 | it('should alert the error to the user', function () {
113 | expect(notification.alert).toHaveBeenCalledWith('Failed to load image. Please try again.', 'Error', 'Ok');
114 | });
115 | });
116 |
117 | describe('when sucessfully loading the image', function () {
118 |
119 | var base64Image = '[SOME CRAZY IMAGE]';
120 |
121 | beforeEach(function () {
122 | spyOn(file, 'fetch').and.callFake(function () {
123 | var d = $.Deferred();
124 | file.set('data', base64Image);
125 | d.resolve();
126 | return d.promise();
127 | });
128 |
129 | view.render();
130 | });
131 |
132 | it('should update the base 64 string in the image tag', function () {
133 | expect(view.$el.find('#photo')).toHaveAttr('src', 'data:image/png;base64,' + base64Image);
134 | });
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/tests/registerspec.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'underscore',
4 | 'extensions',
5 | 'session',
6 | 'adapters/notification',
7 | 'models/credential',
8 | 'views/register',
9 | 'templates'
10 | ], function (
11 | $,
12 | _,
13 | extensions,
14 | session,
15 | notification,
16 | Credential,
17 | RegisterView,
18 | templates
19 | ) {
20 |
21 | 'use strict';
22 |
23 | describe('register view', function () {
24 |
25 | var view,
26 | model,
27 | securityQuestions = ['Where is eiffel tower?', 'Who directed Titanic?'];
28 |
29 | beforeEach(function () {
30 | spyOn(notification, 'alert').and.callFake(function () {
31 | return _.resolve();
32 | });
33 |
34 | model = new Credential();
35 |
36 | view = new RegisterView({
37 | model: model,
38 | securityQuestions: securityQuestions
39 | });
40 | });
41 |
42 | afterEach(function () {
43 | if (view) {
44 | view.remove();
45 | }
46 | });
47 |
48 | describe('when constructed', function () {
49 |
50 | describe('without model', function () {
51 | it('should throw error', function () {
52 | expect(function () {
53 | new RegisterView();
54 | }).toThrow(new Error('model is required'));
55 | });
56 | });
57 |
58 | describe('without security questions', function () {
59 | it('should throw error', function () {
60 | expect(function () {
61 | new RegisterView({
62 | model: model
63 | });
64 | }).toThrow(new Error('security questions required'));
65 | });
66 | });
67 |
68 | describe('with required arguments', function () {
69 | it('should exist with properties initialized', function () {
70 | expect(view).toBeDefined();
71 | expect(view.model).toBe(model);
72 | expect(view.securityQuestions).toBe(securityQuestions);
73 | expect(view.template).toBe(templates.register);
74 | });
75 | });
76 |
77 | });
78 |
79 | describe('when rendered', function () {
80 |
81 | beforeEach(function () {
82 | view.render();
83 | });
84 |
85 | it('should contain password textbox empty with maxlength 15', function () {
86 | var $password = view.$el.find('#password');
87 |
88 | expect($password).toHaveValue('');
89 | expect($password).toHaveAttr('maxlength', '15');
90 | expect($password).toHaveAttr('autocomplete', 'off');
91 | });
92 |
93 | it('should contain security questions dropdown with options of all passed questions', function () {
94 | expect(view.$el.find('#security-question option').length).toBe(securityQuestions.length + 1);
95 | expect(view.$el.find('#security-question option:eq(0)')).toBeSelected();
96 | expect(view.$el.find('#security-question option:eq(0)')).toHaveText('Select a question');
97 |
98 | for (var i = 0; i < securityQuestions.length; i++) {
99 | var $option = view.$el.find('#security-question option:eq(' + (i + 1) + ')');
100 | expect($option).toHaveText(securityQuestions[i]);
101 | }
102 | });
103 |
104 | it('should contain the answer textbox empty, disabled, maxlength 15 and autocomplete off', function () {
105 | var $securityanswer = view.$el.find('#security-answer');
106 |
107 | expect($securityanswer).toHaveValue('');
108 | expect($securityanswer).toHaveAttr('maxlength', '15');
109 | expect($securityanswer).toHaveAttr('autocomplete', 'off');
110 | expect($securityanswer).toBeDisabled();
111 | });
112 |
113 | it('should contain submit button as disabled', function () {
114 | expect(view.$el.find('#register')).toBeDisabled();
115 | });
116 | });
117 |
118 | describe('when changes', function () {
119 |
120 | beforeEach(function () {
121 | view.render();
122 | });
123 |
124 | describe('with no security question is selected', function () {
125 |
126 | beforeEach(function () {
127 | view.$el.find('#security-question').val('Select a question').trigger('change');
128 | });
129 |
130 | it('then the answer textbox should be empty and disabled', function () {
131 | var $securityanswer = view.$el.find('#security-answer');
132 |
133 | expect($securityanswer).toHaveValue('');
134 | expect($securityanswer).toBeDisabled();
135 | });
136 | });
137 |
138 | describe('with security question is selected', function () {
139 |
140 | beforeEach(function () {
141 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
142 | });
143 |
144 | it('then the answer textbox should be enabled', function () {
145 | expect(view.$el.find('#security-answer')).not.toBeDisabled();
146 | });
147 | });
148 |
149 | describe('with all fields not filled', function () {
150 | beforeEach(function () {
151 | view.$el.find('#password').val('S@f5').trigger('change');
152 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
153 | });
154 |
155 | it('then the register button should be disabled', function () {
156 | expect(view.$el.find('#register')).toBeDisabled();
157 | });
158 | });
159 |
160 | describe('with all fields filled', function () {
161 |
162 | beforeEach(function () {
163 | view.$el.find('#password').val('S@f5').trigger('change');
164 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
165 | view.$el.find('#security-answer').val('France').trigger('change');
166 | });
167 |
168 | it('then the register button should be enabled', function () {
169 | expect(view.$el.find('#register')).not.toBeDisabled();
170 | });
171 | });
172 |
173 |
174 | describe('when form is submitted', function () {
175 |
176 | describe('without all fields filled', function () {
177 |
178 | beforeEach(function () {
179 | view.$el.find('#password').val('S@f5').trigger('change');
180 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
181 | view.$el.find('#register-form').trigger('submit');
182 |
183 | spyOn(view.model, 'save');
184 | });
185 |
186 | it('should alert an error', function () {
187 | expect(notification.alert).toHaveBeenCalledWith('Fill all the fields.', 'Error', 'Ok');
188 | });
189 |
190 | it('should not call model.save', function () {
191 | expect(view.model.save).not.toHaveBeenCalled();
192 | });
193 | });
194 |
195 | describe('with all the fields filled', function () {
196 |
197 | beforeEach(function () {
198 | view.$el.find('#password').val('S@f5').trigger('change');
199 | view.$el.find('#security-question').val('Where is eiffel tower?').trigger('change');
200 | view.$el.find('#security-answer').val('France').trigger('change');
201 | });
202 |
203 | describe('and failed to save', function () {
204 |
205 | beforeEach(function () {
206 | spyOn(view.model, 'save').and.callFake(function () {
207 | return _.reject();
208 | });
209 |
210 | view.$el.find('#register-form').trigger('submit');
211 | });
212 |
213 | it('should display notification', function () {
214 | expect(notification.alert).toHaveBeenCalledWith('Registration failed. Please try again.', 'Error', 'Ok');
215 | });
216 |
217 | it('should enable back the register button', function () {
218 | expect(view.$el.find('#register')).not.toBeDisabled();
219 | });
220 | });
221 |
222 | describe('and save succeeded', function () {
223 |
224 | beforeEach(function () {
225 | spyOn(view, 'navigateTo');
226 |
227 | spyOn(view.model, 'save').and.callFake(function () {
228 | return _.resolve();
229 | });
230 |
231 | spyOn(session, 'store');
232 |
233 | view.$el.find('#register-form').trigger('submit');
234 | });
235 |
236 | it('should update the state session variable to LOGGED_OUT', function () {
237 | expect(session.store).toHaveBeenCalledWith('state', 'LOGGED_OUT');
238 | });
239 |
240 | it('should navigate to login page', function () {
241 | expect(view.navigateTo).toHaveBeenCalledWith('#login');
242 | });
243 | });
244 | });
245 |
246 | });
247 | });
248 |
249 | });
250 |
251 | });
--------------------------------------------------------------------------------