From 339b0c6ad9647c24f8c9315c3c3b059806ed9f61 Mon Sep 17 00:00:00 2001 From: Alexander Laevens Date: Wed, 30 Nov 2022 02:52:56 -0700 Subject: [PATCH] add websock functionality --- .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 6168 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 4439 bytes .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 8269 bytes .../ic_launcher_foreground.png | Bin 0 -> 12497 bytes .../ic_launcher_foreground.png | Bin 0 -> 17007 bytes one_trip/assets/icons/adaptive.png | Bin 0 -> 35419 bytes one_trip/lib/api/consts.dart | 5 +- one_trip/lib/api/models/list.dart | 115 ++++++ one_trip/lib/api/models/listingredient.dart | 39 +- one_trip/lib/api/models/recipe.dart | 51 ++- one_trip/lib/api/models/simpleuser.dart | 21 +- one_trip/lib/api/searchresult.dart | 6 + one_trip/lib/pages/list_page/list_page.dart | 343 +++++++++++++++--- .../lib/pages/list_page/widgets/listrow.dart | 85 +++++ .../list_page/widgets/search_recipes.dart | 168 +++++++++ .../widgets/invite_homegroup_dialog.dart | 5 +- .../lib/pages/recipes_page/recipes_page.dart | 80 ++-- .../widgets/recipe_card_widget.dart | 69 ++-- one_trip/lib/screens/home_screen.dart | 3 +- one_trip/lib/theme.dart | 5 +- one_trip/lib/widgets/pagination_listview.dart | 29 +- one_trip/linux/my_application.cc | 4 +- one_trip_api/api/apps.py | 2 +- .../api/migrations/0005_list_updates.py | 18 + one_trip_api/api/models.py | 1 + one_trip_api/api/serializers.py | 10 +- one_trip_api/api/urls.py | 3 +- one_trip_api/api/views.py | 53 ++- one_trip_api/one_trip_api/asgi.py | 10 +- one_trip_api/one_trip_api/settings/base.py | 11 + one_trip_api/one_trip_api/wsgi.py | 2 +- one_trip_api/users/middleware.py | 2 +- one_trip_api/ws/__init__.py | 0 one_trip_api/ws/admin.py | 3 + one_trip_api/ws/apps.py | 6 + one_trip_api/ws/consumers.py | 51 +++ one_trip_api/ws/migrations/__init__.py | 0 one_trip_api/ws/models.py | 3 + one_trip_api/ws/routing.py | 7 + one_trip_api/ws/tests.py | 3 + one_trip_api/ws/views.py | 4 + 41 files changed, 1028 insertions(+), 189 deletions(-) create mode 100644 one_trip/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 one_trip/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 one_trip/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 one_trip/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 one_trip/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 one_trip/assets/icons/adaptive.png create mode 100644 one_trip/lib/api/models/list.dart create mode 100644 one_trip/lib/api/searchresult.dart create mode 100644 one_trip/lib/pages/list_page/widgets/listrow.dart create mode 100644 one_trip/lib/pages/list_page/widgets/search_recipes.dart create mode 100644 one_trip_api/api/migrations/0005_list_updates.py create mode 100644 one_trip_api/ws/__init__.py create mode 100644 one_trip_api/ws/admin.py create mode 100644 one_trip_api/ws/apps.py create mode 100644 one_trip_api/ws/consumers.py create mode 100644 one_trip_api/ws/migrations/__init__.py create mode 100644 one_trip_api/ws/models.py create mode 100644 one_trip_api/ws/routing.py create mode 100644 one_trip_api/ws/tests.py create mode 100644 one_trip_api/ws/views.py diff --git a/one_trip/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/one_trip/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ba5cbe63744ef215f6ab4701bef7a4eb22de6b GIT binary patch literal 6168 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 literal 0 HcmV?d00001 diff --git a/one_trip/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/one_trip/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..8bcdc7283a86e56618415f1cff8544078f2c67c0 GIT binary patch literal 4439 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 E57xs-TL1t6 literal 0 HcmV?d00001 diff --git a/one_trip/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/one_trip/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..d5af25baf719e78a4b1d2e46079766a7fc973f31 GIT binary patch literal 8269 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 literal 0 HcmV?d00001 diff --git a/one_trip/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/one_trip/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..9436920d6511c88d9b11ca8ac2ee1fe1c4e7303d GIT binary patch literal 17007 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 DK>ZHP literal 0 HcmV?d00001 diff --git a/one_trip/assets/icons/adaptive.png b/one_trip/assets/icons/adaptive.png new file mode 100644 index 0000000000000000000000000000000000000000..87272e2b5d7ce733c51582b2618162e9fcc07edc GIT binary patch literal 35419 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 ingredients; + int updates; + int homegroup; + + ShoppingList({ + required this.ingredients, + required this.updates, + required this.homegroup, + }); + + factory ShoppingList.fromJson(Map json) { + List ingredients = []; + for (dynamic ingredient in json["ingredients"]) { + ingredients.add(ListIngredient.fromJson(ingredient)); + } + + return ShoppingList( + ingredients: ingredients, + updates: json["updates"] as int, + homegroup: json["homegroup"] as int); + } + + static Future get(int id) async { + String requestURL = "$baseURL/api/lists/$id/"; + String token = TokenSingleton().getToken(); + http.Response response = await http.get( + Uri.parse(requestURL), + headers: {"Authorization": "Token $token"}, + ); + + if (response.statusCode == 200) { + return ShoppingList.fromJson(jsonDecode(response.body)); + } + + return null; + } + + Future patch({int? updates}) async { + Map body = {}; + if (updates != null) { + body["updates"] = "$updates"; + } + + String requestURL = "$baseURL/api/lists/$homegroup/"; + String token = TokenSingleton().getToken(); + http.Response response = await http.patch(Uri.parse(requestURL), + headers: {"Authorization": "Token $token"}, body: body); + + if (response.statusCode == 200) { + return ShoppingList.fromJson(jsonDecode(response.body)); + } + + return null; + } + + Future addRecipe(int recipeID) async { + Recipe? recipe = await Recipe.get(recipeID); + + if (recipe == null) { + return null; + } + + bool anySuccesses = false; + for (RecipeIngredient ingredient in recipe.ingredients) { + ListIngredient? newIngredient = + await ListIngredient.create(ingredient.name, homegroup); + + if (newIngredient != null) { + anySuccesses = true; + } + } + + if (anySuccesses) { + return patch(updates: updates + 1); + } + + return null; + } + + Future clear() async { + bool anySuccess = false; + for (ListIngredient ingredient in ingredients) { + bool success = await ingredient.delete(); + + if (success) { + anySuccess = true; + } + } + + if (anySuccess) { + return patch(updates: updates + 1); + } + + return null; + } + + @override + bool operator ==(Object other) => + other is ShoppingList && + other.homegroup == homegroup && + other.ingredients == ingredients; + + @override + int get hashCode => Object.hashAll(ingredients); +} diff --git a/one_trip/lib/api/models/listingredient.dart b/one_trip/lib/api/models/listingredient.dart index 03560c1..8ad23a3 100644 --- a/one_trip/lib/api/models/listingredient.dart +++ b/one_trip/lib/api/models/listingredient.dart @@ -4,21 +4,21 @@ import 'package:one_trip/api/auth.dart'; import 'package:one_trip/api/consts.dart'; import 'package:http/http.dart' as http; -class RecipeIngredient { +class ListIngredient { int id; String name; int list; bool inCart; - RecipeIngredient({ + ListIngredient({ required this.id, required this.name, required this.list, required this.inCart, }); - factory RecipeIngredient.fromJson(Map json) { - return RecipeIngredient( + factory ListIngredient.fromJson(Map json) { + return ListIngredient( id: json["id"] as int, name: json["name"] as String, list: json["list"] as int, @@ -26,7 +26,7 @@ class RecipeIngredient { ); } - static Future create(String name, int recipeID) async { + static Future create(String name, int list) async { const String requestURL = "$baseURL/api/listingredients/"; String token = TokenSingleton().getToken(); http.Response response = await http.post( @@ -34,26 +34,35 @@ class RecipeIngredient { headers: {"Authorization": "Token $token"}, body: { "name": name, - "recipe": "$recipeID", + "list": "$list", }, ); if (response.statusCode == 201) { - return RecipeIngredient.fromJson(jsonDecode(response.body)); + return ListIngredient.fromJson(jsonDecode(response.body)); } else { return null; } } - Future patch(String name) async { + Future patch({String? name, bool? inCart}) async { String requestURL = "$baseURL/api/listingredients/$id/"; String token = TokenSingleton().getToken(); + Map body = {}; + if (name != null) { + body["name"] = name; + } + + if (inCart != null) { + body["in_cart"] = "$inCart"; + } + http.Response response = await http.patch(Uri.parse(requestURL), - headers: {"Authorization": "Token $token"}, body: {"name": name}); + headers: {"Authorization": "Token $token"}, body: body); if (response.statusCode == 200) { - return RecipeIngredient.fromJson(jsonDecode(response.body)); + return ListIngredient.fromJson(jsonDecode(response.body)); } return null; @@ -71,4 +80,14 @@ class RecipeIngredient { return false; } + + @override + bool operator ==(Object other) => + other is ListIngredient && + other.id == id && + other.name == name && + other.inCart == inCart; + + @override + int get hashCode => Object.hash(id, name, inCart); } diff --git a/one_trip/lib/api/models/recipe.dart b/one_trip/lib/api/models/recipe.dart index 0619aae..288b159 100644 --- a/one_trip/lib/api/models/recipe.dart +++ b/one_trip/lib/api/models/recipe.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'package:one_trip/api/auth.dart'; import 'package:one_trip/api/consts.dart'; -import 'package:one_trip/api/models/homegroup.dart'; import 'package:one_trip/api/models/recipeingredient.dart'; import 'package:http/http.dart' as http; +import 'package:one_trip/api/searchresult.dart'; class Recipe { int id; @@ -46,26 +46,51 @@ class Recipe { return null; } - static Future> getList(int groupID) async { - Homegroup? group = await Homegroup.get(groupID); - if (group == null) { - return []; - } + static Future> getList() async { + const String requestURL = "$baseURL/api/recipes/"; + + String token = TokenSingleton().getToken(); + http.Response response = await http.get( + Uri.parse(requestURL), + headers: {"Authorization": "Token $token"}, + ); List recipes = []; - for (int recipeID in group.recipes) { - Recipe? recipe = await Recipe.get(recipeID); - if (recipe != null) { - // TODO: implement sorted insert - recipes.add(recipe); + if (response.statusCode == 200) { + var body = jsonDecode(response.body); + for (var recipe in body) { + recipes.add(Recipe.fromJson(recipe)); } } - recipes.sort(((a, b) => a.name.compareTo(b.name))); - return recipes; } + static Future> search(String query, int page) async { + String requestURL = "$baseURL/api/searchrecipes/?page=$page&search=$query"; + requestURL = requestURL.replaceAll(RegExp(r"\s+"), "+"); + + String token = TokenSingleton().getToken(); + final http.Response response = await http.get( + Uri.parse(requestURL), + headers: {"Authorization": "Token $token"}, + ); + + if (response.statusCode == 200) { + Map json = jsonDecode(response.body); + List recipes = []; + for (var recipeObject in json["results"]) { + Recipe r = Recipe.fromJson(recipeObject); + recipes.add(r); + } + + return SearchResult( + results: recipes, next: json["next"] as String?); + } + + return SearchResult(results: [], next: null); + } + static Future create(String name, int group) async { String requestURL = "$baseURL/api/recipes/"; String token = TokenSingleton().getToken(); diff --git a/one_trip/lib/api/models/simpleuser.dart b/one_trip/lib/api/models/simpleuser.dart index 68ed7b2..8db949a 100644 --- a/one_trip/lib/api/models/simpleuser.dart +++ b/one_trip/lib/api/models/simpleuser.dart @@ -3,13 +3,14 @@ import 'dart:convert'; import 'package:one_trip/api/auth.dart'; import 'package:one_trip/api/consts.dart'; import 'package:http/http.dart' as http; +import 'package:one_trip/api/searchresult.dart'; -class SearchResult { - List users; - String? next; +// class SearchResult { +// List users; +// String? next; - SearchResult({required this.users, required this.next}); -} +// SearchResult({required this.users, required this.next}); +// } class SimpleUser { int id; @@ -38,7 +39,8 @@ class SimpleUser { } static Future get({int? id}) async { - String requestURL = "$baseURL/auth/users/${id ?? 'me'}"; + String requestURL = + id == null ? "$baseURL/auth/users/me" : "$baseURL/auth/users/$id/"; String token = TokenSingleton().getToken(); final http.Response response = await http.get( @@ -54,7 +56,7 @@ class SimpleUser { } } - static Future search(String query, int page) async { + static Future> search(String query, int page) async { // String requestURL = ""; // if (url != null) { // requestURL = url; @@ -81,9 +83,10 @@ class SimpleUser { users.add(u); } - return SearchResult(users: users, next: json["next"] as String?); + return SearchResult( + results: users, next: json["next"] as String?); } - return SearchResult(users: [], next: null); + return SearchResult(results: [], next: null); } } diff --git a/one_trip/lib/api/searchresult.dart b/one_trip/lib/api/searchresult.dart new file mode 100644 index 0000000..34ec5eb --- /dev/null +++ b/one_trip/lib/api/searchresult.dart @@ -0,0 +1,6 @@ +class SearchResult { + List results; + String? next; + + SearchResult({required this.results, required this.next}); +} diff --git a/one_trip/lib/pages/list_page/list_page.dart b/one_trip/lib/pages/list_page/list_page.dart index dd8dc6c..c944800 100644 --- a/one_trip/lib/pages/list_page/list_page.dart +++ b/one_trip/lib/pages/list_page/list_page.dart @@ -1,52 +1,305 @@ -// import 'package:flutter/material.dart'; -// import 'package:one_trip/api/models/recipe.dart'; -// import 'package:one_trip/api/models/user.dart'; -// import 'package:one_trip/pages/recipes_page/widgets/recipe_card_widget.dart'; -// import 'package:one_trip/widgets/text_entry_dialog.dart'; +import 'dart:convert'; +import 'dart:io'; -// class RecipesPage extends StatefulWidget { -// const RecipesPage({super.key}); +import 'package:flutter/material.dart'; +import 'package:one_trip/api/auth.dart'; +import 'package:one_trip/api/consts.dart'; +import 'package:one_trip/api/models/list.dart'; +import 'package:one_trip/api/models/listingredient.dart'; +import 'package:one_trip/api/models/user.dart'; +import 'package:one_trip/pages/list_page/widgets/listrow.dart'; +import 'package:one_trip/pages/list_page/widgets/search_recipes.dart'; +import 'package:one_trip/widgets/text_entry_dialog.dart'; -// @override -// State createState() => _RecipesPageState(); -// } +class ListPage extends StatefulWidget { + const ListPage({super.key}); -// class _RecipesPageState extends State { -// late Future> _recipes; -// late User _userInfo; + @override + State createState() => _ListPageState(); +} -// Future> _fetchList() async { -// User? userInfo = await User.getMe(); -// if (userInfo == null || userInfo.homegroup == null) { -// return []; -// } -// _userInfo = userInfo; +class _ListPageState extends State { + ShoppingList? _list; + late Future _isLoaded; + User? _userInfo; + WebSocket? _ws; -// List recipes = await Recipe.getList(_userInfo.homegroup!); -// return recipes; -// } + Future _fetchList() async { + User? userInfo = await User.getMe(); + _userInfo = userInfo; -// @override -// void initState() { -// super.initState(); -// _recipes = _fetchList(); -// } + if (userInfo == null || userInfo.homegroup == null) { + return false; + } -// @override -// Widget build(BuildContext context) { -// return FutureBuilder( -// future: _recipes, -// builder: (context, snapshot) { -// if (snapshot.hasError) { -// return Text(snapshot.error.toString()); -// } else if (snapshot.hasData && -// snapshot.connectionState == ConnectionState.done) { -// return RecipeList( -// recipes: snapshot.data!, homegroup: _userInfo.homegroup!); -// } else { -// return const Center(child: CircularProgressIndicator()); -// } -// }, -// ); -// } -// } \ No newline at end of file + _list = await ShoppingList.get(userInfo.homegroup!); + return true; + } + + void _connectSocket() async { + String token = TokenSingleton().getToken(); + _ws = await WebSocket.connect("$baseWsURL/ws/", + headers: {"Authorization": "Token $token"}); + + if (_ws == null) { + return; + } + + _ws!.listen((event) async { + Map json = jsonDecode(event); + + if (json.keys.contains("type") && json["type"] == "recommend_update") { + if (json["hash"] != _list.hashCode) { + ShoppingList? newList = await ShoppingList.get(_list!.homegroup); + + if (newList != null) { + setState(() { + _list = newList; + }); + } + } + } + }); + } + + void _sendUpdate() async { + if (_ws == null) { + return; + } + _ws!.add(jsonEncode({"type": "broadcast_update", "hash": _list.hashCode})); + } + + @override + void dispose() { + if (_ws != null) { + _ws!.close(); + } + super.dispose(); + } + + @override + void initState() { + super.initState(); + _isLoaded = _fetchList(); + _connectSocket(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _isLoaded, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text(snapshot.error.toString()); + } else if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + if (_userInfo == null) { + return const Center( + child: Text("Could not load user, try logging in again..."), + ); + } else if (_userInfo!.homegroup == null) { + return const Center( + child: Text("You must be in a homegroup to use this feature"), + ); + } else if (_list == null) { + return const Center( + child: Text("Issue loading list"), + ); + } else { + return ListArea( + list: _list!, + onAddOne: () async { + String? itemName = + await textEntryDialog(context, "Item Name", "Item"); + + if (itemName == null || itemName == "") { + return; + } + + ListIngredient? newIngredient = + await ListIngredient.create(itemName, _list!.homegroup); + if (newIngredient == null) { + return; + } + + ShoppingList? newList = + await ShoppingList.get(_list!.homegroup); + + if (newList != null) { + setState(() { + _list = newList; + }); + } + + _sendUpdate(); + }, + onAddMany: () async { + List? selectedIDs = await searchRecipesDialog(context); + + if (selectedIDs == null) { + return; + } + + ShoppingList tempList = _list!; + for (int id in selectedIDs) { + ShoppingList? newList = await tempList.addRecipe(id); + + if (newList != null) { + tempList = newList; + } + } + + setState(() { + _list = tempList; + }); + + _sendUpdate(); + }, + onDelete: (ingredient) async { + bool success = await ingredient.delete(); + if (success) { + // ShoppingList? newList = + // await _list!.patch(updates: _list!.updates + 1); + + ShoppingList? newList = + await ShoppingList.get(_list!.homegroup); + + setState(() { + _list = newList; + }); + + _sendUpdate(); + } + + return success; + }, + onUpdate: (ingredient, {inCart, name}) async { + ListIngredient? updated = + await ingredient.patch(name: name, inCart: inCart); + if (updated != null) { + ShoppingList? newList = + await ShoppingList.get(_list!.homegroup); + + setState(() { + _list = newList; + }); + + _sendUpdate(); + } + }, + onClear: () async { + ShoppingList? newList = await _list!.clear(); + + if (newList != null) { + setState(() { + _list = newList; + }); + } + + _sendUpdate(); + }, + ); + } + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } +} + +class ListArea extends StatelessWidget { + final ShoppingList list; + final Function() onAddOne; + final Function() onAddMany; + final Function() onClear; + final Future Function(ListIngredient ingredient) onDelete; + final Function(ListIngredient ingredient, {String? name, bool? inCart}) + onUpdate; + const ListArea({ + super.key, + required this.list, + required this.onAddOne, + required this.onAddMany, + required this.onDelete, + required this.onUpdate, + required this.onClear, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ListView.separated( + key: UniqueKey(), + itemCount: list.ingredients.length, + padding: const EdgeInsets.all(8), + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + return ListRow( + ingredient: list.ingredients[index], + onToggle: (value) { + onUpdate(list.ingredients[index], inCart: value); + }, + apiRemove: (ingredient) async => await onDelete(ingredient), + index: index, + ); + }, + ), + ), + LayoutBuilder( + builder: (context, constraints) { + ButtonStyle buttonStyle = ButtonStyle( + fixedSize: MaterialStatePropertyAll( + Size(constraints.maxWidth / 3, 45), + ), + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.zero, top: Radius.circular(10)), + ), + ), + padding: const MaterialStatePropertyAll(EdgeInsets.zero)); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + style: buttonStyle, + onPressed: () => onAddMany(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [Icon(Icons.post_add), Text("Add Recipes")], + ), + ), + ElevatedButton( + style: buttonStyle, + onPressed: () => onAddOne(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [Icon(Icons.add), Text("Add Item")], + ), + ), + ElevatedButton( + style: buttonStyle.copyWith( + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.error), + foregroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.onError), + ), + onPressed: () => onClear(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [Icon(Icons.delete), Text("Clear List")], + ), + ) + ], + ); + }, + ) + ], + ); + } +} diff --git a/one_trip/lib/pages/list_page/widgets/listrow.dart b/one_trip/lib/pages/list_page/widgets/listrow.dart new file mode 100644 index 0000000..f00c536 --- /dev/null +++ b/one_trip/lib/pages/list_page/widgets/listrow.dart @@ -0,0 +1,85 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:one_trip/api/models/listingredient.dart'; + +class ListRow extends StatefulWidget { + final ListIngredient ingredient; + final Future Function(ListIngredient ingredient) apiRemove; + final Function(bool value) onToggle; + final int index; + const ListRow({ + super.key, + required this.ingredient, + required this.onToggle, + required this.index, + required this.apiRemove, + }); + + @override + State createState() => _ListRowState(); +} + +class _ListRowState extends State { + double dismissAmount = 0.0; + bool willDismiss = false; + final UniqueKey key = UniqueKey(); + + @override + void didUpdateWidget(covariant ListRow oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => widget.onToggle(!widget.ingredient.inCart), + child: Dismissible( + key: key, + direction: DismissDirection.endToStart, + onUpdate: (details) => setState(() { + dismissAmount = details.progress; + willDismiss = details.reached; + }), + confirmDismiss: (direction) async => + await widget.apiRemove(widget.ingredient), + background: Container( + color: Color.lerp( + Colors.transparent, Colors.red, min(dismissAmount * 2.5, 1)), + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 45, + child: Icon( + Icons.delete, + size: min(27.5 * dismissAmount + 20, 35), + color: Colors.white, + ), + ), + ), + ), + child: Row( + children: [ + Checkbox( + value: widget.ingredient.inCart, + onChanged: (value) => widget.onToggle(value!), + ), + Expanded( + child: Text( + // _ingredient.name, + widget.ingredient.name, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + decoration: widget.ingredient.inCart + ? TextDecoration.lineThrough + : null, + color: widget.ingredient.inCart ? Colors.green : null, + ), + ), + ), + // IconButton(onPressed: () {}, icon: const Icon(Icons.edit)), + ], + ), + ), + ); + } +} diff --git a/one_trip/lib/pages/list_page/widgets/search_recipes.dart b/one_trip/lib/pages/list_page/widgets/search_recipes.dart new file mode 100644 index 0000000..4d8bd08 --- /dev/null +++ b/one_trip/lib/pages/list_page/widgets/search_recipes.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:one_trip/api/models/recipe.dart'; +import 'package:one_trip/api/searchresult.dart'; +import 'package:one_trip/theme.dart'; +import 'package:one_trip/widgets/pagination_listview.dart'; + +class SearchRecipesDialog extends StatefulWidget { + const SearchRecipesDialog({super.key}); + + @override + State createState() => _SearchRecipesDialogState(); +} + +class _SearchRecipesDialogState extends State { + final TextEditingController _searchController = TextEditingController(); + ListViewState _listState = ListViewState.inactive; + List selectedIDs = []; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Search your Recipes", + style: Theme.of(context).textTheme.titleMedium, + ), + const Divider(), + TextFormField( + controller: _searchController, + textInputAction: TextInputAction.search, + onFieldSubmitted: (value) { + setState(() { + _listState = ListViewState.changed; + }); + }, + onChanged: (value) { + setState(() { + _listState = ListViewState.inactive; + }); + }, + decoration: InputDecoration( + label: const Text("Search"), + isDense: true, + suffix: IconButton( + onPressed: () { + setState(() { + _listState = ListViewState.changed; + }); + + // https://flutterigniter.com/dismiss-keyboard-form-lose-focus/ + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + }, + icon: const Icon(Icons.search), + ), + ), + ), + const SizedBox(height: 8), + LayoutBuilder( + builder: (builder, constraints) { + return ConstrainedBox( + constraints: BoxConstraints.expand( + width: constraints.maxWidth - 8, + height: 160, + ), + child: PaginationListView( + state: _listState, + shrinkWrap: true, + itemBuilder: (context, data) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + setState(() { + _listState = ListViewState.inUse; + if (selectedIDs.contains(data.id)) { + selectedIDs.remove(data.id); + } else { + selectedIDs.add(data.id); + } + }); + }, + child: Container( + padding: const EdgeInsets.all(4), + color: selectedIDs.contains(data.id) + ? Theme.of(context).colorScheme.secondary + : null, + child: Text(data.name), + ), + ); + }, + seperatorBuilder: (context, data) { + return const Divider(); + }, + dataProvider: (int page) async { + // SearchResult result = + // await SimpleUser.search(_searchController.text, page); + // List users = List.from(result.results); + + // if (result.next == null) { + // users.add(null); + // } + + // return users; + + SearchResult result = + await Recipe.search(_searchController.text, page); + List recipes = + List.from(result.results); + + if (result.next == null) { + recipes.add(null); + } + + return recipes; + }, + ), + ); + }, + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, selectedIDs), + child: const Text("Done")), + ], + ), + ) + ], + ), + ), + ); + } +} + +Future?> searchRecipesDialog(BuildContext context) async { + List? selectedIDs = await showDialog( + context: context, + builder: (context) { + return Dialog( + child: ScrollConfiguration( + behavior: MyBehavior(), child: const SearchRecipesDialog()), + ); + }, + ); + + return selectedIDs; +} diff --git a/one_trip/lib/pages/profile_page/widgets/invite_homegroup_dialog.dart b/one_trip/lib/pages/profile_page/widgets/invite_homegroup_dialog.dart index 50be508..76cd102 100644 --- a/one_trip/lib/pages/profile_page/widgets/invite_homegroup_dialog.dart +++ b/one_trip/lib/pages/profile_page/widgets/invite_homegroup_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:one_trip/api/models/simpleuser.dart'; +import 'package:one_trip/api/searchresult.dart'; import 'package:one_trip/pages/profile_page/widgets/user_chip.dart'; import 'package:one_trip/theme.dart'; import 'package:one_trip/widgets/pagination_listview.dart'; @@ -118,9 +119,9 @@ class _InviteHomegroupDialogState extends State { return const Divider(); }, dataProvider: (int page) async { - SearchResult result = + SearchResult result = await SimpleUser.search(_searchController.text, page); - List users = List.from(result.users); + List users = List.from(result.results); if (result.next == null) { users.add(null); diff --git a/one_trip/lib/pages/recipes_page/recipes_page.dart b/one_trip/lib/pages/recipes_page/recipes_page.dart index f617a9d..9193650 100644 --- a/one_trip/lib/pages/recipes_page/recipes_page.dart +++ b/one_trip/lib/pages/recipes_page/recipes_page.dart @@ -23,7 +23,7 @@ class _RecipesPageState extends State { return []; } - List recipes = await Recipe.getList(userInfo.homegroup!); + List recipes = await Recipe.getList(); return recipes; } @@ -106,47 +106,44 @@ class _RecipeListState extends State { Widget build(BuildContext context) { return Stack( children: [ - ListView.separated( + Scrollbar( controller: _scrollController, - padding: const EdgeInsets.fromLTRB( - 8, 8, 8, kFloatingActionButtonMargin + 48), - itemCount: _recipes.length, - separatorBuilder: (context, index) => const SizedBox(height: 12), - itemBuilder: (context, index) => RecipeCard( - recipe: _recipes[index], - isExpanded: _expandedCard == index, - onTap: () { - setState(() { - if (_expandedCard == index) { - _expandedCard = null; - } else { - _expandedCard = index; - } - }); - }, - onDismiss: () async { - if (_expandedCard != null && _expandedCard! > index) { - _expandedCard = _expandedCard! - 1; - } - - bool success = await _recipes[index].delete(); - - if (!success) { - showError("Permanent deletion of recipe failed."); - } - - setState(() { - _recipes.removeAt(index); - }); - }, - onChanged: () async { - Recipe? newRecipe = await Recipe.get(_recipes[index].id); - if (newRecipe != null) { + child: ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.fromLTRB( + 8, 8, 8, kFloatingActionButtonMargin + 48), + itemCount: _recipes.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) => RecipeCard( + recipe: _recipes[index], + isExpanded: _expandedCard == index, + onTap: () { setState(() { - _recipes[index] = newRecipe; + if (_expandedCard == index) { + _expandedCard = null; + } else { + _expandedCard = index; + } }); - } - }, + }, + onDismiss: () { + if (_expandedCard != null && _expandedCard! > index) { + _expandedCard = _expandedCard! - 1; + } + + setState(() { + _recipes.removeAt(index); + }); + }, + onChanged: () async { + Recipe? newRecipe = await Recipe.get(_recipes[index].id); + if (newRecipe != null) { + setState(() { + _recipes[index] = newRecipe; + }); + } + }, + ), ), ), Align( @@ -154,6 +151,7 @@ class _RecipeListState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: FloatingActionButton.extended( + heroTag: "add-ingredient", onPressed: () async { String? name = await textEntryDialog(context, "Recipe Name", "Recipe"); @@ -180,11 +178,11 @@ class _RecipeListState extends State { } }, label: Row( - children: const [Icon(Icons.note_add), Text("New Recipe")], + children: const [Icon(Icons.post_add), Text("Recipe")], ), ), ), - ) + ), ], ); } diff --git a/one_trip/lib/pages/recipes_page/widgets/recipe_card_widget.dart b/one_trip/lib/pages/recipes_page/widgets/recipe_card_widget.dart index ed0ec63..30db849 100644 --- a/one_trip/lib/pages/recipes_page/widgets/recipe_card_widget.dart +++ b/one_trip/lib/pages/recipes_page/widgets/recipe_card_widget.dart @@ -85,6 +85,7 @@ class _RecipeCardState extends State with TickerProviderStateMixin { : DismissDirection.endToStart, key: Key("${widget.recipe.id}"), onDismissed: (direction) => widget.onDismiss(), + confirmDismiss: (direction) => widget.recipe.delete(), onUpdate: (details) { setState(() { dismissAmount = details.progress; @@ -92,16 +93,8 @@ class _RecipeCardState extends State with TickerProviderStateMixin { }); }, background: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Color.fromARGB(255, 255, 0, 0), - Color.fromARGB(255, 255, 170, 170), - ], - ), - ), + color: Color.lerp(Colors.transparent, Colors.red, + min(dismissAmount * 2.5, 1)), child: Align( alignment: Alignment.centerRight, child: SizedBox( @@ -109,7 +102,7 @@ class _RecipeCardState extends State with TickerProviderStateMixin { child: Icon( Icons.delete, size: min(27.5 * dismissAmount + 20, 35), - color: willDismiss ? Colors.red : Colors.white, + color: Colors.white, ), ), ), @@ -193,9 +186,11 @@ class _IngredientSectionState extends State { @override Widget build(BuildContext context) { - return Container( + return Material( + elevation: 10, color: Theme.of(context).colorScheme.surface, child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), padding: widget.ingredients.isEmpty ? EdgeInsets.zero : const EdgeInsets.all(8), @@ -208,16 +203,8 @@ class _IngredientSectionState extends State { key: Key("${widget.ingredients[index].id}"), direction: DismissDirection.endToStart, background: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Color.fromARGB(255, 255, 0, 0), - Color.fromARGB(255, 255, 170, 170), - ], - ), - ), + color: Color.lerp(Colors.transparent, Colors.red, + min(dismissAmount * 2.5, 1)), child: Align( alignment: Alignment.centerRight, child: SizedBox( @@ -225,7 +212,7 @@ class _IngredientSectionState extends State { child: Icon( Icons.delete, size: min(27.5 * dismissAmount + 20, 35), - color: willDismiss ? Colors.red : Colors.white, + color: Colors.white, ), ), ), @@ -236,11 +223,10 @@ class _IngredientSectionState extends State { willDismiss = details.reached; }); }, + confirmDismiss: (direction) async => + await widget.ingredients[index].delete(), onDismissed: (direction) async { - bool success = await widget.ingredients[index].delete(); - if (success) { - widget.onChanged(); - } + widget.onChanged(); }, child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -251,22 +237,23 @@ class _IngredientSectionState extends State { style: Theme.of(context).textTheme.titleMedium, )), IconButton( - onPressed: () async { - String? name = await textEntryDialog( - context, "Change Ingredient Name", "Ingredient", - defaultValue: widget.ingredients[index].name); + onPressed: () async { + String? name = await textEntryDialog( + context, "Change Ingredient Name", "Ingredient", + defaultValue: widget.ingredients[index].name); - if (name == null || name == "") { - return; - } + if (name == null || name == "") { + return; + } - RecipeIngredient? changed = - await widget.ingredients[index].patch(name); - if (changed != null) { - widget.onChanged(); - } - }, - icon: const Icon(Icons.edit)), + RecipeIngredient? changed = + await widget.ingredients[index].patch(name); + if (changed != null) { + widget.onChanged(); + } + }, + icon: const Icon(Icons.edit), + ), ], ), ), diff --git a/one_trip/lib/screens/home_screen.dart b/one_trip/lib/screens/home_screen.dart index 5c4bdd7..baca9de 100644 --- a/one_trip/lib/screens/home_screen.dart +++ b/one_trip/lib/screens/home_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:one_trip/pages/list_page/list_page.dart'; import 'package:one_trip/pages/profile_page/profile_page.dart'; import 'package:one_trip/pages/recipes_page/recipes_page.dart'; import 'package:one_trip/pages/themetest.dart'; @@ -23,7 +24,7 @@ class _HomeScreenState extends State { @override void initState() { _pages = [ - Container(), + const ListPage(), const RecipesPage(), const ProfilePage(), const ColorPage() diff --git a/one_trip/lib/theme.dart b/one_trip/lib/theme.dart index 0c9c6f2..1882547 100644 --- a/one_trip/lib/theme.dart +++ b/one_trip/lib/theme.dart @@ -12,10 +12,6 @@ final _lightScheme = final darkTheme = ThemeData( colorScheme: _darkScheme, toggleableActiveColor: _darkScheme.primary, - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: _darkScheme.primary, - splashColor: _darkScheme.secondary, - ), cardColor: _darkScheme.secondaryContainer); final lightTheme = ThemeData( @@ -31,6 +27,7 @@ final bottomButtonStyle = ButtonStyle( BorderRadius.vertical(top: Radius.zero, bottom: Radius.circular(10)), ), ), + elevation: const MaterialStatePropertyAll(10), ); // https://stackoverflow.com/a/51119796/13538080 diff --git a/one_trip/lib/widgets/pagination_listview.dart b/one_trip/lib/widgets/pagination_listview.dart index 74f3d39..0019979 100644 --- a/one_trip/lib/widgets/pagination_listview.dart +++ b/one_trip/lib/widgets/pagination_listview.dart @@ -6,16 +6,21 @@ class PaginationListView extends StatefulWidget { final Widget Function(BuildContext context, dynamic data) itemBuilder; final Widget Function(BuildContext context, dynamic data) seperatorBuilder; final bool? shrinkWrap; + final bool? prefetchOne; final ListViewState state; + final EdgeInsetsGeometry? padding; final Future> Function(int page) dataProvider; - const PaginationListView( - {super.key, - required this.itemBuilder, - required this.dataProvider, - required this.state, - required this.seperatorBuilder, - this.shrinkWrap}); + const PaginationListView({ + super.key, + required this.itemBuilder, + required this.dataProvider, + required this.state, + required this.seperatorBuilder, + this.prefetchOne, + this.shrinkWrap, + this.padding, + }); @override State createState() => _PaginationListViewState(); @@ -66,6 +71,15 @@ class _PaginationListViewState extends State { super.initState(); _scrollController = ScrollController(); _state = widget.state; + + if (widget.prefetchOne ?? false) { + _state = ListViewState.inUse; + _data = []; + _dataLeft = true; + _isLoading = false; + _pagesLoaded = 0; + consumeData(); + } } @override @@ -98,6 +112,7 @@ class _PaginationListViewState extends State { controller: _scrollController, itemCount: _data.length, shrinkWrap: widget.shrinkWrap ?? false, + padding: widget.padding, itemBuilder: (context, index) => widget.itemBuilder(context, _data[index]), separatorBuilder: (context, index) => diff --git a/one_trip/linux/my_application.cc b/one_trip/linux/my_application.cc index 8bd544d..c9e9c70 100644 --- a/one_trip/linux/my_application.cc +++ b/one_trip/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "one_trip"); + gtk_header_bar_set_title(header_bar, "One Trip"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "one_trip"); + gtk_window_set_title(window, "One Trip"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/one_trip_api/api/apps.py b/one_trip_api/api/apps.py index 66656fd..f4187b4 100644 --- a/one_trip_api/api/apps.py +++ b/one_trip_api/api/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig - +from django.db.models.signals import post_save class ApiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' diff --git a/one_trip_api/api/migrations/0005_list_updates.py b/one_trip_api/api/migrations/0005_list_updates.py new file mode 100644 index 0000000..a02c66b --- /dev/null +++ b/one_trip_api/api/migrations/0005_list_updates.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-11-29 16:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_remove_recipe_list_delete_ingredient'), + ] + + operations = [ + migrations.AddField( + model_name='list', + name='updates', + field=models.BigIntegerField(default=0), + ), + ] diff --git a/one_trip_api/api/models.py b/one_trip_api/api/models.py index 53dfe55..412d466 100644 --- a/one_trip_api/api/models.py +++ b/one_trip_api/api/models.py @@ -26,6 +26,7 @@ class Homegroup(models.Model): class List(models.Model): # Foreign Key ListIngredient -> List [as ingredients] homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True) + updates = models.BigIntegerField(default=0); class Recipe(models.Model): diff --git a/one_trip_api/api/serializers.py b/one_trip_api/api/serializers.py index 63a2c7f..811dfd3 100644 --- a/one_trip_api/api/serializers.py +++ b/one_trip_api/api/serializers.py @@ -1,6 +1,10 @@ from rest_framework import serializers from api.models import * from users.serializers import UserSerializer +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + +channel_layer = get_channel_layer() class RecipeIngredientSerializer(serializers.ModelSerializer): class Meta: @@ -29,9 +33,13 @@ class ListSerializer(serializers.ModelSerializer): class Meta: model = List - fields = ["homegroup", "ingredients"] + fields = ["homegroup", "updates", "ingredients"] read_only_fields = ["homegroup"] + def update(self, instance, validated_data): + # async_to_sync(channel_layer.group_send)(f"group_{instance.homegroup.id}", {"type": "model_update"}) + return super().update(instance, validated_data) + def get_ingredients(self, instance): ingredients = instance.ingredients.all().order_by("name") return ListIngredientSerializer(ingredients, many=True).data diff --git a/one_trip_api/api/urls.py b/one_trip_api/api/urls.py index 4ed61d2..4694d17 100644 --- a/one_trip_api/api/urls.py +++ b/one_trip_api/api/urls.py @@ -3,7 +3,8 @@ from rest_framework import routers from api import views router = routers.DefaultRouter() -router.register(r'recipes', views.RecipeView) +router.register(r'recipes', views.RecipeAllView) +router.register(r'searchrecipes', views.RecipeSearchView) router.register(r'lists', views.ListView) router.register(r'recipeingredients', views.RecipeIngredientView) router.register(r'listingredients', views.ListIngredientView) diff --git a/one_trip_api/api/views.py b/one_trip_api/api/views.py index fd8f661..368b30e 100644 --- a/one_trip_api/api/views.py +++ b/one_trip_api/api/views.py @@ -1,28 +1,69 @@ -from rest_framework import viewsets, mixins, views, status, permissions +from rest_framework import viewsets, mixins, permissions, request, pagination, filters from rest_framework.response import Response +from rest_framework.request import Request from api.serializers import * from api.models import * -class RecipeView(viewsets.ModelViewSet): +class HasHomegroup(permissions.BasePermission): + def has_permission(self, request: Request, view): + if not request.user.homegroup: + return False + + return super().has_permission(request, view) + +class Pagination(pagination.PageNumberPagination): + page_size = 4 + +class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): + pass + +class RecipeSearchView(viewsets.ModelViewSet): serializer_class = RecipeSerializer + permission_classes = [permissions.IsAuthenticated, HasHomegroup] queryset = Recipe.objects.all() + filter_backends = [filters.SearchFilter] + search_fields = ["name"] + pagination_class = Pagination + + def list(self, request: Request, *args, **kwargs): + queryset = self.filter_queryset(Recipe.objects.filter(homegroup=request.user.homegroup).order_by("name")); + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.serializer_class(queryset, many=True) + return Response(serializer.data) + +class RecipeAllView(viewsets.ModelViewSet): + serializer_class = RecipeSerializer + permission_classes = [permissions.IsAuthenticated, HasHomegroup] + queryset = Recipe.objects.all() + filter_backends = [filters.SearchFilter] + search_fields = ["name"] + + def list(self, request: Request, *args, **kwargs): + queryset = self.filter_queryset(Recipe.objects.filter(homegroup=request.user.homegroup).order_by("name")); + serializer = self.serializer_class(queryset, many=True) + return Response(serializer.data) class HomegroupView(viewsets.ModelViewSet): serializer_class = HomegroupSerializer queryset = Homegroup.objects.all() -class HomegroupInviteView(viewsets.ModelViewSet): +class HomegroupInviteView(NoListModelViewset): serializer_class = InviteSerializer queryset = HomegroupInvite.objects.all() -class RecipeIngredientView(viewsets.ModelViewSet): +class RecipeIngredientView(NoListModelViewset): serializer_class = RecipeIngredientSerializer queryset = RecipeIngredient.objects.all() -class ListIngredientView(viewsets.ModelViewSet): +class ListIngredientView(NoListModelViewset): serializer_class = ListIngredientSerializer queryset = ListIngredient.objects.all() -class ListView(mixins.RetrieveModelMixin, viewsets.GenericViewSet): +class ListView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer_class = ListSerializer queryset = List.objects.all() \ No newline at end of file diff --git a/one_trip_api/one_trip_api/asgi.py b/one_trip_api/one_trip_api/asgi.py index 73ae753..6218732 100644 --- a/one_trip_api/one_trip_api/asgi.py +++ b/one_trip_api/one_trip_api/asgi.py @@ -10,6 +10,9 @@ https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ import os from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +import ws.routing settings = 'one_trip_api.settings.dev' if os.getenv("DJANGO_RELEASE", False): @@ -17,5 +20,10 @@ if os.getenv("DJANGO_RELEASE", False): os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings) +print("ASGI Started") +django_asgi_app = get_asgi_application() -application = get_asgi_application() +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack(URLRouter(ws.routing.websocket_urlpatterns)) +}) diff --git a/one_trip_api/one_trip_api/settings/base.py b/one_trip_api/one_trip_api/settings/base.py index 63076ae..1dd979c 100644 --- a/one_trip_api/one_trip_api/settings/base.py +++ b/one_trip_api/one_trip_api/settings/base.py @@ -33,6 +33,8 @@ REST_FRAMEWORK = { INSTALLED_APPS = [ 'api', 'users', + 'ws', + 'daphne', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -76,6 +78,15 @@ TEMPLATES = [ ] WSGI_APPLICATION = 'one_trip_api.wsgi.application' +ASGI_APPLICATION = 'one_trip_api.asgi.application' +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} DATABASES = { 'default': { diff --git a/one_trip_api/one_trip_api/wsgi.py b/one_trip_api/one_trip_api/wsgi.py index 9f12603..1d49dc7 100644 --- a/one_trip_api/one_trip_api/wsgi.py +++ b/one_trip_api/one_trip_api/wsgi.py @@ -17,5 +17,5 @@ if os.getenv("DJANGO_RELEASE", False): os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings) - +print("WSGI Started") application = get_wsgi_application() diff --git a/one_trip_api/users/middleware.py b/one_trip_api/users/middleware.py index 5f6fc56..86025b0 100644 --- a/one_trip_api/users/middleware.py +++ b/one_trip_api/users/middleware.py @@ -8,7 +8,7 @@ class ExemptCSRFMiddleware: def __call__(self, request): - if request.path_info == "/auth/token": + if request.path_info in ["/auth/token", "/auth/users/"]: setattr(request, '_dont_enforce_csrf_checks', True) response = self.get_response(request) diff --git a/one_trip_api/ws/__init__.py b/one_trip_api/ws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/one_trip_api/ws/admin.py b/one_trip_api/ws/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/one_trip_api/ws/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/one_trip_api/ws/apps.py b/one_trip_api/ws/apps.py new file mode 100644 index 0000000..2c87a48 --- /dev/null +++ b/one_trip_api/ws/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'ws' diff --git a/one_trip_api/ws/consumers.py b/one_trip_api/ws/consumers.py new file mode 100644 index 0000000..fdf3046 --- /dev/null +++ b/one_trip_api/ws/consumers.py @@ -0,0 +1,51 @@ +from channels.db import database_sync_to_async +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from rest_framework.authtoken.models import Token +from api.models import Homegroup +from users.models import User + +class ChatConsumer(AsyncJsonWebsocketConsumer): + async def connect(self): + token_homegroup = await self.get_homegroup_by_token(self.scope["headers"]) + if token_homegroup is None: + await self.disconnect(1) + else: + self.room_name = token_homegroup.id + self.room_group_name = f"group_{self.room_name}" + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + await self.accept() + + + async def receive_json(self, content, **kwargs): + await self.channel_layer.group_send( + self.room_group_name, + content + ) + + async def disconnect(self, close_code): + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + async def broadcast_update(self, event): + print(event) + await self.send_json(content={"type": "recommend_update", "hash": event["hash"]}) + + @database_sync_to_async + def get_homegroup_by_token(self, headers): + headers = self.scope["headers"] + for pair in headers: + if pair[0].decode("UTF-8") == "authorization": + tokenType, tokenString = pair[1].decode("UTF-8").split() + + queryset = Token.objects.filter(key=tokenString) + if queryset.exists(): + return Token.objects.get(key=tokenString).user.homegroup + else: + return None + + @database_sync_to_async + def get_homegroup_by_id(self, group_id): + queryset = Homegroup.objects.filter(id=group_id) + if queryset.exists(): + return queryset.get() + else: + return None diff --git a/one_trip_api/ws/migrations/__init__.py b/one_trip_api/ws/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/one_trip_api/ws/models.py b/one_trip_api/ws/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/one_trip_api/ws/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/one_trip_api/ws/routing.py b/one_trip_api/ws/routing.py new file mode 100644 index 0000000..5c678c9 --- /dev/null +++ b/one_trip_api/ws/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path, path + +from ws import consumers + +websocket_urlpatterns = [ + path('ws/', consumers.ChatConsumer.as_asgi(), name='room') +] \ No newline at end of file diff --git a/one_trip_api/ws/tests.py b/one_trip_api/ws/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/one_trip_api/ws/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/one_trip_api/ws/views.py b/one_trip_api/ws/views.py new file mode 100644 index 0000000..cf3d353 --- /dev/null +++ b/one_trip_api/ws/views.py @@ -0,0 +1,4 @@ +from django.shortcuts import render +from rest_framework.views import APIView + +# Create your views here. \ No newline at end of file