Skip to content

Commit

Permalink
session persistence and "remember me" option
Browse files Browse the repository at this point in the history
also updates the login request to return a long-term session
  • Loading branch information
Noiredd committed May 23, 2020
1 parent 09e12b5 commit 7e33b17
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 18 deletions.
41 changes: 39 additions & 2 deletions filmatyk/filmweb.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from datetime import date
import binascii
import html
import json
import pickle

from bs4 import BeautifulSoup as BS
import html
import requests_html

import containers


ConnectionError = requests_html.requests.ConnectionError


class Constants():
"""URLs and HTML component names for data acquisition.
Expand Down Expand Up @@ -51,6 +54,8 @@ def login(username, password):
auth_package = {
'j_username': username,
'j_password': password,
'_login_redirect_url': '',
'_prm': True,
}
# Catch connection errors
try:
Expand All @@ -77,11 +82,19 @@ def enforceSession(fun):
https://stackoverflow.com/q/21382801/6919631
https://stackoverflow.com/q/11058686/6919631
The bottom line is that it should NEVER be called directly.
Also checks if the session cookies were changed in the process of making
a request.
"""
def wrapper(*args, **kwargs):
self = args[0]
if self.checkSession():
return fun(*args, **kwargs)
old_cookies = set(self.session.cookies.values())
result = fun(*args, **kwargs)
new_cookies = set(self.session.cookies.values())
if old_cookies != new_cookies:
self.isDirty = True
return result
else:
return None
return wrapper
Expand All @@ -91,6 +104,7 @@ def __init__(self, login_handler, username:str=''):
self.constants = Constants(username)
self.login_handler = login_handler
self.session = None
self.isDirty = False
self.parsingRules = {}
for container in containers.classByString.keys():
self.__cacheParsingRules(container)
Expand Down Expand Up @@ -147,11 +161,16 @@ def checkSession(self):
(cause we'll nearly always have a session, except it might sometimes get stale
resulting in an acquisition failure)
"""
session_requested = False
if not self.session:
self.requestSession()
session_requested = True
# Check again - in case the user cancelled a login
if not self.session:
return False
# If a new session was requested in the process - set the dirty flag
if session_requested:
self.isDirty = True
# At this point everything is definitely safe
return True

Expand All @@ -172,6 +191,24 @@ def requestSession(self):
return None
self.session = session

def storeSession(self):
"""Stores the sessions cookies to a base64-encoded pickle string."""
if self.session:
cookies_bin = pickle.dumps(self.session.cookies)
cookies_str = binascii.b2a_base64(cookies_bin).decode('utf-8').strip()
return cookies_str
else:
return 'null'

def restoreSession(self, pickle_str:str):
"""Restores the session cookies from a base64-encoded pickle string."""
if pickle_str == 'null':
return
self.session = requests_html.HTMLSession()
cookies_bin = binascii.a2b_base64(pickle_str.encode('utf-8'))
cookies_obj = pickle.loads(cookies_bin)
self.session.cookies = cookies_obj

@enforceSession
def getNumOf(self, itemtype:str):
"""Return the number of items of a given type that the user has rated.
Expand Down
27 changes: 22 additions & 5 deletions filmatyk/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ def __construct(self):
self.passwordEntry = tk.Entry(master=cw, width=20, show='*')
self.passwordEntry.grid(row=2, column=1, sticky=tk.W)
self.passwordEntry.bind('<Key>', self._setStateGood)
self.rememberBox = tk.Checkbutton(self.window, text='pamiętaj mnie', variable=Options.var('rememberLogin'))
self.rememberBox.grid(row=3, column=1, columnspan=2, sticky=tk.W)
self.infoLabel = tk.Label(master=cw, text='')
self.infoLabel.grid(row=3, column=0, columnspan=2)
tk.Button(master=cw, text='Zaloguj', command=self._loginClick).grid(row=4, column=1, sticky=tk.W)
tk.Button(master=cw, text='Anuluj', command=self._cancelClick).grid(row=4, column=0, sticky=tk.E)
self.infoLabel.grid(row=4, column=0, columnspan=2)
tk.Button(master=cw, text='Zaloguj', command=self._loginClick).grid(row=5, column=1, sticky=tk.W)
tk.Button(master=cw, text='Anuluj', command=self._cancelClick).grid(row=5, column=0, sticky=tk.E)
self.window.withdraw()

def centerWindow(self):
Expand Down Expand Up @@ -171,6 +173,7 @@ def __init__(self, debugMode=False, isOnLinux=False):
self.presenters = []
# instantiate Presenters and Databases
self.api = FilmwebAPI(self.loginHandler.requestLogin, userdata.username)
self.api.restoreSession(userdata.session_pkl)
movieDatabase = Database.restoreFromString('Movie', userdata.movies_data, self.api, self._setProgress)
self.databases.append(movieDatabase)
moviePresenter = Presenter(self, self.api, movieDatabase, userdata.movies_conf)
Expand Down Expand Up @@ -240,7 +243,8 @@ def centerWindow(self):
y = hs/2 - h/2
self.root.geometry('+{:.0f}+{:.0f}'.format(x, y))

#USER DATA MANAGEMENT
# USER DATA MANAGEMENT

def getFilename(self):
if self.debugMode:
return self.filename
Expand All @@ -249,14 +253,24 @@ def getFilename(self):
return os.path.join(userdir, subpath)

def saveUserData(self):
"""Save the user data, if any of it has changed during the run time."""
# if for any reason the first update hasn't commenced - don't save anything
if self.api.username is None:
return
# if the session is set to be stored - serialize it
# if it is also dirty - set the flag for a check later
session_pkl = 'null'
session_isDirty = False
if Options.get('rememberLogin'):
session_pkl = self.api.storeSession()
if self.api.isDirty:
session_isDirty = True
# if there is no need to save anything - stop right there too
if not (
any([db.isDirty for db in self.databases]) or
any([ps.isDirty for ps in self.presenters]) or
Options.isDirty
Options.isDirty or
session_isDirty
):
return
# construct the UserData object
Expand All @@ -269,6 +283,7 @@ def saveUserData(self):
games_conf=self.presenters[2].storeToString(),
games_data=self.databases[2].storeToString(),
options_json=Options.storeToString(),
session_pkl=session_pkl,
)
# request the manager to save it
self.dataManager.save(serialized_data)
Expand All @@ -277,6 +292,8 @@ def saveUserData(self):
db.isDirty = False
for ps in self.presenters:
ps.isDirty = False
Options.isDirty = False
self.api.isDirty = False

#CALLBACKS
def _setProgress(self, value:int, abort:bool=False):
Expand Down
1 change: 1 addition & 0 deletions filmatyk/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class _Options():
Defining a new option is done simply by adding it to the prototypes list.
"""
option_prototypes = [
('rememberLogin', tk.BooleanVar, True),
]

def __init__(self):
Expand Down
28 changes: 17 additions & 11 deletions filmatyk/userdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,25 @@ class UserData(object):
def __init__(
self,
username='',
options_json='{}',
session_pkl='null',
movies_conf='',
movies_data='',
series_conf='',
series_data='',
games_conf='',
games_data='',
options_json='{}',
is_empty=True
):
self.username = username
self.options_json = options_json
self.session_pkl = session_pkl
self.movies_conf = movies_conf
self.movies_data = movies_data
self.series_conf = series_conf
self.series_data = series_data
self.games_conf = games_conf
self.games_data = games_data
self.options_json = options_json
self.is_empty = is_empty


Expand Down Expand Up @@ -104,6 +106,8 @@ def save(self, userData):
user_file.write(userData.username + '\n')
user_file.write('#OPTIONS\n')
user_file.write(userData.options_json + '\n')
user_file.write('#SESSION\n')
user_file.write(userData.session_pkl + '\n')
user_file.write('#MOVIES\n')
user_file.write(userData.movies_conf + '\n')
user_file.write(userData.movies_data + '\n')
Expand Down Expand Up @@ -153,12 +157,13 @@ def readFile(self):
This always has to be done first (independent of the actual content), as
the version string must be extracted before doing anything further.
Lines starting with '#' are always ignored as comments.
Empty lines are always ignored.
"""
with open(self.path, 'r') as user_file:
user_data = [
line.strip('\n')
for line in user_file.readlines()
if not line.startswith('#')
if not line.startswith('#') and len(line) > 1
]
return user_data

Expand Down Expand Up @@ -205,8 +210,8 @@ def decorator(loader):
class Loaders(object):
"""Just a holder for different data loading functions.
It's friends with DataManager class, that is: updates its "loaders" ODict
with any loader defined here.
It's friends with DataManager class, that is: updates its list of loaders
with all loaders defined here.
"""
@DataManager.registerLoaderSince('1.0.0-beta.1')
def loader100b(user_data):
Expand All @@ -225,10 +230,11 @@ def loader100b4(user_data):
return UserData(
username=user_data[1],
options_json=user_data[2],
movies_conf=user_data[3],
movies_data=user_data[4],
series_conf=user_data[5],
series_data=user_data[6],
games_conf=user_data[7],
games_data=user_data[8],
session_pkl=user_data[3],
movies_conf=user_data[4],
movies_data=user_data[5],
series_conf=user_data[6],
series_data=user_data[7],
games_conf=user_data[8],
games_data=user_data[9],
)

0 comments on commit 7e33b17

Please sign in to comment.