Skip to content
Snippets Groups Projects
Commit d2229fbc authored by Hafiz Emin Kosar's avatar Hafiz Emin Kosar
Browse files

- split RWTHFeedback class into different sub-classes for better readability

parent 544ca3b6
No related branches found
No related tags found
1 merge request!10- major changes in feedback
%% Cell type:markdown id: tags:
# Miscellaneous
1. [Feedback](#Feedback)
2. [Transforms](#Transforms)
1. [Fourier Transform](#Fourier-Transform)
2. [Laplace Transform](#Laplace-Transform)
3. [$z$-Transform](#$z$-Transform)
%% Cell type:markdown id: tags:
---
%% Cell type:markdown id: tags:
## Feedback
A feedback form can be created for participants to rate the course.
Use `RWTHFeedback` class in `rwth_nb.misc.rwth_feedback`
The arguments of the class that is created are depending on which environment the feedback form is intended to be used.
%% Cell type:markdown id: tags:
### Offline
In this method, the feedback is sent via mail to a receiver.
%% Cell type:code id: tags:
``` python
import rwth_nb.misc.feedback as rwth_feedback
mail_to = "hafiz.emin.kosar@gmail.com" # send feedback via mail
mail_to = "example@rwth-aachen.de" # send feedback via mail
feedback_name = rwth_feedback.get_notebook_name() # get name of notebook automatically
rwth_feedback.RWTHFeedback(feedback_name, [
{'id': 'likes', 'type': 'free-text-required', 'label': 'Das war gut:'},
{'id': 'dislikes', 'type': 'free-text-required', 'label': 'Das könnte verbessert werden:'},
{'id': 'misc', 'type': 'free-text', 'label': 'Was ich sonst noch sagen möchte:'},
{'id': 'learning', 'type': 'scale', 'label' : 'Ich habe das Gefühl etwas gelernt zu haben.'},
{'id': 'supervision', 'type': 'scale', 'label' : 'Die Betreuung des Versuchs war gut.'},
{'id': 'script', 'type': 'scale', 'label' : 'Die Versuchsunterlagen sind verständlich.'},
], feedback_path='feedback.json', lang='de', mail_to=mail_to);
```
%% Cell type:markdown id: tags:
### _RWTHjupyter_ Cluster
%% Cell type:markdown id: tags:
This method uses _RWTHJupyters_ submission service.
%% Cell type:code id: tags:
``` python
import rwth_nb.misc.feedback as rwth_feedback
feedback_name = rwth_feedback.get_notebook_name() # get name of notebook automatically
rwth_feedback.RWTHFeedback(feedback_name, [
{'id': 'likes', 'type': 'free-text-required', 'label': 'Das war gut:'},
{'id': 'dislikes', 'type': 'free-text-required', 'label': 'Das könnte verbessert werden:'},
{'id': 'misc', 'type': 'free-text', 'label': 'Was ich sonst noch sagen möchte:'},
{'id': 'learning', 'type': 'scale', 'label' : 'Ich habe das Gefühl etwas gelernt zu haben.'},
{'id': 'supervision', 'type': 'scale', 'label' : 'Die Betreuung des Versuchs war gut.'},
{'id': 'script', 'type': 'scale', 'label' : 'Die Versuchsunterlagen sind verständlich.'},
], realm="feedback_gdet3");
```
%% Cell type:markdown id: tags:
---
%% Cell type:markdown id: tags:
## Transforms
Following transforms are defined in `rwth_nb.misc.transforms`:
- [Fourier Transform](#Fourier-Transform)
- [Laplace Transform](#Laplace-Transform)
- [$z$-Transform](#$z$-Transform)
*Note that plotting basics are described in [RWTH Plots](RWTH%20Plots.ipynb).*
%% Cell type:markdown id: tags:
### Fourier Transform
```dft(s, fs, NFFT)```
%% Cell type:code id: tags:
``` python
import matplotlib.pyplot as plt
import numpy as np
import rwth_nb.plots.mpl_decorations as rwth_plots
import rwth_nb.misc.transforms as rwth_transforms
# Time Domain
fs = 44100 # very high sampling rate assumed, to simulate quasi-continuous time and frequency axis
t = np.linspace(-2.5, 2.5, 5*fs)
s = np.sin(2*np.pi*500*t)
# Fourier Transform
S,f = rwth_transforms.dft(s, fs)
# plots
fig,axs = plt.subplots(2,1, **rwth_plots.landscape);
ax = axs[0]; ax.plot(t*1000, s);
ax.set_xlabel(r'$\rightarrow t$ [ms]'); ax.set_ylabel(r'$\uparrow s(t)$')
ax.set_xlim([-11, 11]); ax.set_ylim([-1.1, 1.19]); rwth_plots.axis(ax);
ax = axs[1]; ax.plot(f, np.abs(S));
ax.set_xlabel(r'$\rightarrow f$ [Hz]'); ax.set_ylabel(r'$\uparrow |S(f)|$')
ax.set_xlim([-1100, 1100]); ax.set_ylim([0, 0.65]); rwth_plots.axis(ax);
```
%% Cell type:markdown id: tags:
Inverse Fourier transform
```idft(S, Ntime, NFF)```
%% Cell type:code id: tags:
``` python
s2 = rwth_transforms.idft(S, len(s));
fig,ax = plt.subplots(**rwth_plots.landscape);
ax.plot(t*1000, np.real(s2));
ax.set_xlabel(r'$\rightarrow t$ [ms]'); ax.set_ylabel(r'$\uparrow \mathcal{F}^{-1}\{S(f)\}$')
ax.set_xlim([-11, 11]); ax.set_ylim([-1.1, 1.19]); rwth_plots.axis(ax);
```
%% Cell type:markdown id: tags:
### Laplace Transform
Pole-zero plot is explained in [RWTH Plots](RWTH%20Plots.ipynb).
Inverse Laplace Transform
```ilaplace_ht(t, H0, pp, pz, ord_p, ord_z, roc)```
```ilaplace_Hf(f, H0, pp, pz, ord_p, ord_z, dB)```
%% Cell type:code id: tags:
``` python
fig,axs = plt.subplots(1, 2, figsize=(10, 4))
t = np.linspace(-6, 6, 1024)
f = np.linspace(-6, 6, 1024)
pp = np.array([-2]); pz = np.array([]) # Poles and Zeros
ord_p = np.array([1]); ord_z = np.array([]) # Poles' and Zeros' orders
roc = np.array([-2, np.inf]) # region of convergence
H0 = 1
# Time Domain
s1, t1d , s1d = rwth_transforms.ilaplace_ht(t, H0, pp, pz, ord_p, ord_z, roc)
ax = axs[0]
ax.set_xlabel(r'$\rightarrow t$'); ax.set_ylabel(r'$\uparrow s_1(t)$')
rwth_plots.grid(ax); rwth_plots.axis(ax)
ax.set_xlim([-5.5,5.5]); axs[0].set_ylim([-0.1,1.05]);
ax.plot(t, np.real(s1))
rwth_plots.plot_dirac(axs[0], t1d, s1d);
# Frequency Domain
S1f = rwth_transforms.ilaplace_Hf(f, H0, pp, pz, ord_p, ord_z, dB=False)
ax = axs[1]
ax.set_xlabel(r'$\rightarrow f$'); ax.set_ylabel(r'$\uparrow S_1(f)$')
rwth_plots.grid(ax); rwth_plots.axis(ax)
ax.set_xlim([-5.5,5.5]); ax.set_ylim([-0.1,0.55]);
ax.plot(f, S1f);
```
%% Cell type:markdown id: tags:
### $z$ Transform
Pole-zero plot is explained in [RWTH Plots](RWTH%20Plots.ipynb).
Inverse $z$ Transform
```iz_hn(n, H0, pp, pz, ord_p, ord_z, roc)```
```iz_Hf(f, H0, pp, pz, ord_p, ord_z, dB)```
%% Cell type:code id: tags:
``` python
fig,axs = plt.subplots(1, 2, figsize=(10, 4))
n = np.linspace(-6, 6, 13)
f = np.linspace(-6, 6, 1024)
zp = np.array([.5, 2]); zz = np.array([0]) # Poles and Zeros
ord_p = np.array([1, 1]); ord_z = np.array([1]) # Poles' and Zeros' orders
roc = np.array([.5, 2]) # region of convergence
H0 = -3/2
# Time Domain
s1= rwth_transforms.iz_hn(n, H0, zp, zz, ord_p, ord_z, roc)
ax = axs[0]
ax.set_xlabel(r'$\rightarrow n$'); ax.set_ylabel(r'$\uparrow s_1(n)$')
rwth_plots.grid(ax); rwth_plots.axis(ax)
ax.set_xlim([-5.5,5.5]); axs[0].set_ylim([-0.1,1.05]);
rwth_plots.stem(axs[0], n, s1);
# Frequency Domain
S1f = rwth_transforms.iz_Hf(f, H0, zp, zz, ord_p, ord_z, dB=False)
ax = axs[1]
ax.set_xlabel(r'$\rightarrow f$'); ax.set_ylabel(r'$\uparrow S_1(f)$')
rwth_plots.grid(ax); rwth_plots.axis(ax)
ax.set_xlim([-5.5,5.5]); ax.set_ylim([0.3, 3.1]);
ax.plot(f, S1f);
```
%% Cell type:markdown id: tags:
---
%% Cell type:markdown id: tags:
This code is licensed under the [MIT license](https://opensource.org/licenses/MIT).
......
......@@ -7,9 +7,32 @@ rwth_colors = rwthcolors.rwth_colors
import datetime
import json
import os
import hashlib # for anonymizing the username
import platform # for determining operating system
import subprocess # for hiding files on Windows
import hashlib
import platform
import subprocess
# Internationalization
feedback_scale_options_de = ['Stimme voll zu', 'Ich stimme zu', 'Keine Meinung', 'Ich stimme nicht zu',
'Ich stimme gar nicht zu']
feedback_scale_options_en = ['Strongly agree', 'Agree', 'Neutral', 'Disagree', 'Strongly disagree']
feedback_text_de = {"your-feedback": "Feedback ...",
"send": "Abschicken",
"confirm_send": "Abschicken bestätigen.",
"sent": "Das Feedback wurde abgeschickt. Vielen Dank!",
"required": "Pflichtfeld",
"empty": "Bitte ein Feedback eingeben, das abgesendet werden kann.",
"mailfailure": "Die Mail mit dem Feedback konnte nicht versendet werden. Das Feedback wurde lokal abgespeichert."}
feedback_text_en = {"your-feedback": "Your Feedback ...",
"send": "Submit",
"confirm_send": "Confirm submission.",
"sent": "Feedback was submitted. Thank You!",
"required": "Required field",
"empty": "Please fill required fields before submitting.",
"mailfailure": "The mail containing your feedback could not be sent. Your feedback was saved locally."
}
def get_notebook_name():
......@@ -40,54 +63,21 @@ def get_notebook_name():
return 'tmp'
class RWTHFeedback:
"""
RWTH Feedback submission class
Use as described in RWTH\ Misc.ipynb
"""
class RWTHFeedbackBase:
is_submit_confirmed = is_submitted = False
# Internationalization
feedback_scale_options_de = ['Stimme voll zu', 'Ich stimme zu', 'Keine Meinung', 'Ich stimme nicht zu',
'Ich stimme gar nicht zu'] # Likert-scale
feedback_scale_options_en = ['Strongly agree', 'Agree', 'Neutral', 'Disagree', 'Strongly disagree']
feedback_text_de = {"your-feedback": "Feedback ...",
"send": "Abschicken",
"confirm_send": "Abschicken bestätigen.",
"sent": "Das Feedback wurde abgeschickt. Vielen Dank!",
"required": "Pflichtfeld",
"empty": "Bitte ein Feedback eingeben, das abgesendet werden kann.",
"mailfailure": "Die Mail mit dem Feedback konnte nicht versendet werden. Das Feedback wurde lokal abgespeichert."}
feedback_text_en = {"your-feedback": "Your Feedback ...",
"send": "Submit",
"confirm_send": "Confirm submission.",
"sent": "Feedback was submitted. Thank You!",
"required": "Required field",
"empty": "Please fill required fields before submitting.",
"mailfailure": "The mail containing your feedback could not be sent. Your feedback was saved locally."
}
def __init__(self, feedback_name, questions, realm=None, feedback_path='feedback.json', lang='en', mail_to=None,
mail_from='feedback@jupyter.rwth-aachen.de', mail_subject=None,
mail_smtp_host='smarthost.rwth-aachen.de'):
def __init__(self, feedback_name, questions, lang):
self.feedback_name = feedback_name
self.questions = questions
self.realm = realm
self.feedback_path = '.' + feedback_path if not platform.system() == 'Windows' and \
not feedback_path.startswith('.') else feedback_path
self.lang = lang
self.mail_to = mail_to
self.mail_from = mail_from
self.mail_subject = mail_subject
self.mail_smtp_host = mail_smtp_host
user_name = os.environ.get('JUPYTERHUB_USER',
os.environ.get('LOGNAME', os.environ.get('USER', os.environ.get('USERNAME', 'TMP'))))
self.hashed_username = hashlib.sha256(str.encode(user_name)).hexdigest()
# Select language
self.feedback_scale_options = getattr(self, 'feedback_scale_options_' + self.lang)
self.feedback_text = getattr(self, 'feedback_text_' + self.lang)
self.feedback_scale_options = globals()['feedback_scale_options_' + self.lang]
self.feedback_text = globals()['feedback_text_' + self.lang]
# Default arguments for toggle_button and textarea
self.toggle_args = {"options": self.feedback_scale_options,
......@@ -110,17 +100,6 @@ class RWTHFeedback:
# list containing all widgets that require non empty entries
self.widgets_required_entries = []
# submission configurations
profile = os.environ.get('JUPYTERHUB_PROFILE')
if profile is not None:
self.submission_type = 'jupyter'
self.realm = f'feedback_{profile}' if self.realm is None else self.realm
self.sub = Submission(self.realm)
else:
self.submission_type = 'json'
self.entry = {}
self.entries = []
......@@ -173,50 +152,6 @@ class RWTHFeedback:
self.update_ui_state()
def load_entries(self):
"""
Load entries
Entries are either loaded from juypter service or from json file
according to self.submission_type
"""
if self.submission_type == 'jupyter':
# load from jupyter hub submission service
self.entries = [submission['data'] for submission in self.sub.get() if type(submission['data']) is dict]
else:
# load from json file, create if non existent
if not os.path.isfile(self.feedback_path):
# file does not exist, create
with open(self.feedback_path, mode='w', encoding='utf-8') as f:
json.dump([], f)
# hide file
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '+H', self.feedback_path])
# load json file into self.entries
with open(self.feedback_path, mode='r', encoding='utf-8') as f:
self.entries = json.load(f)
def save_entries(self):
"""
Save entries into json file.
Not used if user is in jupyter cluster.
"""
if self.submission_type == 'json':
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '-H', self.feedback_path])
# write
with open(self.feedback_path, mode='w', encoding='utf-8') as f:
json.dump(self.entries, f)
# hide again
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '+H', self.feedback_path])
def on_btn_submit_clicked(self, _):
"""
Submit button onClick method
......@@ -229,11 +164,7 @@ class RWTHFeedback:
# set up json entries
self.entry['name'] = self.feedback_name
self.entry['date'] = "{}".format(datetime.datetime.now())
if self.submission_type == 'jupyter':
user_name = os.environ.get('JUPYTERHUB_USER')
else:
user_name = os.environ.get('LOGNAME', os.environ.get('USER', os.environ.get('USERNAME', 'TMP')))
self.entry['userhash'] = hashlib.sha256(str.encode(user_name)).hexdigest()
self.entry['userhash'] = self.hashed_username
self.entry['answer'] = {key: w.value for key, w in self.widgets_container.items()}
self.entry['status'] = 'saved_locally' if self.submission_type == 'json' else 'submitted'
......@@ -258,97 +189,11 @@ class RWTHFeedback:
# update ui
self.update_ui_state()
def send_mail(self):
"""
Sends JSON file as attachment of a mail to predefined recipient
Sets self.is_submitted to True if mail was sent successfully. False otherwise.
"""
try:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = self.mail_from
msg["Subject"] = self.mail_subject if self.mail_subject is not None else self.feedback_name
msg["To"] = self.mail_to
with open(self.feedback_path, 'r') as f:
msg.add_attachment(f.read(), filename=self.feedback_path)
s = smtplib.SMTP(self.mail_smtp_host)
s.send_message(msg)
self.is_submitted = True
except ConnectionRefusedError:
# Not connected to the RWTH network
self.output.clear_output()
with self.output:
print(self.feedback_text['mailfailure'])
self.is_submitted = False
def submit(self):
"""
Submit feedback
Sends json file via mail if user is not in jupyter cluster
else the feedback is submitted to jupyters submission service
"""
if self.submission_type == 'json':
# dump entries into json file
# append only if not entry does not already exist in json file
self.load_entries()
if self.check_submission_status() == 'idle':
self.entries.append(self.entry)
self.save_entries()
# try to send json file as attachment of mail
if self.mail_to is not None:
self.send_mail()
# open and set statuses to submitted if mail is successfully sent
if self.is_submitted:
self.load_entries()
for entry in self.entries:
entry['status'] = 'submitted'
self.save_entries()
else:
# submit to jupyter submission service
self.sub.submit(self.entry)
self.is_submitted = True
def check_submission_status(self):
"""
Check entry submission status
Returns
-------
status: {'idle', 'saved_locally', 'submitted'}, str
submission status
'idle', if feedback does not exist in self.entires
'saved_locally', if feedback exists but was not sent
'submitted', if feedback was already submitted
"""
if self.submission_type == 'json':
try:
for entry in self.entries:
if self.feedback_name == entry['name']:
return entry['status']
return 'idle'
except FileNotFoundError:
return 'idle'
else:
for entry in self.entries:
if self.feedback_name == entry['name']:
return 'submitted'
return 'idle'
def update_ui_state(self):
"""
......@@ -412,3 +257,167 @@ class RWTHFeedback:
# disable button and change description
self.btn_submit.disabled = True
self.btn_submit.description = self.feedback_text['sent']
class RWTHFeedbackJupyter(RWTHFeedbackBase):
def __init__(self, feedback_name, questions, lang='en', realm='feedback'):
self.realm = realm
profile = os.environ.get('JUPYTERHUB_PROFILE')
self.realm = f'feedback_{profile}' if self.realm is None else self.realm
self.sub = Submission(self.realm)
super().__init__(feedback_name, questions, lang)
self.submission_type = 'jupyter'
def load_entries(self):
self.entries = [submission['data'] for submission in self.sub.get() if type(submission['data']) is dict]
def submit(self):
# submit to jupyter submission service
self.sub.submit(self.entry)
self.is_submitted = True
class RWTHFeedbackMail(RWTHFeedbackBase):
def __init__(self, feedback_name, questions, lang='en', feedback_path='feedback.json', mail_to=None,
mail_from='feedback@jupyter.rwth-aachen.de', mail_subject=None,
mail_smtp_host='smarthost.rwth-aachen.de'):
self.feedback_path = '.' + feedback_path if not platform.system() == 'Windows' and \
not feedback_path.startswith('.') else feedback_path
self.mail_to = mail_to
self.mail_from = mail_from
self.mail_subject = mail_subject
self.mail_smtp_host = mail_smtp_host
super().__init__(feedback_name, questions, lang)
self.submission_type = 'json'
def save_entries(self):
"""
Save entries into json file.
Not used if user is in jupyter cluster.
"""
if self.submission_type == 'json':
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '-H', self.feedback_path])
# write
with open(self.feedback_path, mode='w', encoding='utf-8') as f:
json.dump(self.entries, f)
# hide again
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '+H', self.feedback_path])
def load_entries(self):
if not os.path.isfile(self.feedback_path):
# file does not exist, create
with open(self.feedback_path, mode='w', encoding='utf-8') as f:
json.dump([], f)
# hide file
if platform.system() == 'Windows':
subprocess.check_call(['attrib', '+H', self.feedback_path])
# load json file into self.entries
with open(self.feedback_path, mode='r', encoding='utf-8') as f:
self.entries = json.load(f)
def send_mail(self):
"""
Sends JSON file as attachment of a mail to predefined recipient
Sets self.is_submitted to True if mail was sent successfully. False otherwise.
"""
try:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = self.mail_from
msg["Subject"] = self.mail_subject if self.mail_subject is not None else self.feedback_name
msg["To"] = self.mail_to
with open(self.feedback_path, 'r') as f:
msg.add_attachment(f.read(), filename=self.feedback_path)
s = smtplib.SMTP(self.mail_smtp_host)
s.send_message(msg)
self.is_submitted = True
except ConnectionRefusedError:
# Not connected to the RWTH network
self.output.clear_output()
with self.output:
print(self.feedback_text['mailfailure'])
self.is_submitted = False
def submit(self):
# dump entries into json file
# append only if not entry does not already exist in json file
self.load_entries()
if self.check_submission_status() == 'idle':
self.entries.append(self.entry)
self.save_entries()
# try to send json file as attachment of mail
if self.mail_to is not None:
self.send_mail()
# open and set statuses to submitted if mail is successfully sent
if self.is_submitted:
self.load_entries()
for entry in self.entries:
entry['status'] = 'submitted'
self.save_entries()
class RWTHFeedback:
""" RWTHFeedback calling specific classes dependent on use cases
Parameters
----------
feedback_name: str
the feedbacks name
questions: dict
feedback options to be filled out
lang: str, optional
feedback language, scales are shown in that language
-- Only for RWTHJupyter usage ---
realm: str, optional
jupyter submission realm in which the feedback should be stored, is set automatically if None
-- Only for Offline usage --
feedback_path: str, optional
path in which a feedback json file should be stored
mail_to: str, optional
mail adress to which the feedback should be sent when submitted
mail_from: str, optional
mail adress from which the feedback should be sent when submitted
mail_subject: str, optional
subject of the mail
mail_smtp_host: str, optional
smtp host
"""
def __init__(self, feedback_name, questions, lang='de', realm=None, feedback_path='feedback.json', mail_to=None,
mail_from='feedback@jupyter.rwth-aachen.de', mail_subject=None,
mail_smtp_host='smarthost.rwth-aachen.de'):
profile = os.environ.get('JUPYTERHUB_PROFILE')
if profile is not None:
RWTHFeedbackJupyter(feedback_name, questions, lang, realm)
else:
RWTHFeedbackMail(feedback_name, questions, lang, feedback_path, mail_to, mail_from, mail_subject
, mail_smtp_host)
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment