diff --git a/README.md b/README.md index acfdbf0..c8613e0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Telegram bot for selecting movies to watch together. - `/vote ` - Vote for a suggested movie. - `/list` - Show the list of suggested movies. - `/watched ` - Mark a movie as watched. +- `/random` - Show a random movie from list. - `/veto ` - Remove movie from random. ## Development diff --git a/coverage/badge-branches.svg b/coverage/badge-branches.svg index 8e2ef1e..8577e9e 100644 --- a/coverage/badge-branches.svg +++ b/coverage/badge-branches.svg @@ -1 +1 @@ -Coverage: 77.04%Coverage77.04% \ No newline at end of file +Coverage: 81.39%Coverage81.39% \ No newline at end of file diff --git a/coverage/badge-lines.svg b/coverage/badge-lines.svg index 96def5b..0f6e375 100644 --- a/coverage/badge-lines.svg +++ b/coverage/badge-lines.svg @@ -1 +1 @@ -Coverage: 93.18%Coverage93.18% \ No newline at end of file +Coverage: 98.47%Coverage98.47% \ No newline at end of file diff --git a/coverage/badge-statements.svg b/coverage/badge-statements.svg index cfba62b..20b86f3 100644 --- a/coverage/badge-statements.svg +++ b/coverage/badge-statements.svg @@ -1 +1 @@ -Coverage: 93.65%Coverage93.65% \ No newline at end of file +Coverage: 98.61%Coverage98.61% \ No newline at end of file diff --git a/coverage/coverage-summary.json b/coverage/coverage-summary.json index 8d71836..86b55bb 100644 --- a/coverage/coverage-summary.json +++ b/coverage/coverage-summary.json @@ -1,9 +1,13 @@ -{"total": {"lines":{"total":176,"covered":164,"skipped":0,"pct":93.18},"statements":{"total":189,"covered":177,"skipped":0,"pct":93.65},"functions":{"total":46,"covered":46,"skipped":0,"pct":100},"branches":{"total":61,"covered":47,"skipped":0,"pct":77.04},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"/Users/gpont/Documents/watch_together/src/controllers/bot.ts": {"lines":{"total":120,"covered":108,"skipped":0,"pct":90},"functions":{"total":30,"covered":30,"skipped":0,"pct":100},"statements":{"total":130,"covered":118,"skipped":0,"pct":90.76},"branches":{"total":55,"covered":43,"skipped":0,"pct":78.18}} +{"total": {"lines":{"total":197,"covered":194,"skipped":0,"pct":98.47},"statements":{"total":217,"covered":214,"skipped":0,"pct":98.61},"functions":{"total":67,"covered":67,"skipped":0,"pct":100},"branches":{"total":43,"covered":35,"skipped":0,"pct":81.39},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} +,"/Users/gpont/Documents/watch_together/src/controllers/bot.ts": {"lines":{"total":80,"covered":77,"skipped":0,"pct":96.25},"functions":{"total":31,"covered":31,"skipped":0,"pct":100},"statements":{"total":90,"covered":87,"skipped":0,"pct":96.66},"branches":{"total":13,"covered":9,"skipped":0,"pct":69.23}} +,"/Users/gpont/Documents/watch_together/src/controllers/checkers.ts": {"lines":{"total":36,"covered":36,"skipped":0,"pct":100},"functions":{"total":9,"covered":9,"skipped":0,"pct":100},"statements":{"total":41,"covered":41,"skipped":0,"pct":100},"branches":{"total":16,"covered":15,"skipped":0,"pct":93.75}} ,"/Users/gpont/Documents/watch_together/src/controllers/helpers.ts": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":10,"covered":10,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/gpont/Documents/watch_together/src/dbController/database.ts": {"lines":{"total":8,"covered":8,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":8,"covered":8,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/gpont/Documents/watch_together/src/dbController/index.ts": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} +,"/Users/gpont/Documents/watch_together/src/errorHandler/CheckError.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} +,"/Users/gpont/Documents/watch_together/src/errorHandler/handleCheckErrors.ts": {"lines":{"total":10,"covered":10,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":11,"covered":11,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} +,"/Users/gpont/Documents/watch_together/src/errorHandler/index.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/gpont/Documents/watch_together/src/models/index.ts": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/gpont/Documents/watch_together/src/models/movies/index.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"/Users/gpont/Documents/watch_together/src/models/movies/model.ts": {"lines":{"total":37,"covered":37,"skipped":0,"pct":100},"functions":{"total":11,"covered":11,"skipped":0,"pct":100},"statements":{"total":37,"covered":37,"skipped":0,"pct":100},"branches":{"total":6,"covered":4,"skipped":0,"pct":66.66}} +,"/Users/gpont/Documents/watch_together/src/models/movies/model.ts": {"lines":{"total":47,"covered":47,"skipped":0,"pct":100},"functions":{"total":17,"covered":17,"skipped":0,"pct":100},"statements":{"total":48,"covered":48,"skipped":0,"pct":100},"branches":{"total":12,"covered":9,"skipped":0,"pct":75}} } diff --git a/coverage/lcov-report/controllers/bot.ts.html b/coverage/lcov-report/controllers/bot.ts.html index b96ebcd..675051e 100644 --- a/coverage/lcov-report/controllers/bot.ts.html +++ b/coverage/lcov-report/controllers/bot.ts.html @@ -23,30 +23,30 @@

All files / controllers
- 90.76% + 96.66% Statements - 118/130 + 87/90
- 78.18% + 69.23% Branches - 43/55 + 9/13
100% Functions - 30/30 + 31/31
- 90% + 96.25% Lines - 108/120 + 77/80
@@ -256,70 +256,7 @@

All files / controllers 191 192 193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257  +194      1x @@ -335,10 +272,13 @@

All files / controllers       -  -  1x 1x +1x +  +1x +  +        @@ -356,6 +296,15 @@

All files / controllers 1x 1x   +1x +1x +  +  +  +  +  +1x +        @@ -375,19 +324,8 @@

All files / controllers   4x 4x -  4x -1x -1x -  -  -3x 3x -1x -1x -  -  -2x   2x 2x @@ -395,39 +333,17 @@

All files / controllers 1x   1x +1x         -  -3x 3x 3x -  3x -1x -1x -    2x 1x -1x -  -  -1x -  -1x -1x -  -1x -  -  -  -  -1x -  -  -    1x   @@ -449,29 +365,24 @@

All files / controllers       -3x -3x +5x +5x +5x   -3x -  -      -3x -  -3x -3x -2x -2x   -1x +4x +4x +4x   -1x +3x 1x 1x   -    +2x +2x       @@ -480,76 +391,44 @@

All files / controllers 2x   2x -2x -1x 1x     1x -  1x   -1x -1x -  -  -  -        4x 4x -  4x -1x -1x     -3x -  -3x -3x -1x -1x     +3x 2x   -2x 1x 1x   -1x -  -        3x 3x -  3x -1x -1x     -2x -2x -  -      2x 2x   -2x 1x 1x   -1x -  -        @@ -557,59 +436,66 @@

All files / controllers 3x   3x -3x -1x -1x -  -  2x   -2x 1x     1x   -1x       +1x +10x    
// Fix for error: node-telegram-bot-api
 // deprecated Automatic enabling of cancellation
 // of promises is deprecated In the future
 process.env.NTBA_FIX_319 = '1';
-import TelegramBot, { SendMessageOptions } from 'node-telegram-bot-api';
+import { SendMessageOptions } from 'node-telegram-bot-api';
 import {
   createGroup,
   findGroupByCode,
   createUser,
   addUserToGroup,
   suggestMovie,
-  findMovieById,
   voteForMovie,
-  listMovies,
   markMovieAsWatched,
   markMovieAsVetoed,
-  findUserById,
+  hasUserMovieVote,
 } from '../models';
 import texts from '../texts.json';
 import { getImdbUrl, getKinopoiskUrl, getMovieDescription } from './helpers';
- 
-type THandler = (
-  bot: TelegramBot,
-) => (msg: TelegramBot.Message, match: string[] | null) => void;
+import { CheckError, handleCheckErrors } from '../errorHandler';
+import { THandler } from '../controllersTypes';
+import {
+  checkAndGetArg,
+  checkAndGetGroup,
+  checkAndGetMovie,
+  checkAndGetUserByUsername,
+  checkAndGetMoviesList,
+} from './checkers';
  
 const MSG_OPTIONS: SendMessageOptions = {
   disable_web_page_preview: true,
   parse_mode: 'Markdown',
 };
  
-export const botHandlers: [RegExp, THandler][] = [
+const rawBotHandlers: [RegExp, THandler][] = [
   [
     /\/start/,
     (bot) => async (msg) => {
-      await createUser();
+      try {
+        await checkAndGetUserByUsername(msg);
+      } catch (error) {
+        if (error instanceof CheckError) {
+          await createUser(msg.from?.username ?? '');
+        } else E{
+          throw error;
+        }
+      }
+ 
       bot.sendMessage(msg.chat.id, texts.start);
     },
   ],
@@ -631,64 +517,31 @@ 

All files / controllers /\/join_group (.*)/, (bot) => async (msg, match) => { const chatId = msg.chat.id; -  - if (!match?.[1]) { - bot.sendMessage(chatId, texts.no_group_code); - return; - } -  - const userId = msg.from?.id; - if (!userId) { - bot.sendMessage(chatId, texts.user_not_found); - return; - } -  - const groupCode = match.slice(1).join(' '); + const groupCode = checkAndGetArg(match, texts.no_group_code); + const user = await checkAndGetUserByUsername(msg);   const group = await findGroupByCode(groupCode); - if (group) { - await addUserToGroup(group.id, userId); - bot.sendMessage(chatId, texts.joined_group); - } else { + if (!group) { bot.sendMessage(chatId, texts.group_not_found); + return; } + await addUserToGroup(group.id, user.id); + bot.sendMessage(chatId, texts.joined_group); }, ], [ /\/suggest (.*)/, (bot) => async (msg, match) => { const chatId = msg.chat.id; - const userId = msg.from?.id; + const movieName = checkAndGetArg(match, texts.no_movie_name);   - if (!match?.[1]) { - bot.sendMessage(chatId, texts.no_movie_name); - return; - } -  - if (!userId) { - bot.sendMessage(chatId, texts.user_not_found); - return; - } -  - const movieName = match.slice(1).join(' '); -  - const user = await findUserById(userId); - const groupUser = await findGroupByCode(String(chatId)); -  - Iif (!user) { - bot.sendMessage(chatId, texts.user_not_found); - return; - } -  - Iif (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); - return; - } + const user = await checkAndGetUserByUsername(msg); + const group = await checkAndGetGroup(msg);   const movie = await suggestMovie( movieName, user.id, - groupUser.id, + group.id, getKinopoiskUrl(movieName), getImdbUrl(movieName), ); @@ -707,27 +560,22 @@

All files / controllers /\/vote ?(.*)/, (bot) => async (msg, match) => { const chatId = msg.chat.id; + const movieId = parseInt( + checkAndGetArg(match, texts.movie_not_found), + 10, + );   - Iif (!match) { - bot.sendMessage(chatId, texts.movie_not_found); - return; - } -  - const movieId = parseInt(match.slice(1).join(''), 10); + const user = await checkAndGetUserByUsername(msg); + const group = await checkAndGetGroup(msg); + const movie = await checkAndGetMovie(movieId, group.id);   - const groupUser = await findGroupByCode(String(chatId)); - if (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); + if (await hasUserMovieVote(user.id, movieId)) { + bot.sendMessage(chatId, texts.already_voted); return; } - const movie = await findMovieById(movieId, groupUser.id);   - if (movie) { - await voteForMovie(movieId); - bot.sendMessage(chatId, `${texts.voted} "${movie.name}"`); - } else E{ - bot.sendMessage(chatId, texts.movie_not_found); - } + await voteForMovie(movieId, user.id); + bot.sendMessage(chatId, `${texts.voted} "${movie.name}"`); }, ], [ @@ -735,76 +583,44 @@

All files / controllers (bot) => async (msg) => { const chatId = msg.chat.id;   - const groupUser = await findGroupByCode(String(chatId)); - if (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); - return; - } -  - const movies = await listMovies(groupUser.id); + const group = await checkAndGetGroup(msg); + const movies = await checkAndGetMoviesList(group.id);   - if (!!movies && movies.length > 0) { - const movieList = - texts.movie_list + movies.map(getMovieDescription).join(''); - bot.sendMessage(chatId, movieList, MSG_OPTIONS); - } else E{ - bot.sendMessage(chatId, texts.movie_list_empty); - } + const movieList = + texts.movie_list + movies.map(getMovieDescription).join(''); + bot.sendMessage(chatId, movieList, MSG_OPTIONS); }, ], [ /\/watched (.*)/, (bot) => async (msg, match) => { const chatId = msg.chat.id; + const movieId = parseInt( + checkAndGetArg(match, texts.movie_not_found), + 10, + );   - if (!match?.[1]) { - bot.sendMessage(chatId, texts.movie_not_found); - return; - } -  - const movieId = parseInt(match.slice(1).join(''), 10); -  - const groupUser = await findGroupByCode(String(chatId)); - if (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); - return; - } -  - const movie = await findMovieById(movieId, groupUser.id); + const group = await checkAndGetGroup(msg); + const movie = await checkAndGetMovie(movieId, group.id);   - if (movie) { - await markMovieAsWatched(movieId, groupUser.id); - bot.sendMessage(chatId, `${texts.movie_watched} "${movie.name}"`); - } else { - bot.sendMessage(chatId, texts.movie_not_found); - } + await markMovieAsWatched(movieId, group.id); + bot.sendMessage(chatId, `${texts.movie_watched} "${movie.name}"`); }, ], [ /\/veto (.*)/, (bot) => async (msg, match) => { const chatId = msg.chat.id; + const movieId = parseInt( + checkAndGetArg(match, texts.movie_not_found), + 10, + );   - if (!match?.[1]) { - bot.sendMessage(chatId, texts.movie_not_found); - return; - } -  - const groupUser = await findGroupByCode(String(chatId)); - Iif (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); - return; - } -  - const movieId = parseInt(match.slice(1).join(''), 10); - const movie = await findMovieById(movieId, groupUser.id); + const group = await checkAndGetGroup(msg); + const movie = await checkAndGetMovie(movieId, group.id);   - if (movie) { - await markMovieAsVetoed(movieId); - bot.sendMessage(chatId, `${texts.vetoed} "${movie.name}"`); - } else { - bot.sendMessage(chatId, texts.movie_not_found); - } + await markMovieAsVetoed(movieId); + bot.sendMessage(chatId, `${texts.vetoed} "${movie.name}"`); }, ], [ @@ -812,25 +628,20 @@

All files / controllers (bot) => async (msg) => { const chatId = msg.chat.id;   - const groupUser = await findGroupByCode(String(chatId)); - if (!groupUser) { - bot.sendMessage(chatId, texts.not_in_group); - return; - } -  - const movies = await listMovies(groupUser.id); + const group = await checkAndGetGroup(msg); + const movies = await checkAndGetMoviesList(group.id);   - if (!!movies && movies.length > 0) { - const movie = movies.filter((movie) => !movie.is_vetoed)[ - Math.floor(Math.random() * movies.length) - ]; - bot.sendMessage(chatId, getMovieDescription(movie), MSG_OPTIONS); - } else { - bot.sendMessage(chatId, texts.movie_list_empty); - } + const movie = movies.filter((movie) => !movie.is_vetoed)[ + Math.floor(Math.random() * movies.length) + ]; + bot.sendMessage(chatId, getMovieDescription(movie), MSG_OPTIONS); }, ], ]; +  +export const botHandlers = rawBotHandlers.map( + ([regexp, handler]) => [regexp, handleCheckErrors(handler)] as const, +);  

@@ -838,7 +649,7 @@

All files / controllers + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/controllers/helpers.ts.html b/coverage/lcov-report/controllers/helpers.ts.html index db46081..32484b1 100644 --- a/coverage/lcov-report/controllers/helpers.ts.html +++ b/coverage/lcov-report/controllers/helpers.ts.html @@ -106,7 +106,7 @@

All files / controllers + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/dbController/database.ts.html b/coverage/lcov-report/dbController/database.ts.html index 5d4b969..a753e3e 100644 --- a/coverage/lcov-report/dbController/database.ts.html +++ b/coverage/lcov-report/dbController/database.ts.html @@ -104,12 +104,18 @@

All files / dbController39 40 41 -422x +42 +43 +44 +45 +46 +47 +482x 2x 2x   2x -90x +137x       @@ -145,6 +151,12 @@

All files / dbController      +  +  +  +  +  +   
import sqlite3 from 'sqlite3';
 import { open } from 'sqlite';
 import { DATABASE_FILENAME } from '../consts';
@@ -164,13 +176,13 @@ 

All files / dbControllerAll files / dbController

@@ -193,7 +211,7 @@

All files / dbController Code coverage generated by istanbul - at 2024-08-02T06:19:45.240Z + at 2024-08-07T14:30:24.159Z

+ + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/errorHandler/handleCheckErrors.ts.html b/coverage/lcov-report/errorHandler/handleCheckErrors.ts.html new file mode 100644 index 0000000..5fedd23 --- /dev/null +++ b/coverage/lcov-report/errorHandler/handleCheckErrors.ts.html @@ -0,0 +1,136 @@ + + + + + + Code coverage report for errorHandler/handleCheckErrors.ts + + + + + + + + + +
+
+

All files / errorHandler handleCheckErrors.ts

+
+ +
+ 100% + Statements + 11/11 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 10/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18  +2x +  +2x +2x +12x +29x +29x +29x +  +15x +14x +  +1x +  +  +  + 
import { THandler } from '../controllersTypes';
+import { CheckError } from './CheckError';
+ 
+export const handleCheckErrors =
+  (handler: THandler): THandler =>
+  (bot) =>
+  async (msg, match) => {
+    try {
+      await handler(bot)(msg, match);
+    } catch (error) {
+      if (error instanceof CheckError) {
+        bot.sendMessage(msg.chat.id, error.message);
+      } else {
+        throw error;
+      }
+    }
+  };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/errorHandler/index.html b/coverage/lcov-report/errorHandler/index.html new file mode 100644 index 0000000..0502178 --- /dev/null +++ b/coverage/lcov-report/errorHandler/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for errorHandler + + + + + + + + + +
+
+

All files errorHandler

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 15/15 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
CheckError.ts +
+
100%3/3100%0/0100%1/1100%3/3
handleCheckErrors.ts +
+
100%11/11100%2/2100%4/4100%10/10
index.ts +
+
100%2/2100%0/0100%0/0100%2/2
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/errorHandler/index.ts.html b/coverage/lcov-report/errorHandler/index.ts.html new file mode 100644 index 0000000..638a1de --- /dev/null +++ b/coverage/lcov-report/errorHandler/index.ts.html @@ -0,0 +1,91 @@ + + + + + + Code coverage report for errorHandler/index.ts + + + + + + + + + +
+
+

All files / errorHandler index.ts

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +31x +1x + 
export * from './CheckError';
+export * from './handleCheckErrors';
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html index b0ffc87..f8a7637 100644 --- a/coverage/lcov-report/index.html +++ b/coverage/lcov-report/index.html @@ -23,30 +23,30 @@

All files

- 93.65% + 98.61% Statements - 177/189 + 214/217
- 77.04% + 81.39% Branches - 47/61 + 35/43
100% Functions - 46/46 + 67/67
- 93.18% + 98.47% Lines - 164/176 + 194/197
@@ -80,17 +80,17 @@

All files

controllers - -
+ +
- 91.42% - 128/140 - 78.18% - 43/55 - 100% - 33/33 - 90.55% - 115/127 + 97.87% + 138/141 + 82.75% + 24/29 + 100% + 43/43 + 97.56% + 120/123 @@ -108,6 +108,21 @@

All files

9/9 + + errorHandler + +
+ + 100% + 16/16 + 100% + 2/2 + 100% + 5/5 + 100% + 15/15 + + models @@ -129,13 +144,13 @@

All files

100% - 39/39 - 66.66% - 4/6 + 50/50 + 75% + 9/12 100% - 11/11 + 17/17 100% - 39/39 + 49/49 @@ -146,7 +161,7 @@

All files