Kivy recycleview duplicating data items - python-3.x

I have a recycle view in kivy im writing to it with a list containing text and images, the text is fine but as soon as i write an image its duplicating the image in the view as i scroll down please see code below, how would i stop this from happening, here is my kv file I have a boxlayout with two sets of data, heres a picture of the problem as wellexample
kv file:
<SelectableReportTextbox#BoxLayout>:
text: ''
imagesource: ''
color: ''
Button:
size_hint: .8, 1
text_size : self.text_size
size_hint_y: None
foreground_color: [1, 1, 1, 1]
background_color: (0.2, 0.4, 0.8, 0.9) # root.color
font_name: 'C:\kivy_venv\Graphics\GIL_____.TTF'
font_size: self.height*0.2
background_normal: root.imagesource
text: root.text
ScreenTwo:
rv2: rv2
canvas.before:
Rectangle:
size:self.size #100, 100
pos: self.pos
source: "C:\kivy_venv\Graphics\Jetfireback.png"
RecycleView_B:
bar_width: 6
size_hint: (None, None)
do_scroll_y: True
id: scrlv2
size: (500, 500)
pos_hint: {'center_x': .75, 'center_y': .64}
multiline:True
ProjectRV:
viewclass: 'SelectableReportTextbox' #
orientation: "horizontal"
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
key_size: "height"
padding:1, 1
space_x: self.size[0]/3
id: rv2
pos_hint: {'center_x': 0.32, 'center_y': 0.525}
bar_width: dp(25)
bar_color: (0.7, 0.1, 0.3, 0.7)
bar_inactive_color: (0.1, 0.1, 0.1 , 1)
scroll_y : 0
SelectableRecycleBoxLayout:
rv2:rv2
spacing : '6'
default_size_hint: 1, None
size_hint_y: None
size_hint_x: 1
height: self.minimum_height
multiselect: True
touch_multiselect: True
orientation: 'vertical'
main.py:
class SelectableRecycleBoxLayout(FocusBehavior,LayoutSelectionBehavior,RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableReportTextbox(RecycleDataViewBehavior):
selectable = BooleanProperty(True)
index =None
selected = BooleanProperty(False)
def __init__(self, **kw):
super().__init__(**kw)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableRecycleBoxLayout, self).refresh_view_attrs(
rv, index, data)
class ProjectRV(RecycleView):
def __init__(self, **kwargs):
super(ProjectRV, self).__init__(**kwargs)
class RecycleView_B(RecycleView):
pass
class ScreenTwo(Screen):
TEXT_FILE = open("output.txt", "a")
zdata = re.search('{.*,', strdata).group(0)
splitex = zdata.split(':' + "'")[1]
print (zdata)
TEXT_FILE.write(zdata)
TEXT_FILE.close()
with open("output.txt", "r") as f:
txtdata = eval(str('[' + ''.join(f.readlines()) + ']'))#ast.literal_eval
nstring = type(txtdata)
self.rv2.data = txtdata
self.rv2.refresh_from_viewport()
self.rv2.refresh_from_data()

The RecycleView re-uses viewclass object instances, and sets properties of those instances based on the data dictionaries. So, if one of your dictionaries sets the imagesource property of a viewclsas instance and then that instance is re-used for a dictionary that does not set the imagesource property, then that instance will retain that prior value of imagesource. The fix is to make sure that every dictionary in the data contains a value for every property of the viewclass. Each dictionary needs to include values for text, imagesource, and color, even if the value is ''.

Related

Adding Kivy recycleview to screenmanager breaks example

I'm trying to combine this recycleview example with this screenmanager example so that the recycleview example can be one of the screens in my app. The app runs but all the control buttons display at the bottom of the UI (They're supposed to be at the top) and are either disabled or obscured, disallowing input. Consequently, the recycleview cannot be populated and/or viewed.
Here's my attempt:
import asyncio
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
kv = '''
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
#:import BoxLayout kivy.uix.boxlayout.BoxLayout
#:import RecycleDataViewBehavior kivy.uix.recycleview.views.RecycleDataViewBehavior
ScreenManagement:
transition: FadeTransition()
ContactsScreen:
name: 'contacts'
MessengerScreen:
name: 'messenger'
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<ContactsScreen>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
<MessengerScreen>:
Button:
on_release: app.root.current = 'contacts'
text: 'back to the home screen'
font_size: 50
'''
class ContactsScreen(Screen):
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class MessengerScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_string(kv)
class MainApp(App):
def build(self):
return presentation
def main_runner(self):
async def run_wrapper():
# we don't actually need to set asyncio as the lib because it is
# the default, but it doesn't hurt to be explicit
await self.async_run(async_lib='asyncio')
print('App done')
#self.other_task.cancel()
return asyncio.gather(run_wrapper())
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(MainApp().main_runner())
loop.close()
The trouble seems to come from the <Row#RecycleKVIDsDataViewBehavior+BoxLayout> directive but I can't seem to fix it no matter where I place it.
You can position the Buttons using pos_hint, like this:
GridLayout:
id: grid
pos_hint: {'center_x': 0.5, 'top': 1}
Then, you need to size the RecycleView so that it is not over the Buttons, like this:
RecycleView:
id: rv
size_hint_y: None
height: root.height - grid.height

Kivy use two files .kv

i'm trying to switch kv files by a function on my .py code. In my first kivy code there are few screens and in my second kivy code there is a swipebutton#carausel.Is there any way for connect the two files. I post an example: <Screen1> id:_screen1 on another file there is: <SwipeButton#Carousel>. I hope someone can help me
This is the output received when I unload the first file and load the second, it doesn't break the code:
`
class MainApp(MDApp)
def build(self):
self.title = "Meet!"
if "Colore" in impostazioni:
self.theme_cls.theme_style = impostazioni.get("Colore")["coloresfondo"]
else:
print("Nulla")
self.theme_cls.theme_style = "Light"
if "Nome" in impostazioni:
Nome = impostazioni.get("Nome")["nome"]
print(Nome)
else:
print("Non trovato")
if "Sesso" in impostazioni:
Sesso1 = impostazioni.get("Sesso")["sesso"]
print(Sesso1)
else:
print("Non trovato")
self.theme_cls.primary_palette = "Red"
self.theme_cls.primary_hue = "A700"
self.dativari = [{'id': i, 'data_index': i, 'index': 1, 'height': 48, 'text': str(calendariofile.get(str(i)))} for i in calendariofile]
self.screen = Builder.load_file("num3.kv")
self.root = Builder.load_file("prova.kv")
return self.screen`
#:import C kivy.utils.get_color_from_hex <SwipeButton#Carousel>: text: '' size_hint_y: None height: 48 ignore_perpendicular_swipes: True data_index: 0 min_move: 20 / self.width on__offset: app.aggiorna(root.data_index)#print(root.data_index) #app.update_index(root.data_index, self.index) canvas.before: Color: rgba: C('FFFFFF33') Rectangle: pos: self.pos size: self.size Line: rectangle: self.pos + self.size Button: text: 'delete ({}:{})'.format(root.text, root.data_index) on_press: app.elimina(root.data_index) Label: text: root.text Button: text: 'archive' on_press: app.passachat(root.data_index) RecycleView: data: app.dativari viewclass: 'SwipeButton' do_scroll_x: False scroll_timeout: 100 RecycleBoxLayout: orientation: 'vertical' size_hint_y: None height: self.minimum_height default_size_hint: 1, None
If the two kv files do not redefine the same classes, then you can just load them both.
If the two kv files do redefine the same classes, then you can use Builder.unload_file() to unload one before loading the other. Note that loading/unloading kv files will not affect Widgets already created, it will only affect Widgets created after the change.

Content at bottom of screen... after adding ToolBar with Kivy

I'm just trying to add a simple 'ToolBar' but after the tool bar kept aligning to the bottom I found that AnchorLayout: let's me anchor the toolbar to the top. But all my content is now only showing in the bottom half of the screen... I'm not sure why.. Adjusting the 'center_y': does nothing... Does anyone see my issue, I really appreciate it.
main
from kivymd.app import MDApp
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.dialog import MDDialog
from kivymd.uix.button import MDFlatButton, MDIconButton
from kivy.uix.scrollview import ScrollView
from kivy.uix.screenmanager import Screen, ScreenManager
from PyDictionary import PyDictionary
import sys
import json
import requests
class Manager(ScreenManager):
"""Manages Screens"""
class Main(Screen):
"""main application goes here"""
def close_dialog(self, obj):
self.dialog.dismiss()
def show_data(self):
message = """
This little program was
designed to help re-think
your sentences.
"""
close = MDIconButton(icon="close-circle", on_release=self.close_dialog)
#more = MDIconButton(icon="more")
self.dialog = MDDialog(title="Probably Knot", text=message,
size_hint=(0.5, 1), buttons=[close])
self.dialog.open()
class Analyzer(Screen):
def analyze(self, main): # main is pointing to ---> Main().show_data()
"""Analyse data with PyDictionary"""
sent = main.ids.sentence.text.lower()
wrd = main.ids.word.text.lower()
# Definition Section #
dictionary = PyDictionary()
define_wrd = dictionary.meaning(wrd)
noun = ''
verb = ''
adjective = ''
result = ''
try:
noun = " ".join(define_wrd['Noun'])
result += f"Definition:\n1. {wrd}:\n{noun}\n"
except TypeError or KeyError:
noun = False
print('Noun, is not found in API http://words.bighugelabs.com/api')
try:
verb = " ".join(define_wrd['Verb'])
result += f"2.\n{verb}\n"
except TypeError or KeyError:
verb = False
print('Verb, is not found in API http://words.bighugelabs.com/api')
try:
adjective = " ".join(define_wrd['Adjective'])
result += f"3.\n{adjective}\n"
except TypeError or KeyError:
adjective = False
print('Adjective, is not found in API http://words.bighugelabs.com/api')
if not noun and not verb and not adjective:
error = MDDialog(title="Error", text=f"Word: '{wrd}' is not in\n\n'dictionary'")
error.open()
if wrd != '' and sent != '':
API_KEY = 'a701e74e453ee6695e450310340401f5'
URL = f'http://words.bighugelabs.com/api/2/{API_KEY}/{wrd}/json'
if wrd not in sent:
error = MDDialog(title="Error", text=f"Word: '{wrd}' is not in\n\n'{sent}'")
error.open()
else:
r = requests.get(URL) # get's url json file
j = json.loads(r.text) # loads json into 'j' as a dict
if type(j) == dict: # check is 'j' variable is coming in as a Dict
# holds the new sentences
new = f"{result}\n"
try:
for num, w in enumerate(j['adjective']['syn'], 1):
new += f"{num}: {sent.replace(wrd, w)}\n"
except KeyError:
print(f'Adjective for "{wrd}" is not found.')
try:
for num, w in enumerate(j['noun']['syn'], 1):
new += f"{num}: {sent.replace(wrd, w)}\n"
except KeyError:
print(f'Noun for "{wrd}" is not found.')
try:
for num, w in enumerate(j['verb']['syn'], 1):
new += f"{num}: {sent.replace(wrd, w)}\n"
except KeyError:
print(f'Verb for "{wrd}" is not found.')
class ProbablyKnotApp(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Yellow"
self.theme_cls.primary_hue = "A700"
return Manager()
ProbablyKnotApp().run()
kv file
<Manager>:
Main:
name: 'main'
Analyzer:
name: 'analyzer'
<Main>:
AnchorLayout:
anchor_x: 'center'
anchor_y: 'top'
MDToolbar:
title: "Probably Knot"
pos_hint_y: None
pos_y: 1
md_bg_color: app.theme_cls.accent_color
right_action_items: [["dots-vertical", lambda x: app.callback()]]
MDBoxLayout:
orientation: 'vertical'
MDRectangleFlatButton:
text: "help"
pos_hint: {'center_x': 0.75, 'center_y': 1}
on_release: app.root.get_screen('main').show_data()
MDTextField:
id: sentence
icon_right: "book-open-outline"
icon_right_color: app.theme_cls.primary_color
hint_text: "Enter Sentence"
helper_text: "Write a problem statement to analyze"
helper_text_mode: "on_focus"
pos_hint: {'center_x': 0.5, 'center_y': 0.7}
size_hint_x: None
width: 400
MDTextField:
id: word
icon_right: "lead-pencil"
icon_right_color: app.theme_cls.primary_color
hint_text: "Enter Word"
helper_text: "Write ONE word from the above sentence"
helper_text_mode: "on_focus"
pos_hint: {'center_x': 0.5, 'center_y': 0.6}
size_hint_x: None
width: 400
MDIconButton:
icon: "card-plus"
pos_hint: {'center_x': 0.75, 'center_y': 0.5}
on_release: app.root.get_screen('analyzer').analyze(root)
<Analyzer>:
MDToolbar:
title: "Probably Knot"
md_bg_color: app.theme_cls.accent_color
right_action_items: [["dots-vertical", lambda x: app.callback()]]
MDList:
id: container
The problem is that you are not taking into account the default values for size_hint (1,1) and pos (0,0). So your AnchorLayout fills your Main Screen, because of the default values, but the anchor_y setting puts the MDToolBar at the top of the AnchorLayout.
Similarly, your MDBoxLayout also fills the entire Main Screen, but because the MDRectangleFlatButton and the MDTextField have pre-defined sizes, they don't fill the MDBoxLayout. So they only fill the bottom half of the MDBoxLayout.
So here is a version of your kv (for the Main Screen) that uses pos_hint for the MDToolBar and minimum_height for the MDBoxLayout and actually setting its y value to bump up against the tool bar:
<Main>:
MDToolbar:
id: toolbar
title: "Probably Knot"
pos_hint: {'top':1.0}
md_bg_color: app.theme_cls.accent_color
right_action_items: [["dots-vertical", lambda x: app.callback()]]
MDBoxLayout:
size_hint: 1, None
height: self.minimum_height
y: root.height - toolbar.height - self.height
orientation: 'vertical'
MDRectangleFlatButton:
text: "help"
pos_hint: {'center_x': 0.75, 'top': 1}
on_release: app.root.get_screen('main').show_data()
MDTextField:
id: sentence
icon_right: "book-open-outline"
icon_right_color: app.theme_cls.primary_color
hint_text: "Enter Sentence"
helper_text: "Write a problem statement to analyze"
helper_text_mode: "on_focus"
pos_hint: {'center_x': 0.5, 'top': 0.7}
size_hint_x: None
width: 400
MDTextField:
id: word
icon_right: "lead-pencil"
icon_right_color: app.theme_cls.primary_color
hint_text: "Enter Word"
helper_text: "Write ONE word from the above sentence"
helper_text_mode: "on_focus"
pos_hint: {'center_x': 0.5, 'center_y': 0.6}
size_hint_x: None
width: 400
MDIconButton:
icon: "card-plus"
pos_hint: {'center_x': 0.75, 'center_y': 0.5}
on_release: app.root.get_screen('analyzer').analyze(root)

How to add multiple widget at the same time to a scrollview?

I'm new at kivy, so I'm working on a test app. I want to create a screen with ScrollView and I want to add multiple things to a 'line' in the ScrollView, a text (description) and an image.
I have tried this way:
class PresentUploadedData(Screen):
container = ObjectProperty(None)
def __init__(self, **kwargs):
super(PresentUploadedData, self).__init__(**kwargs)
Clock.schedule_once(self.setup_scrollview, 1)
def setup_scrollview(self, dt):
self.container.bind(minimum_height=self.container.setter('height'))
self.add_text_inputs()
def add_text_inputs(self):
for x in range(30):
self.container.add_widget(Label(text="Label {}".format(x), size_hint_y=None, height=40, color= [128,0,0,1]))
self.container.add_widget(Image(source='test.jpg', size_hint=(None, None,), width=50, height=50))
with this .kv file:
<PresentUploadedData>:
name: "presentupload"
message: send
container: container
canvas:
Color:
rgba: 1,1,1,1
Rectangle:
size: self.size
pos: self.pos
GridLayout:
rows: 3
cols: 1
spacing: 5
padding: 5
font_name: "Calibri"
background_color: 1,1,1, 1
ScrollView:
background_color: 1,1,1, 1
size_hint: (1, .9)
bar_width: 10
bar_color: 128,0,0,0.7
bar_inactive_color: 128,0,0,1
effect_cls: "ScrollEffect"
scroll_type: ['bars', 'content']
color: 128,0,0,1
StackLayout:
id: container
size_hint_y: None
color: 128,0,0,1
GridLayout:
cols: 2
BoxLayout:
spacing: 5
size_hint: .7, .1
Button:
text: "< BACK"
id: send
color: 0, 0, 0, 1
background_color: .88,.88,.88, 1
size_hint: .2, 1
on_release:
app.root.current = "main"
root.manager.transition.direction = "right"
But I've got ScrollView accept only one widget Exception.
Why I got this, and how can I resolve this?
There is an error in .kvfile:
GridLayout:
cols: 2
This is useless.
The problem can be solved with one more for inside the for loop:
row = [Label(text="Label {}".format(x), size_hint_y=None, height=50, color= [128,0,0,1]), Image(source='test.jpg', size_hint=(None, None,), width=50, height=50)]
for r in row:
self.container.add_widget(r)
But this also didn't put the things near each other, just let them be there.

How to add widgets to a label in scrollview Kivy

I'm trying to make a chat page which looks like a standard messaging app on a phone (Whatsapp, Imessage etc). To do this I have created a screen which contains ScrollView and have added a label widget to it.
To get the desired effect of the Whatsapp/Imessage look I think I then want a label widget to be added to the label on the ScrollView everytime the send button is pressed, this will then add the text of the text input box onto the screen.
Is it possible to add a label to another label? Every question I've looked at so far has been about adding a widget to a BoxLayout or GridLayout.
I'm also not sure on how I'll get the position of the label to change everytime the button is pressed but one step at a time!
kv file:
WindowManager:
ChatPage:
<ChatPage>:
name: "chat_page"
layout_content: layout_content
NavigationLayout:
id: nav_layout
MDNavigationDrawer:
NavigationDrawerIconButton:
text: "Test"
FloatLayout:
MDToolbar:
pos_hint: {'top': 1}
md_bg_color: 0.2, 0.6, 1, 1
ScrollView:
size_hint: 1, 0.6
pos_hint: {"top" : 0.8, "bottom" : 0.5}
GridLayout:
id: layout_content
cols: 1
size_hint_y: None
height: self.minimum_height
canvas:
Color:
rgba: (1, 1, 1, 1)
Rectangle:
size: self.size
pos: self.pos
Label:
text_size: self.width - 20, None
size_hint_y: None
height: self.texture_size[1]
color: 0,0,0,1
BoxLayout:
TextInput:
id: msg
hint_text: "Type your message here"
pos_hint: {"x": 0, "top": 0.15}
size_hint: 0.75, 0.15
Button:
text: "Send"
background_normal: ""
background_color: 0, 0.6, 0, 1
pos_hint: {"x": 0.75, "top": 0.15}
size_hint: 0.25, 0.15
on_release: root.btn_press()
<SmoothLabel#Label>:
background_color: 0,0,0,0
background_normal: ""
back_color: 1,0,1,1
border_radius: [6]
canvas.before:
Color:
rgba: 0.2,0.6,1,1 #This changes the label colour
RoundedRectangle:
size: self.size
pos: self.pos
radius: self.border_radius
py file:
import kivy
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty, NumericProperty, ListProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.core.window import Window
from kivymd.theming import ThemeManager
from kivy.uix.scrollview import ScrollView
Window.clearcolor = (1,1,1,1)
class SmoothLabel(Label):
pass
class WindowManager(ScreenManager):
pass
class ChatPage(Screen):
layout_content = ObjectProperty(None)
def btn_press(self):
if self.ids.msg.text:
self.layout_content.add_widget(SmoothLabel(text=self.ids.msg.text, size_hint_x=0.5, size_hint_y=0.1, pos_hint={"x": 0.1, "top": 0.8}, background_color=(0.2, 0.6, 1, 1)))
self.ids.msg.text = ""
else:
pass
class MyApp(App):
theme_cls = ThemeManager()
def build(self):
kv = Builder.load_file("kivy.kv")
sm = WindowManager()
screens = [ChatPage(name="chat_page")]
for screen in screens:
sm.add_widget(screen)
sm.current = "chat_page"
return sm
if __name__ == '__main__':
MyApp().run()
Thanks
Made a few changes to your code (in addition to removing all the kivyMD) to get it to work:
First, I changed the SmoothLabel rule in your kv to:
<SmoothLabel>:
size_hint: None, None
size: self.texture_size
background_color: 0,0,0,0
background_normal: ""
back_color: 1,0,1,1
border_radius: [6]
canvas.before:
Color:
rgba: 0.2,0.6,1,1 #This changes the label colour
RoundedRectangle:
size: self.size
pos: self.pos
radius: self.border_radius
Took out the #Label since that is already specified in your python code, and added the size_hint and size (otherwise, the GridLayout decides the size of the SmoothLabel children).
Also, removed the
background_color=(0.2, 0.6, 1, 1)
from the SmoothLabel creation (that was throwing an exception for me), so the ChatPage definition now looks like:
class ChatPage(Screen):
layout_content = ObjectProperty(None)
def btn_press(self):
if self.ids.msg.text:
self.layout_content.add_widget(SmoothLabel(text=self.ids.msg.text))
self.ids.msg.text = ""
else:
pass
The Label widget has no background_color attribute. And, since I added size_hint to the rule, I removed it from the SmoothLabel() call. Also, the GridLayout does not support pos_hint.
That was enough to get the code working for me.

Resources