+Add quantity field to ingredients

+Clear list now requires confirmation
+Confirm / Cancel buttons are now coloured
This commit is contained in:
Alexander Laevens
2022-12-07 02:13:34 -07:00
parent 31c4505e49
commit 4d0388b262
20 changed files with 396 additions and 83 deletions

View File

@@ -9,12 +9,10 @@ import 'package:one_trip/api/models/recipeingredient.dart';
class ShoppingList { class ShoppingList {
List<ListIngredient> ingredients; List<ListIngredient> ingredients;
int updates;
int homegroup; int homegroup;
ShoppingList({ ShoppingList({
required this.ingredients, required this.ingredients,
required this.updates,
required this.homegroup, required this.homegroup,
}); });
@@ -25,9 +23,7 @@ class ShoppingList {
} }
return ShoppingList( return ShoppingList(
ingredients: ingredients, ingredients: ingredients, homegroup: json["homegroup"] as int);
updates: json["updates"] as int,
homegroup: json["homegroup"] as int);
} }
static Future<ShoppingList?> get(int id) async { static Future<ShoppingList?> get(int id) async {
@@ -72,8 +68,8 @@ class ShoppingList {
bool anySuccesses = false; bool anySuccesses = false;
for (RecipeIngredient ingredient in recipe.ingredients) { for (RecipeIngredient ingredient in recipe.ingredients) {
ListIngredient? newIngredient = ListIngredient? newIngredient = await ListIngredient.create(
await ListIngredient.create(ingredient.name, homegroup); homegroup, ingredient.name, ingredient.quantity);
if (newIngredient != null) { if (newIngredient != null) {
anySuccesses = true; anySuccesses = true;
@@ -81,7 +77,7 @@ class ShoppingList {
} }
if (anySuccesses) { if (anySuccesses) {
return patch(updates: updates + 1); return get(homegroup);
} }
return null; return null;
@@ -98,7 +94,7 @@ class ShoppingList {
} }
if (anySuccess) { if (anySuccess) {
return patch(updates: updates + 1); return get(homegroup);
} }
return null; return null;

View File

@@ -7,12 +7,14 @@ import 'package:http/http.dart' as http;
class ListIngredient { class ListIngredient {
int id; int id;
String name; String name;
String? quantity;
int list; int list;
bool inCart; bool inCart;
ListIngredient({ ListIngredient({
required this.id, required this.id,
required this.name, required this.name,
required this.quantity,
required this.list, required this.list,
required this.inCart, required this.inCart,
}); });
@@ -21,21 +23,33 @@ class ListIngredient {
return ListIngredient( return ListIngredient(
id: json["id"] as int, id: json["id"] as int,
name: json["name"] as String, name: json["name"] as String,
quantity: json["quantity"] as String?,
list: json["list"] as int, list: json["list"] as int,
inCart: json["in_cart"] as bool, inCart: json["in_cart"] as bool,
); );
} }
static Future<ListIngredient?> create(String name, int list) async { static Future<ListIngredient?> create(
int list, String name, String? quantity) async {
const String requestURL = "$baseURL/api/listingredients/"; const String requestURL = "$baseURL/api/listingredients/";
String token = TokenSingleton().getToken(); String token = TokenSingleton().getToken();
Map<String, dynamic> body = {
"name": name,
"list": list,
};
if (quantity != null) {
body["quantity"] = quantity;
}
http.Response response = await http.post( http.Response response = await http.post(
Uri.parse(requestURL), Uri.parse(requestURL),
headers: {"Authorization": "Token $token"}, headers: {
body: { "Authorization": "Token $token",
"name": name, "Content-Type": "application/json",
"list": "$list",
}, },
body: jsonEncode(body),
); );
if (response.statusCode == 201) { if (response.statusCode == 201) {
@@ -45,21 +59,27 @@ class ListIngredient {
} }
} }
Future<ListIngredient?> patch({String? name, bool? inCart}) async { Future<ListIngredient?> patch(
{String? name, String? quantity, bool? inCart}) async {
String requestURL = "$baseURL/api/listingredients/$id/"; String requestURL = "$baseURL/api/listingredients/$id/";
String token = TokenSingleton().getToken(); String token = TokenSingleton().getToken();
Map<String, String> body = {}; Map<String, dynamic> body = {"quantity": quantity ?? this.quantity};
if (name != null) { if (name != null) {
body["name"] = name; body["name"] = name;
} }
if (inCart != null) { if (inCart != null) {
body["in_cart"] = "$inCart"; body["in_cart"] = inCart;
} }
http.Response response = await http.patch(Uri.parse(requestURL), http.Response response = await http.patch(Uri.parse(requestURL),
headers: {"Authorization": "Token $token"}, body: body); headers: {
"Authorization": "Token $token",
"Content-Type": "application/json",
},
body: jsonEncode(body));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return ListIngredient.fromJson(jsonDecode(response.body)); return ListIngredient.fromJson(jsonDecode(response.body));
@@ -86,8 +106,9 @@ class ListIngredient {
other is ListIngredient && other is ListIngredient &&
other.id == id && other.id == id &&
other.name == name && other.name == name &&
other.quantity == quantity &&
other.inCart == inCart; other.inCart == inCart;
@override @override
int get hashCode => Object.hash(id, name, inCart); int get hashCode => Object.hash(id, name, quantity, inCart);
} }

View File

@@ -7,11 +7,13 @@ import 'package:http/http.dart' as http;
class RecipeIngredient { class RecipeIngredient {
int id; int id;
String name; String name;
String? quantity;
int recipe; int recipe;
RecipeIngredient({ RecipeIngredient({
required this.id, required this.id,
required this.name, required this.name,
required this.quantity,
required this.recipe, required this.recipe,
}); });
@@ -19,20 +21,32 @@ class RecipeIngredient {
return RecipeIngredient( return RecipeIngredient(
id: json["id"] as int, id: json["id"] as int,
name: json["name"] as String, name: json["name"] as String,
quantity: json["quantity"] as String?,
recipe: json["recipe"] as int, recipe: json["recipe"] as int,
); );
} }
static Future<RecipeIngredient?> create(String name, int recipeID) async { static Future<RecipeIngredient?> create(
int recipeID, String name, String? quantity) async {
const String requestURL = "$baseURL/api/recipeingredients/"; const String requestURL = "$baseURL/api/recipeingredients/";
String token = TokenSingleton().getToken(); String token = TokenSingleton().getToken();
Map<String, dynamic> body = {
"name": name,
"recipe": recipeID,
};
if (quantity != null) {
body["quantity"] = quantity;
}
http.Response response = await http.post( http.Response response = await http.post(
Uri.parse(requestURL), Uri.parse(requestURL),
headers: {"Authorization": "Token $token"}, headers: {
body: { "Authorization": "Token $token",
"name": name, "Content-Type": "application/json",
"recipe": "$recipeID",
}, },
body: jsonEncode(body),
); );
if (response.statusCode == 201) { if (response.statusCode == 201) {
@@ -42,12 +56,22 @@ class RecipeIngredient {
} }
} }
Future<RecipeIngredient?> patch(String name) async { Future<RecipeIngredient?> patch({String? name, String? quantity}) async {
Map<String, dynamic> body = {"quantity": quantity};
if (name != null) {
body["name"] = name;
}
String requestURL = "$baseURL/api/recipeingredients/$id/"; String requestURL = "$baseURL/api/recipeingredients/$id/";
String token = TokenSingleton().getToken(); String token = TokenSingleton().getToken();
http.Response response = await http.patch(Uri.parse(requestURL), http.Response response = await http.patch(Uri.parse(requestURL),
headers: {"Authorization": "Token $token"}, body: {"name": name}); headers: {
"Authorization": "Token $token",
"Content-Type": "application/json",
},
body: jsonEncode(body));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return RecipeIngredient.fromJson(jsonDecode(response.body)); return RecipeIngredient.fromJson(jsonDecode(response.body));

View File

@@ -6,8 +6,9 @@ import 'package:one_trip/api/models/list.dart';
import 'package:one_trip/api/models/listingredient.dart'; import 'package:one_trip/api/models/listingredient.dart';
import 'package:one_trip/api/models/user.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/listrow.dart';
import 'package:one_trip/pages/list_page/widgets/search_recipes.dart'; import 'package:one_trip/pages/list_page/widgets/search_recipes_dialog.dart';
import 'package:one_trip/widgets/text_entry_dialog.dart'; import 'package:one_trip/widgets/confirm_dialog.dart';
import 'package:one_trip/widgets/ingredient_dialog.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
class ListPage extends StatefulWidget { class ListPage extends StatefulWidget {
@@ -56,8 +57,8 @@ class _ListPageState extends State<ListPage> {
} }
} }
}, },
// ignore: avoid_print
onError: (error) => print("Websocket error: $error"), onError: (error) => print("Websocket error: $error"),
onDone: () => print("Websocket Done"),
); );
} }
@@ -108,15 +109,18 @@ class _ListPageState extends State<ListPage> {
return ListArea( return ListArea(
list: _list!, list: _list!,
onAddOne: () async { onAddOne: () async {
String? itemName = IngredientDetails? details =
await textEntryDialog(context, "Item Name", "Item"); await ingredientDialog(context, "", "");
if (itemName == null || itemName == "") { if (details == null || details.name == "") {
return; return;
} }
ListIngredient? newIngredient = ListIngredient? newIngredient = await ListIngredient.create(
await ListIngredient.create(itemName, _list!.homegroup); _list!.homegroup,
details.name,
details.quantity != "" ? details.quantity : null);
if (newIngredient == null) { if (newIngredient == null) {
return; return;
} }
@@ -288,7 +292,12 @@ class ListArea extends StatelessWidget {
foregroundColor: MaterialStatePropertyAll( foregroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.onError), Theme.of(context).colorScheme.onError),
), ),
onPressed: () => onClear(), onPressed: () async {
bool doDelete = await confirmDialog(context, "Clear List");
if (doDelete) {
onClear();
}
},
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [Icon(Icons.delete), Text("Clear List")], children: const [Icon(Icons.delete), Text("Clear List")],

View File

@@ -67,7 +67,9 @@ class _ListRowState extends State<ListRow> {
Expanded( Expanded(
child: Text( child: Text(
// _ingredient.name, // _ingredient.name,
widget.ingredient.name, widget.ingredient.quantity == null
? widget.ingredient.name
: "${widget.ingredient.name} - ${widget.ingredient.quantity}",
style: Theme.of(context).textTheme.titleMedium!.copyWith( style: Theme.of(context).textTheme.titleMedium!.copyWith(
decoration: widget.ingredient.inCart decoration: widget.ingredient.inCart
? TextDecoration.lineThrough ? TextDecoration.lineThrough

View File

@@ -80,6 +80,7 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
child: PaginationListView( child: PaginationListView(
state: _listState, state: _listState,
shrinkWrap: true, shrinkWrap: true,
prefetchOne: true,
itemBuilder: (context, data) { itemBuilder: (context, data) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
@@ -98,7 +99,13 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
color: selectedIDs.contains(data.id) color: selectedIDs.contains(data.id)
? Theme.of(context).colorScheme.secondary ? Theme.of(context).colorScheme.secondary
: null, : null,
child: Text(data.name), child: Text(
data.name,
style: TextStyle(
color: selectedIDs.contains(data.id)
? Theme.of(context).colorScheme.onSecondary
: null),
),
), ),
); );
}, },
@@ -106,16 +113,6 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
return const Divider(); return const Divider();
}, },
dataProvider: (int page) async { dataProvider: (int page) async {
// SearchResult<SimpleUser> result =
// await SimpleUser.search(_searchController.text, page);
// List<dynamic> users = List<dynamic>.from(result.results);
// if (result.next == null) {
// users.add(null);
// }
// return users;
SearchResult<Recipe> result = SearchResult<Recipe> result =
await Recipe.search(_searchController.text, page); await Recipe.search(_searchController.text, page);
List<dynamic> recipes = List<dynamic> recipes =
@@ -137,12 +134,15 @@ class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ElevatedButton( ElevatedButton(
style: negativeButtonStyle(context),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context, selectedIDs), style: positiveButtonStyle(context),
child: const Text("Done")), onPressed: () => Navigator.pop(context, selectedIDs),
child: const Text("Done"),
),
], ],
), ),
) )

View File

@@ -139,12 +139,15 @@ class _InviteHomegroupDialogState extends State<InviteHomegroupDialog> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ElevatedButton( ElevatedButton(
style: negativeButtonStyle(context),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context, selectedIDs), style: positiveButtonStyle(context),
child: const Text("Done")), onPressed: () => Navigator.pop(context, selectedIDs),
child: const Text("Done"),
),
], ],
), ),
) )

View File

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:one_trip/api/models/recipeingredient.dart'; import 'package:one_trip/api/models/recipeingredient.dart';
import 'package:one_trip/api/models/recipe.dart'; import 'package:one_trip/api/models/recipe.dart';
import 'package:one_trip/theme.dart'; import 'package:one_trip/theme.dart';
import 'package:one_trip/widgets/text_entry_dialog.dart'; import 'package:one_trip/widgets/ingredient_dialog.dart';
class RecipeCard extends StatefulWidget { class RecipeCard extends StatefulWidget {
final Recipe recipe; final Recipe recipe;
@@ -145,15 +145,18 @@ class _RecipeCardState extends State<RecipeCard> with TickerProviderStateMixin {
shape: const MaterialStatePropertyAll( shape: const MaterialStatePropertyAll(
RoundedRectangleBorder())), RoundedRectangleBorder())),
onPressed: () async { onPressed: () async {
String? name = await textEntryDialog( IngredientDetails? details =
context, "Ingredient Name", "Ingredient"); await ingredientDialog(context, "", "");
if (name == null || name == "") { if (details == null || details.name == "") {
return; return;
} }
RecipeIngredient? ingredient = RecipeIngredient? ingredient =
await RecipeIngredient.create(name, widget.recipe.id); await RecipeIngredient.create(
widget.recipe.id,
details.name,
details.quantity != "" ? details.quantity : null);
if (ingredient != null) { if (ingredient != null) {
widget.onChanged(); widget.onChanged();
} }
@@ -233,21 +236,29 @@ class _IngredientSectionState extends State<IngredientSection> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
widget.ingredients[index].name, widget.ingredients[index].quantity == null
? widget.ingredients[index].name
: "${widget.ingredients[index].name} - ${widget.ingredients[index].quantity}",
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
)), )),
IconButton( IconButton(
onPressed: () async { onPressed: () async {
String? name = await textEntryDialog( IngredientDetails? details = await ingredientDialog(
context, "Change Ingredient Name", "Ingredient", context,
defaultValue: widget.ingredients[index].name); widget.ingredients[index].name,
widget.ingredients[index].quantity ?? "");
if (name == null || name == "") { if (details == null || details.name == "") {
return; return;
} }
RecipeIngredient? changed = RecipeIngredient? changed =
await widget.ingredients[index].patch(name); await widget.ingredients[index].patch(
name: details.name,
quantity: details.quantity != ""
? details.quantity
: null);
if (changed != null) { if (changed != null) {
widget.onChanged(); widget.onChanged();
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:one_trip/pages/list_page/list_page.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/profile_page/profile_page.dart';
import 'package:one_trip/pages/recipes_page/recipes_page.dart'; import 'package:one_trip/pages/recipes_page/recipes_page.dart';
import 'package:one_trip/pages/themetest.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});

View File

@@ -38,3 +38,28 @@ class MyBehavior extends ScrollBehavior {
return child; return child;
} }
} }
ButtonStyle positiveButtonStyle(BuildContext context) {
Brightness brightness = Theme.of(context).colorScheme.brightness;
if (brightness == Brightness.dark) {
return ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.green[200]),
foregroundColor: MaterialStatePropertyAll(Colors.green[900]),
);
} else {
return ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.green[900]),
foregroundColor: const MaterialStatePropertyAll(Colors.white),
);
}
}
ButtonStyle negativeButtonStyle(BuildContext context) {
return ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
foregroundColor:
MaterialStatePropertyAll(Theme.of(context).colorScheme.onError),
);
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:one_trip/theme.dart';
class ConfirmForm extends StatefulWidget {
final String title;
const ConfirmForm({super.key, required this.title});
@override
State<ConfirmForm> createState() => _ConfirmFormState();
}
class _ConfirmFormState extends State<ConfirmForm> {
@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(
widget.title,
style: Theme.of(context).textTheme.titleMedium,
),
const Divider(),
const Text("This action is permanent. Do you want to continue?"),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
style: positiveButtonStyle(context),
onPressed: () => Navigator.pop(context, false),
child: const Text("Go Back"),
),
ElevatedButton(
style: negativeButtonStyle(context),
onPressed: () => Navigator.pop(context, true),
child: const Text("Continue"),
),
],
),
),
],
),
),
);
}
}
Future<bool> confirmDialog(BuildContext context, String title) async {
bool? value = await showDialog(
context: context,
builder: (context) {
return Dialog(
child: ConfirmForm(
title: title,
),
);
},
);
return value ?? false;
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:one_trip/theme.dart';
class IngredientDetails {
String name;
String quantity;
IngredientDetails({required this.name, required this.quantity});
}
class IngredientForm extends StatefulWidget {
final String nameStartingValue;
final String quantityStartingValue;
const IngredientForm(
{super.key,
required this.nameStartingValue,
required this.quantityStartingValue});
@override
State<IngredientForm> createState() => _IngredientFormState();
}
class _IngredientFormState extends State<IngredientForm> {
late TextEditingController _nameController;
late TextEditingController _quantityController;
@override
void dispose() {
_nameController.dispose();
_quantityController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.nameStartingValue);
_quantityController =
TextEditingController(text: widget.quantityStartingValue);
}
@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(
"Add / Edit Ingredient",
style: Theme.of(context).textTheme.titleMedium,
),
const Divider(),
TextFormField(
autofocus: true,
controller: _nameController,
textInputAction: TextInputAction.next,
// onFieldSubmitted: (value) {
// Navigator.pop(context, value);
// },
decoration: const InputDecoration(hintText: "Name"),
),
TextFormField(
controller: _quantityController,
textInputAction: TextInputAction.done,
onFieldSubmitted: (value) => Navigator.pop(
context,
IngredientDetails(
name: _nameController.text,
quantity: _quantityController.text),
),
decoration: const InputDecoration(hintText: "Quantity"),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
style: negativeButtonStyle(context),
onPressed: () => Navigator.pop(context),
child: const Text("Cancel"),
),
ElevatedButton(
style: positiveButtonStyle(context),
onPressed: () => Navigator.pop(
context,
IngredientDetails(
name: _nameController.text,
quantity: _quantityController.text),
),
child: const Text("Done"),
),
],
),
),
],
),
),
);
}
}
Future<IngredientDetails?> ingredientDialog(
BuildContext context, String currentName, String currentQuantity) async {
IngredientDetails? details = await showDialog(
context: context,
builder: (context) {
return Dialog(
child: IngredientForm(
nameStartingValue: currentName,
quantityStartingValue: currentQuantity,
),
);
},
);
return details;
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:one_trip/theme.dart';
class TextEntryForm extends StatefulWidget { class TextEntryForm extends StatefulWidget {
final String title; final String title;
@@ -55,13 +56,16 @@ class _TextEntryFormState extends State<TextEntryForm> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ElevatedButton( ElevatedButton(
style: negativeButtonStyle(context),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => style: positiveButtonStyle(context),
Navigator.pop(context, _textController.text), onPressed: () =>
child: const Text("Done")), Navigator.pop(context, _textController.text),
child: const Text("Done"),
),
], ],
), ),
), ),

View File

@@ -0,0 +1,27 @@
# Generated by Django 4.1.3 on 2022-12-06 22:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0005_list_updates'),
]
operations = [
migrations.RemoveField(
model_name='list',
name='updates',
),
migrations.AddField(
model_name='listingredient',
name='quantity',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name='recipeingredient',
name='quantity',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

View File

@@ -26,7 +26,6 @@ class Homegroup(models.Model):
class List(models.Model): class List(models.Model):
# Foreign Key ListIngredient -> List [as ingredients] # Foreign Key ListIngredient -> List [as ingredients]
homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True) homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True)
updates = models.BigIntegerField(default=0);
class Recipe(models.Model): class Recipe(models.Model):
@@ -37,9 +36,11 @@ class Recipe(models.Model):
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
quantity = models.CharField(max_length=50, null=True, blank=True)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients") recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")
class ListIngredient(models.Model): class ListIngredient(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
quantity = models.CharField(max_length=50, null=True, blank=True)
list = models.ForeignKey(List, on_delete=models.CASCADE, related_name="ingredients") list = models.ForeignKey(List, on_delete=models.CASCADE, related_name="ingredients")
in_cart = models.BooleanField(default=False) in_cart = models.BooleanField(default=False)

View File

@@ -9,12 +9,12 @@ channel_layer = get_channel_layer()
class RecipeIngredientSerializer(serializers.ModelSerializer): class RecipeIngredientSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RecipeIngredient model = RecipeIngredient
fields = ["id", "name", "recipe"] fields = ["id", "name", "quantity", "recipe"]
class ListIngredientSerializer(serializers.ModelSerializer): class ListIngredientSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ListIngredient model = ListIngredient
fields = ["id", "name", "list", "in_cart"] fields = ["id", "name", "quantity", "list", "in_cart"]
class RecipeSerializer(serializers.ModelSerializer): class RecipeSerializer(serializers.ModelSerializer):
ingredients = serializers.SerializerMethodField() ingredients = serializers.SerializerMethodField()
@@ -33,13 +33,9 @@ class ListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = List model = List
fields = ["homegroup", "updates", "ingredients"] fields = ["homegroup", "ingredients"]
read_only_fields = ["homegroup"] 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): def get_ingredients(self, instance):
ingredients = instance.ingredients.all().order_by("name") ingredients = instance.ingredients.all().order_by("name")
return ListIngredientSerializer(ingredients, many=True).data return ListIngredientSerializer(ingredients, many=True).data

View File

@@ -12,7 +12,7 @@ class HasHomegroup(permissions.BasePermission):
return super().has_permission(request, view) return super().has_permission(request, view)
class Pagination(pagination.PageNumberPagination): class Pagination(pagination.PageNumberPagination):
page_size = 4 page_size = 10
class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): class NoListModelViewset(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
pass pass

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -20,30 +20,36 @@
## Pictures ## Pictures
### Main Menu ### Main Menu
![Login Screen](https://user-images.githubusercontent.com/44736322/204997033-fa10ceae-ae54-4816-b043-a1663532b5b3.png) ![Login Screen](https://user-images.githubusercontent.com/44736322/206134503-490b7b5c-9ef1-4a32-af2f-a664b587c4d6.png)
### Profile Page ### Profile Page
![Profile Page](https://user-images.githubusercontent.com/44736322/204997046-d3b09233-266f-4082-8f05-4b97d5369a18.png) ![Profile Page](https://user-images.githubusercontent.com/44736322/206134506-7c035ea7-8431-4116-b61f-f1bbe0b23713.png)
### Search for Users ### Search for Users
![Searching for users](https://user-images.githubusercontent.com/44736322/204997074-464c3b0c-96c2-4ac0-8e28-ebc1319c0065.png) ![Searching for users](https://user-images.githubusercontent.com/44736322/206134507-c2e5c805-3c0e-4ead-9c48-b047e5fb75dc.png)
Searched Users are sorted by first name, then last name, then username
### Create, Edit, and Delete Recipes ### Create, Edit, and Delete Recipes
![Create Edit Delete Recipes](https://user-images.githubusercontent.com/44736322/204997088-cb4a34e4-9c09-408f-b15d-4b30a8409f5c.png) ![Create Edit Delete Recipes](https://user-images.githubusercontent.com/44736322/206134508-94332b3e-ea62-47e5-8426-c876e9f7ce24.png)
Recipes and individual ingredients can be deleted by swiping them off of the screen Recipes and individual ingredients can be deleted by swiping them off of the screen
### Search for Recipes to add to Shopping List ### Search for Recipes to add to Shopping List
![Search Recipes](https://user-images.githubusercontent.com/44736322/204997188-7e03d067-a26e-4629-be3f-ef9039eee54b.png) ![Search Recipes](https://user-images.githubusercontent.com/44736322/206134511-c0fd19b3-9816-4d87-bfde-54fb110c2fe5.png)
### Delete Items from List ### Delete Items from List
![swipe](https://user-images.githubusercontent.com/44736322/205004229-c59ef486-edb6-4595-aa0c-846f008ab1a1.gif) https://user-images.githubusercontent.com/44736322/206134495-b6b97f81-b77b-4068-8359-39e64d5312ca.mp4
### Add Additional Items
![Add Individual](https://user-images.githubusercontent.com/44736322/206134513-c6d7bedd-659f-45de-83ef-7aa4b1a9378f.png)
### Shopping List Page ### Shopping List Page
![Shopping List Page](https://user-images.githubusercontent.com/44736322/204997203-5341a35e-9269-43eb-a212-6404c8119482.png) ![Shopping List Page](https://user-images.githubusercontent.com/44736322/206134515-1636e086-80a7-45df-801b-af90c2059d10.png)
### Backend Data Models ### Backend Data Models
![backend-structure](https://user-images.githubusercontent.com/44736322/205004669-9f9387e0-1f76-42ac-8d49-d94cd17518aa.png) ![backend-structure](https://user-images.githubusercontent.com/44736322/206135954-63c728ce-3662-4e10-9914-557ce65f04ee.png)