Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep watched state 99a169a #373

Open
wants to merge 13 commits into
base: release
Choose a base branch
from
21 changes: 21 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/python-3/.devcontainer/base.Dockerfile

# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.10-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
48 changes: 48 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/python-3
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"args": {
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"VARIANT": "3.8",
// Options
"NODE_VERSION": "lts/*"
}
},

// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",

// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ and the episodes are ordered by air date.

Data is also drawn from Wikipedia when necessary.

### Development

The project has been tested in Python 3.9 and will (currently) not run on Python 3.10+

During development, the environment variable `REDIS_URL` can be set to "None"
removing the requirement of having a Redis instance up and running. This
effectively disables caching.

Setting up the environment:

```
poetry install
```

Running a development instance:

There are two options:

```
QUART_ENV=development QUART_DEBUG=true QUART_APP=./ordering/ poetry run quart run
```

and

```
poetry shell
QUART_ENV=development QUART_DEBUG=true QUART_APP=./ordering/ quart run
```


### Currently Supported Series:

* Arrow
Expand Down
4 changes: 0 additions & 4 deletions ordering/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from quart import Quart
from quart_compress import Compress
from quart_minify import Minify
# from tortoise.contrib.quart import register_tortoise

from .settings import DATABASE_URL, DEBUG
Expand All @@ -10,9 +9,6 @@

app.config.from_pyfile('settings.py')

# Minify HTML and any inline JS or CSS
Minify(app, js=True)

# gzip responses
Compress(app)

Expand Down
13 changes: 11 additions & 2 deletions ordering/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@

from ordering.settings import REDIS_URL


cache = Redis.from_url(REDIS_URL)
logger = getLogger(__name__)

try:
cache = Redis.from_url(REDIS_URL)
except ValueError:
logger.warning("Unable to open cache. Caching will be disabled.", exc_info=True)
cache = None



def serialized_response(response):
def handle_bytes(value):
Expand All @@ -23,6 +28,10 @@ def handle_bytes(value):

def safe_cache_content(timeout=None, backup=False, hash_args=False):
def decorator(func):

if cache is None:
return func

@wraps(func)
def wrapper(*args, **kwargs):
inputs = f'{args}-{kwargs}'
Expand Down
11 changes: 11 additions & 0 deletions ordering/static/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,14 @@ nav.navbar {
table.table {
margin: 0;
}


.hidden {
display: none;
}

.faint {
color: #bbb;
text-decoration: line-through;
background: #ccc;
}
155 changes: 152 additions & 3 deletions ordering/static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,76 @@
/**
* An abstraction around persistent storage for user preferences.
*
* The class provides static variables with the available keys and get/put
* functions to access the storage.
*/
class UserPreferences {
static WATCHED_EPISODES = "watchedEpisodes";
static HIDE_WATCHED = "hideWatched";

constructor (){}

/**
* Convert the "data-series" and "data-episode-id" into a sinlge key for
* LocalStorage.
*
* @param {Element} element the element which contains the data attributes.
* @returns A string identifying the episode
*/
static getEpisodeKey (element) {
let series = element.attributes["data-series"].value;
let episode = element.attributes["data-episode-id"].value;
let key = `${series}-${episode}`;
return key;
}

/**
* Retrieve a value from persistent user-storage.
*
* @param {String} key The name/key of the config-value to retrieve
* @param {*} fallback The default value if the key was not yet set by the
* user.
* @returns The value as defined by the user (or the default)
*/
get(key, fallback) {
// We want to allow "null" as fallback as well, so we have to use a
// sentinel-value to detect any "unset" config value. To keep
// JSON-conversion to a minimum, we keep two values
let nullSentinelJson = '"--config--null--sentinel--"'
let nullSentinel = "--config--null--sentinel--"
let value = JSON.parse(
localStorage.getItem(key) || nullSentinelJson
);
if (value === nullSentinel) {
return fallback;
}
return value
}

/**
* Sotre a new value into persistent user-config
*
* @param {String} key The name/key of the config-value to store
* @param {*} value The value to store
*/
put(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}

}

(function($, Cookies) {
'use strict';

var openWiki = function() {
var url = $(this).data('href');
window.open(url, '_blank');
/**
* CSS class names used to change visual display of "watched" episodes
*/
let watchedStateClasses = {
HIDDEN: 'hidden',
FAINT: 'faint',
};

let prefs = new UserPreferences();
var disableColours = function() {
$('.episode, thead').addClass('no-color');
$('#episode-list').addClass('table-striped table-hover');
Expand Down Expand Up @@ -61,7 +126,89 @@
}
};

/**
* Updates localStorage with "watched" information of an episode
*
*
* @param {ChangeEvent} evt The event
*/
var updateWatched = function (evt) {
let newValue = evt.target.checked;
let key = UserPreferences.getEpisodeKey(evt.target);
let watchedEpisodes = prefs.get(UserPreferences.WATCHED_EPISODES, []);
let index = watchedEpisodes.indexOf(key)
if (newValue === true && index === -1) {
watchedEpisodes.push(key);
} else if (newValue === false && index !== -1) {
watchedEpisodes.splice(index, 1);
}
prefs.put(UserPreferences.WATCHED_EPISODES, watchedEpisodes);
setWatchedDisplayState(prefs.get(UserPreferences.HIDE_WATCHED, true));
};

/**
* Hide/Show episodes, according to the "watched" state
*
* @param {Boolean} doHide Whether to hide the rows or not
*/
var setWatchedDisplayState = function (doHide) {
let watchedEpisodes = prefs.get(UserPreferences.WATCHED_EPISODES, []);
let watchedClass = (
doHide ? watchedStateClasses.HIDDEN : watchedStateClasses.FAINT
);
$('.episode').map(function() {
let key = UserPreferences.getEpisodeKey(this);
for (const [_, value] of Object.entries(watchedStateClasses)) {
$(this).removeClass(value)
}
if (watchedEpisodes.includes(key)) {
$(this).addClass(watchedClass)
} else {
$(this).removeClass(watchedClass)
}
});
};

/**
* Load and initialise checkbox-states for "watched" episodes.
*/
var initWatchedStates = function () {
let watchedEpisodes = prefs.get(UserPreferences.WATCHED_EPISODES, []);
$('.watchedToggle').map(function() {
let key = UserPreferences.getEpisodeKey(this);
if (watchedEpisodes.includes(key)) {
this.checked = true;
} else {
this.checked = false;
}
});
}

var registerListeners = function() {
$('.watchedToggle').change(updateWatched);

$('#show-watched').click(function() {
let linkText;
let newState;
let currentState = prefs.get(UserPreferences.HIDE_WATCHED, true);
if (currentState) {
linkText = "HIDE WATCHED";
newState = false;
} else {
linkText = "SHOW WATCHED";
newState = true;
}
prefs.put(UserPreferences.HIDE_WATCHED, newState);
setWatchedDisplayState(prefs.get(UserPreferences.HIDE_WATCHED, true));

// Accessing "firstChild.innerHTML" is brittle. But I deemed this an
// acceptable trade-off to keep code-churn minimal (unless I missed
// something). Also, the text-value is decoupled from the HTML
// template for the same reason. This might lead to subtle
// display-bugs (only the text-value) if the template is updated.
this.firstChild.innerHTML = linkText;
});

$('#no-color').click(function() {
if (Cookies.get('colour') === '1') {
disableColours();
Expand Down Expand Up @@ -96,6 +243,8 @@
width: '100%',
});
registerListeners();
initWatchedStates();
setWatchedDisplayState(prefs.get(UserPreferences.HIDE_WATCHED, true));

var colourSetting = Cookies.get('colour');
if (colourSetting === undefined) {
Expand Down
13 changes: 12 additions & 1 deletion ordering/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
</a>
| <a href="#" id="no-color"><small class="text">DISABLE COLOR</small></a>
| <a href="#" id="dark-mode"><small class="text">TOGGLE DARK MODE</small></a>
| <a href="#" id="show-watched"><small class="text">SHOW/HIDE WATCHED</small></a>
</div>
</div>
</div>
Expand All @@ -160,6 +161,7 @@
<table class="table table-bordered col-sm-12" id="episode-list">
<thead>
<tr>
<th>Watched</th>
<th>#</th>
<th>Series</th>
<th>Episode</th>
Expand All @@ -170,7 +172,15 @@
</thead>
<tbody>
{% for row in table_content %}
<tr class="episode {{ series_map[row.series].id }}">
<tr class="episode {{ series_map[row.series].id }}"
aria-label="Episode Information"
data-series="{{ row.series }}"
data-episode-id="{{ row.episode_id }}">
<td><input type="checkbox"
exhuma marked this conversation as resolved.
Show resolved Hide resolved
exhuma marked this conversation as resolved.
Show resolved Hide resolved
name="Toggle watched state"
class="watchedToggle"
data-series="{{ row.series }}"
data-episode-id="{{ row.episode_id }}"></td>
<td>{{ row.row_number }}</td>
<td>{{ row.series }}</td>
<td>{{ row.episode_id }}</td>
Expand All @@ -189,6 +199,7 @@
</tbody>
</table>
</div>

</div>

</div>
Expand Down
Loading