diff --git a/example/Tribe1/210122_DS6.csv b/example/Tribe1/210122_DS6.csv new file mode 100644 index 0000000..0b9789c --- /dev/null +++ b/example/Tribe1/210122_DS6.csv @@ -0,0 +1,5 @@ +Trimestre,Nom,Date,Exercice,Question,Competence,Domaine,Commentaire,Bareme,Est_nivele,Star Tice,Umberto Dingate,Starlin Crangle,Humbert Bourcq,Gabriella Handyside,Stewart Eaves,Erick Going,Ase Praton,Rollins Planks,Dunstan Sarjant,Stacy Guiton,Ange Stanes,Amabelle Elleton,Darn Broomhall,Dyan Chatto,Keane Rennebach,Nari Paulton,Brandy Wase,Jaclyn Firidolfi,Violette Lockney +1,DS6,22/01/2021,Exercice 1,Sait pas,,,,,,,,,,,,,,,,,,,,,,,,, +1,DS6,22/01/2021,Exercice 1,Ha,,,,,,,,,,,,,,,,,,,,,,,,, +1,DS6,22/01/2021,Exercice 1,,,,,,,,,,,,,,,,,,,,,,,,,, +1,DS6,22/01/2021,Exercice 2,grr,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/recopytex/__init__.py b/recopytex/__init__.py index 3c672c9..e69de29 100644 --- a/recopytex/__init__.py +++ b/recopytex/__init__.py @@ -1,5 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -from .csv_extraction import flat_df_students, flat_df_for -from .df_marks_manip import pp_q_scores diff --git a/recopytex/config.py b/recopytex/config.py deleted file mode 100644 index c5eb096..0000000 --- a/recopytex/config.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -NO_ST_COLUMNS = { - "term": "Trimestre", - "assessment": "Nom", - "date": "Date", - "exercise": "Exercice", - "question": "Question", - "competence": "Competence", - "theme": "Domaine", - "comment": "Commentaire", - "score_rate": "Bareme", - "is_leveled": "Est_nivele", -} - -COLUMNS = { - **NO_ST_COLUMNS, - "student": "Eleve", - "score": "Score", - "mark": "Note", - "level": "Niveau", - "normalized": "Normalise", -} - -VALIDSCORE = { - "NOTFILLED": "", # The item is not scored yet - "NOANSWER": ".", # Student gives no answer (this score will impact the fianl mark) - "ABS": "a", # Student has absent (this score won't be impact the final mark) -} diff --git a/recopytex/csv_extraction.py b/recopytex/csv_extraction.py deleted file mode 100644 index 85e4e6f..0000000 --- a/recopytex/csv_extraction.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -""" Extracting data from xlsx files """ - -import pandas as pd -from .config import NO_ST_COLUMNS, COLUMNS, VALIDSCORE - -pd.set_option("Precision", 2) - - -def try_replace(x, old, new): - try: - return str(x).replace(old, new) - except ValueError: - return x - - -def extract_students(df, no_student_columns=NO_ST_COLUMNS.values()): - """Extract the list of students from df - - :param df: the dataframe - :param no_student_columns: columns that are not students - :return: list of students - """ - students = df.columns.difference(no_student_columns) - return students - - -def flat_df_students( - df, no_student_columns=NO_ST_COLUMNS.values(), postprocessing=True -): - """Flat the dataframe by returning a dataframe with on student on each line - - :param df: the dataframe (one row per questions) - :param no_student_columns: columns that are not students - :return: dataframe with one row per questions and students - - Columns of csv files: - - - NO_ST_COLUMNS meta data on questions - - one for each students - - This function flat student's columns to "student" and "score" - """ - students = extract_students(df, no_student_columns) - scores = [] - for st in students: - scores.append( - pd.melt( - df, - id_vars=no_student_columns, - value_vars=st, - var_name=COLUMNS["student"], - value_name=COLUMNS["score"], - ).dropna(subset=[COLUMNS["score"]]) - ) - if postprocessing: - return postprocess(pd.concat(scores)) - return pd.concat(scores) - - -def flat_df_for( - df, student, no_student_columns=NO_ST_COLUMNS.values(), postprocessing=True -): - """Extract the data only for one student - - :param df: the dataframe (one row per questions) - :param no_student_columns: columns that are not students - :return: dataframe with one row per questions and students - - Columns of csv files: - - - NO_ST_COLUMNS meta data on questions - - one for each students - - """ - students = extract_students(df, no_student_columns) - if student not in students: - raise KeyError("This student is not in the table") - st_df = df[list(no_student_columns) + [student]] - st_df = st_df.rename(columns={student: COLUMNS["score"]}).dropna( - subset=[COLUMNS["score"]] - ) - if postprocessing: - return postprocess(st_df) - return st_df - - -def postprocess(df): - """Postprocessing score dataframe - - - Replace na with an empty string - - Replace "NOANSWER" with -1 - - Turn commas number to dot numbers - """ - - df[COLUMNS["question"]].fillna("", inplace=True) - df[COLUMNS["exercise"]].fillna("", inplace=True) - df[COLUMNS["comment"]].fillna("", inplace=True) - df[COLUMNS["competence"]].fillna("", inplace=True) - - df[COLUMNS["score"]] = pd.to_numeric( - df[COLUMNS["score"]] - .replace(VALIDSCORE["NOANSWER"], -1) - .apply(lambda x: try_replace(x, ",", ".")) - ) - df[COLUMNS["score_rate"]] = pd.to_numeric( - df[COLUMNS["score_rate"]].apply(lambda x: try_replace(x, ",", ".")), - errors="coerce", - ) - - return df - - -# ----------------------------- -# Reglages pour 'vim' -# vim:set autoindent expandtab tabstop=4 shiftwidth=4: -# cursor: 16 del diff --git a/recopytex/dashboard/__init__.py b/recopytex/dashboard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recopytex/dashboard/app.py b/recopytex/dashboard/app.py deleted file mode 100644 index 7c2254c..0000000 --- a/recopytex/dashboard/app.py +++ /dev/null @@ -1,5 +0,0 @@ -import dash - -app = dash.Dash(__name__, suppress_callback_exceptions=True) -# app = dash.Dash(__name__) -server = app.server diff --git a/recopytex/dashboard/assets/style.css b/recopytex/dashboard/assets/style.css deleted file mode 100644 index 08d1947..0000000 --- a/recopytex/dashboard/assets/style.css +++ /dev/null @@ -1,66 +0,0 @@ -body { - margin: 0px; - font-family: 'Source Sans Pro','Roboto','Open Sans','Liberation Sans','DejaVu Sans','Verdana','Helvetica','Arial',sans-serif; -} - -header { - margin: 0px 0px 20px 0px; - background-color: #333333; - color: #ffffff; - padding: 20px; -} - -header > h1 { - margin: 0px; -} - -main { - width: 95vw; - margin: auto; -} - -section { - margin-top: 20px; - margin-bottom: 20px; - -} - -/* Exam analysis */ - -#select { - margin-bottom: 20px; -} - -#select > div { - width: 40vw; - margin: auto; -} - -#analysis { - display: flex; - flex-flow: row wrap; -} - -#analysis > * { - display: flex; - flex-flow: column; - width: 45vw; - margin: auto; -} - -/* Create new exam */ - -#new-exam { - display: flex; - flex-flow: row; - justify-content: space-between; -} - -#new-exam label { - width: 20%; - display: flex; - flex-flow: column; - justify-content: space-between; -} - - diff --git a/recopytex/dashboard/create_exam/__init__.py b/recopytex/dashboard/create_exam/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recopytex/dashboard/create_exam/app.py b/recopytex/dashboard/create_exam/app.py deleted file mode 100644 index 44318ab..0000000 --- a/recopytex/dashboard/create_exam/app.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import dash -import dash_html_components as html -import dash_core_components as dcc -import dash_table -import plotly.graph_objects as go -from datetime import date, datetime -import uuid -import pandas as pd -import yaml - -from ...scripts.getconfig import config -from ...config import NO_ST_COLUMNS -from ..app import app -from ...scripts.exam import Exam - -QUESTION_COLUMNS = [ - {"id": "id", "name": "Question"}, - { - "id": "competence", - "name": "Competence", - "presentation": "dropdown", - }, - {"id": "theme", "name": "Domaine"}, - {"id": "comment", "name": "Commentaire"}, - {"id": "score_rate", "name": "Bareme"}, - {"id": "is_leveled", "name": "Est_nivele"}, -] - - -def get_current_year_limit(): - today = date.today() - if today.month > 8: - return { - "min_date_allowed": date(today.year, 9, 1), - "max_date_allowed": date(today.year + 1, 7, 15), - "initial_visible_month": today, - } - - return { - "min_date_allowed": date(today.year - 1, 9, 1), - "max_date_allowed": date(today.year, 7, 15), - "initial_visible_month": today, - } - - -layout = html.Div( - [ - html.Header( - children=[ - html.H1("Création d'une évaluation"), - html.P("Pas encore de sauvegarde", id="is-saved"), - html.Button("Enregistrer dans csv", id="save-csv"), - ], - ), - html.Main( - children=[ - html.Section( - children=[ - html.Form( - id="new-exam", - children=[ - html.Label( - children=[ - "Classe", - dcc.Dropdown( - id="tribe", - options=[ - {"label": t["name"], "value": t["name"]} - for t in config["tribes"] - ], - value=config["tribes"][0]["name"], - ), - ] - ), - html.Label( - children=[ - "Nom de l'évaluation", - dcc.Input( - id="exam_name", - type="text", - placeholder="Nom de l'évaluation", - ), - ] - ), - html.Label( - children=[ - "Date", - dcc.DatePickerSingle( - id="date", - date=date.today(), - **get_current_year_limit(), - ), - ] - ), - html.Label( - children=[ - "Trimestre", - dcc.Dropdown( - id="term", - options=[ - {"label": i + 1, "value": i + 1} - for i in range(3) - ], - value=1, - ), - ] - ), - ], - ), - ], - id="form", - ), - html.Section( - children=[ - html.Div( - id="exercises", - children=[], - ), - html.Button( - "Ajouter un exercice", - id="add-exercise", - className="add-exercise", - ), - html.Div( - id="summary", - ), - ], - id="exercises", - ), - html.Section( - children=[ - html.Div( - id="score_rate", - ), - html.Div( - id="exercises-viz", - ), - html.Div( - id="competences-viz", - ), - html.Div( - id="themes-viz", - ), - ], - id="visualisation", - ), - ] - ), - dcc.Store(id="exam_store"), - ] -) - - -@app.callback( - dash.dependencies.Output("exercises", "children"), - dash.dependencies.Input("add-exercise", "n_clicks"), - dash.dependencies.State("exercises", "children"), -) -def add_exercise(n_clicks, children): - if n_clicks is None: - return children - element_table = pd.DataFrame(columns=[c["id"] for c in QUESTION_COLUMNS]) - element_table = element_table.append( - pd.Series( - data={ - "id": 1, - "competence": "Rechercher", - "theme": "", - "comment": "", - "score_rate": 1, - "is_leveled": 1, - }, - name=0, - ) - ) - new_exercise = html.Div( - children=[ - html.Div( - children=[ - dcc.Input( - id={"type": "exercice", "index": str(n_clicks)}, - type="text", - value=f"Exercice {len(children)+1}", - placeholder="Nom de l'exercice", - className="exercise-name", - ), - html.Button( - "X", - id={"type": "rm_exercice", "index": str(n_clicks)}, - className="delete-exercise", - ), - ], - className="exercise-head", - ), - dash_table.DataTable( - id={"type": "elements", "index": str(n_clicks)}, - columns=QUESTION_COLUMNS, - data=element_table.to_dict("records"), - editable=True, - row_deletable=True, - dropdown={ - "competence": { - "options": [ - {"label": i, "value": i} for i in config["competences"] - ] - }, - }, - style_cell={ - "whiteSpace": "normal", - "height": "auto", - }, - ), - html.Button( - "Ajouter un élément de notation", - id={"type": "add-element", "index": str(n_clicks)}, - className="add-element", - ), - ], - className="exercise", - id=f"exercise-{n_clicks}", - ) - children.append(new_exercise) - return children - - -@app.callback( - dash.dependencies.Output( - {"type": "elements", "index": dash.dependencies.MATCH}, "data" - ), - dash.dependencies.Input( - {"type": "add-element", "index": dash.dependencies.MATCH}, "n_clicks" - ), - [ - dash.dependencies.State( - {"type": "elements", "index": dash.dependencies.MATCH}, "data" - ), - ], - prevent_initial_call=True, -) -def add_element(n_clicks, elements): - if n_clicks is None or n_clicks < len(elements): - return elements - - df = pd.DataFrame.from_records(elements) - df = df.append( - pd.Series( - data={ - "id": len(df) + 1, - "competence": "", - "theme": "", - "comment": "", - "score_rate": 1, - "is_leveled": 1, - }, - name=n_clicks, - ) - ) - return df.to_dict("records") - - -def exam_generalities(tribe, exam_name, date, term, exercices=[], elements=[]): - return [ - html.H1(f"{exam_name} pour les {tribe}"), - html.P(f"Fait le {date} (Trimestre {term})"), - ] - - -def exercise_summary(identifier, name, elements=[]): - df = pd.DataFrame.from_records(elements) - return html.Div( - [ - html.H2(name), - dash_table.DataTable( - columns=[{"id": c, "name": c} for c in df], data=elements - ), - ] - ) - - -@app.callback( - dash.dependencies.Output("exam_store", "data"), - [ - dash.dependencies.Input("tribe", "value"), - dash.dependencies.Input("exam_name", "value"), - dash.dependencies.Input("date", "date"), - dash.dependencies.Input("term", "value"), - dash.dependencies.Input( - {"type": "exercice", "index": dash.dependencies.ALL}, "value" - ), - dash.dependencies.Input( - {"type": "elements", "index": dash.dependencies.ALL}, "data" - ), - ], - dash.dependencies.State({"type": "elements", "index": dash.dependencies.ALL}, "id"), -) -def store_exam(tribe, exam_name, date, term, exercices, elements, elements_id): - exam = Exam(exam_name, tribe, date, term) - for (i, name) in enumerate(exercices): - ex_elements_id = [el for el in elements_id if el["index"] == str(i + 1)][0] - index = elements_id.index(ex_elements_id) - ex_elements = elements[index] - exam.add_exercise(name, ex_elements) - - return exam.to_dict() - - -@app.callback( - dash.dependencies.Output("score_rate", "children"), - dash.dependencies.Input("exam_store", "data"), - prevent_initial_call=True, -) -def score_rate(data): - exam = Exam(**data) - return [html.P(f"Barème /{exam.score_rate}")] - - -@app.callback( - dash.dependencies.Output("competences-viz", "figure"), - dash.dependencies.Input("exam_store", "data"), - prevent_initial_call=True, -) -def competences_viz(data): - exam = Exam(**data) - return [html.P(str(exam.competences_rate))] - - -@app.callback( - dash.dependencies.Output("themes-viz", "children"), - dash.dependencies.Input("exam_store", "data"), - prevent_initial_call=True, -) -def themes_viz(data): - exam = Exam(**data) - themes_rate = exam.themes_rate - fig = go.Figure() - if themes_rate: - fig.add_trace(go.Pie(labels=list(themes_rate.keys()), values=list(themes_rate.values()))) - return [dcc.Graph(figure=fig)] - return [] - - -@app.callback( - dash.dependencies.Output("is-saved", "children"), - dash.dependencies.Input("save-csv", "n_clicks"), - dash.dependencies.State("exam_store", "data"), - prevent_initial_call=True, -) -def save_to_csv(n_clicks, data): - exam = Exam(**data) - csv = exam.path(".csv") - exam.write_csv() - return [f"Dernière sauvegarde {datetime.today()} dans {csv}"] diff --git a/recopytex/dashboard/exam_analysis/__init__.py b/recopytex/dashboard/exam_analysis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recopytex/dashboard/exam_analysis/app.py b/recopytex/dashboard/exam_analysis/app.py deleted file mode 100644 index 54e36f8..0000000 --- a/recopytex/dashboard/exam_analysis/app.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import dash -import dash_html_components as html -import dash_core_components as dcc -import dash_table -from dash.exceptions import PreventUpdate -import plotly.graph_objects as go -from pathlib import Path -from datetime import datetime -import pandas as pd -import numpy as np - - -from ... import flat_df_students, pp_q_scores -from ...config import NO_ST_COLUMNS -from ...scripts.getconfig import config -from ..app import app - -COLORS = { - ".": "black", - 0: "#E7472B", - 1: "#FF712B", - 2: "#F2EC4C", - 3: "#68D42F", -} - -layout = html.Div( - children=[ - html.Header( - children=[ - html.H1("Analyse des notes"), - html.P("Dernière sauvegarde", id="lastsave"), - ], - ), - html.Main( - [ - html.Section( - [ - html.Div( - [ - "Classe: ", - dcc.Dropdown( - id="tribe", - options=[ - {"label": t["name"], "value": t["name"]} - for t in config["tribes"] - ], - value=config["tribes"][0]["name"], - ), - ], - style={ - "display": "flex", - "flex-flow": "column", - }, - ), - html.Div( - [ - "Evaluation: ", - dcc.Dropdown(id="csv"), - ], - style={ - "display": "flex", - "flex-flow": "column", - }, - ), - ], - id="select", - style={ - "display": "flex", - "flex-flow": "row wrap", - }, - ), - html.Div( - [ - html.Div( - dash_table.DataTable( - id="final_score_table", - columns=[ - {"id": "Eleve", "name": "Élève"}, - {"id": "Note", "name": "Note"}, - {"id": "Bareme", "name": "Barème"}, - ], - data=[], - style_data_conditional=[ - { - "if": {"row_index": "odd"}, - "backgroundColor": "rgb(248, 248, 248)", - } - ], - style_data={ - "width": "100px", - "maxWidth": "100px", - "minWidth": "100px", - }, - ), - id="final_score_table_container", - ), - html.Div( - [ - dash_table.DataTable( - id="final_score_describe", - columns=[ - {"id": "count", "name": "count"}, - {"id": "mean", "name": "mean"}, - {"id": "std", "name": "std"}, - {"id": "min", "name": "min"}, - {"id": "25%", "name": "25%"}, - {"id": "50%", "name": "50%"}, - {"id": "75%", "name": "75%"}, - {"id": "max", "name": "max"}, - ], - ), - dcc.Graph( - id="fig_assessment_hist", - ), - dcc.Graph(id="fig_competences"), - ], - id="desc_plots", - ), - ], - id="analysis", - ), - html.Div( - [ - dash_table.DataTable( - id="scores_table", - columns=[ - {"id": "id", "name": "Question"}, - { - "id": "competence", - "name": "Competence", - }, - {"id": "theme", "name": "Domaine"}, - {"id": "comment", "name": "Commentaire"}, - {"id": "score_rate", "name": "Bareme"}, - {"id": "is_leveled", "name": "Est_nivele"}, - ], - style_cell={ - "whiteSpace": "normal", - "height": "auto", - }, - fixed_columns={"headers": True, "data": 7}, - style_table={"minWidth": "100%"}, - style_data_conditional=[], - editable=True, - ), - html.Button("Ajouter un élément", id="btn_add_element"), - ], - id="big_table", - ), - dcc.Store(id="final_score"), - ], - className="content", - style={ - "width": "95vw", - "margin": "auto", - }, - ), - ], -) - - -@app.callback( - [ - dash.dependencies.Output("csv", "options"), - dash.dependencies.Output("csv", "value"), - ], - [dash.dependencies.Input("tribe", "value")], -) -def update_csvs(value): - if not value: - raise PreventUpdate - p = Path(value) - csvs = list(p.glob("*.csv")) - try: - return [{"label": str(c), "value": str(c)} for c in csvs], str(csvs[0]) - except IndexError: - return [] - - -@app.callback( - [ - dash.dependencies.Output("final_score", "data"), - ], - [dash.dependencies.Input("scores_table", "data")], -) -def update_final_scores(data): - if not data: - raise PreventUpdate - - scores = pd.DataFrame.from_records(data) - try: - if scores.iloc[0]["Commentaire"] == "commentaire": - scores.drop([0], inplace=True) - except KeyError: - pass - scores = flat_df_students(scores).dropna(subset=["Score"]) - if scores.empty: - return [{}] - - scores = pp_q_scores(scores) - assessment_scores = scores.groupby(["Eleve"]).agg({"Note": "sum", "Bareme": "sum"}) - return [assessment_scores.reset_index().to_dict("records")] - - -@app.callback( - [ - dash.dependencies.Output("final_score_table", "data"), - ], - [dash.dependencies.Input("final_score", "data")], -) -def update_final_scores_table(data): - assessment_scores = pd.DataFrame.from_records(data) - return [assessment_scores.to_dict("records")] - - -@app.callback( - [ - dash.dependencies.Output("final_score_describe", "data"), - ], - [dash.dependencies.Input("final_score", "data")], -) -def update_final_scores_descr(data): - scores = pd.DataFrame.from_records(data) - if scores.empty: - return [[{}]] - desc = scores["Note"].describe().T.round(2) - return [[desc.to_dict()]] - - -@app.callback( - [ - dash.dependencies.Output("fig_assessment_hist", "figure"), - ], - [dash.dependencies.Input("final_score", "data")], -) -def update_final_scores_hist(data): - assessment_scores = pd.DataFrame.from_records(data) - - if assessment_scores.empty: - return [go.Figure(data=[go.Scatter(x=[], y=[])])] - - ranges = np.linspace( - -0.5, - assessment_scores.Bareme.max(), - int(assessment_scores.Bareme.max() * 2 + 2), - ) - bins = pd.cut(assessment_scores["Note"], ranges) - assessment_scores["Bin"] = bins - assessment_grouped = ( - assessment_scores.reset_index() - .groupby("Bin") - .agg({"Bareme": "count", "Eleve": lambda x: "\n".join(x)}) - ) - assessment_grouped.index = assessment_grouped.index.map(lambda i: i.right) - fig = go.Figure() - fig.add_bar( - x=assessment_grouped.index, - y=assessment_grouped.Bareme, - text=assessment_grouped.Eleve, - textposition="auto", - hovertemplate="", - marker_color="#4E89DE", - ) - fig.update_layout( - height=300, - margin=dict(l=5, r=5, b=5, t=5), - ) - return [fig] - - -@app.callback( - [ - dash.dependencies.Output("fig_competences", "figure"), - ], - [dash.dependencies.Input("scores_table", "data")], -) -def update_competence_fig(data): - scores = pd.DataFrame.from_records(data) - try: - if scores.iloc[0]["Commentaire"] == "commentaire": - scores.drop([0], inplace=True) - except KeyError: - pass - scores = flat_df_students(scores).dropna(subset=["Score"]) - - if scores.empty: - return [go.Figure(data=[go.Scatter(x=[], y=[])])] - - scores = pp_q_scores(scores) - pt = pd.pivot_table( - scores, - index=["Exercice", "Question", "Commentaire"], - columns="Score", - aggfunc="size", - fill_value=0, - ) - for i in {i for i in pt.index.get_level_values(0)}: - pt.loc[(str(i), "", ""), :] = "" - pt.sort_index(inplace=True) - index = ( - pt.index.get_level_values(0).map(str) - + ":" - + pt.index.get_level_values(1).map(str) - + " " - + pt.index.get_level_values(2).map(str) - ) - - fig = go.Figure() - bars = [ - {"score": -1, "name": "Pas de réponse", "color": COLORS["."]}, - {"score": 0, "name": "Faux", "color": COLORS[0]}, - {"score": 1, "name": "Peu juste", "color": COLORS[1]}, - {"score": 2, "name": "Presque juste", "color": COLORS[2]}, - {"score": 3, "name": "Juste", "color": COLORS[3]}, - ] - for b in bars: - try: - fig.add_bar( - x=index, y=pt[b["score"]], name=b["name"], marker_color=b["color"] - ) - except KeyError: - pass - fig.update_layout(barmode="relative") - fig.update_layout( - height=500, - margin=dict(l=5, r=5, b=5, t=5), - ) - return [fig] - - -@app.callback( - [ - dash.dependencies.Output("lastsave", "children"), - ], - [ - dash.dependencies.Input("scores_table", "data"), - dash.dependencies.State("csv", "value"), - ], -) -def save_scores(data, csv): - try: - scores = pd.DataFrame.from_records(data) - scores.to_csv(csv, index=False) - except: - return [f"Soucis pour sauvegarder à {datetime.today()} dans {csv}"] - else: - return [f"Dernière sauvegarde {datetime.today()} dans {csv}"] - - -def highlight_value(df): - """ Cells style """ - hight = [] - for v, color in COLORS.items(): - hight += [ - { - "if": {"filter_query": "{{{}}} = {}".format(col, v), "column_id": col}, - "backgroundColor": color, - "color": "white", - } - for col in df.columns - if col not in NO_ST_COLUMNS.values() - ] - return hight - - -@app.callback( - [ - dash.dependencies.Output("scores_table", "columns"), - dash.dependencies.Output("scores_table", "data"), - dash.dependencies.Output("scores_table", "style_data_conditional"), - ], - [ - dash.dependencies.Input("csv", "value"), - dash.dependencies.Input("btn_add_element", "n_clicks"), - dash.dependencies.State("scores_table", "data"), - ], -) -def update_scores_table(csv, add_element, data): - ctx = dash.callback_context - if ctx.triggered[0]["prop_id"] == "csv.value": - stack = pd.read_csv(csv, encoding="UTF8") - elif ctx.triggered[0]["prop_id"] == "btn_add_element.n_clicks": - stack = pd.DataFrame.from_records(data) - infos = pd.DataFrame.from_records( - [{k: stack.iloc[-1][k] for k in NO_ST_COLUMNS.values()}] - ) - stack = stack.append(infos) - return ( - [ - {"id": c, "name": c} - for c in stack.columns - if c not in ["Trimestre", "Nom", "Date"] - ], - stack.to_dict("records"), - highlight_value(stack), - ) diff --git a/recopytex/dashboard/index.py b/recopytex/dashboard/index.py deleted file mode 100644 index 3052671..0000000 --- a/recopytex/dashboard/index.py +++ /dev/null @@ -1,29 +0,0 @@ -import dash_core_components as dcc -import dash_html_components as html -from dash.dependencies import Input, Output - -from .app import app -from .exam_analysis import app as exam_analysis -from .create_exam import app as create_exam -from .student_analysis import app as student_analysis - - -app.layout = html.Div( - [dcc.Location(id="url", refresh=False), html.Div(id="page-content")] -) - - -@app.callback(Output("page-content", "children"), Input("url", "pathname")) -def display_page(pathname): - if pathname == "/": - return exam_analysis.layout - elif pathname == "/create-exam": - return create_exam.layout - elif pathname == "/students": - return student_analysis.layout - else: - return "404" - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/recopytex/dashboard/student_analysis/__init__.py b/recopytex/dashboard/student_analysis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recopytex/dashboard/student_analysis/app.py b/recopytex/dashboard/student_analysis/app.py deleted file mode 100644 index 1f7cc74..0000000 --- a/recopytex/dashboard/student_analysis/app.py +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import dash -import dash_html_components as html -import dash_core_components as dcc -import dash_table -import plotly.graph_objects as go -from datetime import date, datetime -import uuid -import pandas as pd -import yaml -from pathlib import Path - -from ...scripts.getconfig import config -from ... import flat_df_students, pp_q_scores -from ...config import NO_ST_COLUMNS -from ..app import app -from ...scripts.exam import Exam - - -def get_students(csv): - return list(pd.read_csv(csv).T.to_dict().values()) - - -COLORS = { - ".": "black", - 0: "#E7472B", - 1: "#FF712B", - 2: "#F2EC4C", - 3: "#68D42F", -} - -QUESTION_COLUMNS = [ - {"id": "id", "name": "Question"}, - { - "id": "competence", - "name": "Competence", - "presentation": "dropdown", - }, - {"id": "theme", "name": "Domaine"}, - {"id": "comment", "name": "Commentaire"}, - {"id": "score_rate", "name": "Bareme"}, - {"id": "is_leveled", "name": "Est_nivele"}, -] - -layout = html.Div( - [ - html.Header( - children=[ - html.H1("Bilan des élèves"), - ], - ), - html.Main( - children=[ - html.Section( - children=[ - html.Form( - id="select-student", - children=[ - html.Label( - children=[ - "Classe", - dcc.Dropdown( - id="tribe", - options=[ - {"label": t["name"], "value": t["name"]} - for t in config["tribes"] - ], - value=config["tribes"][0]["name"], - ), - ] - ), - html.Label( - children=[ - "Élève", - dcc.Dropdown( - id="student", - options=[ - {"label": t["Nom"], "value": t["Nom"]} - for t in get_students(config["tribes"][0]["students"]) - ], - value=get_students(config["tribes"][0]["students"])[0]["Nom"], - ), - ] - ), - html.Label( - children=[ - "Trimestre", - dcc.Dropdown( - id="term", - options=[ - {"label": i + 1, "value": i + 1} - for i in range(3) - ], - value=1, - ), - ] - ), - ], - ), - ], - id="form", - ), - html.Section( - children=[ - html.H2("Évaluations"), - html.Div( - dash_table.DataTable( - id="exam_scores", - columns=[ - {"id": "Nom", "name": "Évaluations"}, - {"id": "Note", "name": "Note"}, - {"id": "Bareme", "name": "Barème"}, - ], - data=[], - style_data_conditional=[ - { - "if": {"row_index": "odd"}, - "backgroundColor": "rgb(248, 248, 248)", - } - ], - style_data={ - "width": "100px", - "maxWidth": "100px", - "minWidth": "100px", - }, - ), - id="eval-table", - ), - ], - id="Évaluations", - ), - html.Section( - children=[ - html.Div( - id="competences-viz", - ), - html.Div( - id="themes-vizz", - ), - ], - id="visualisation", - ), - ] - ), - dcc.Store(id="student-scores"), - ] -) - - -@app.callback( - [ - dash.dependencies.Output("student", "options"), - dash.dependencies.Output("student", "value"), - ], - [ - dash.dependencies.Input("tribe", "value") - ],) -def update_students_list(tribe): - tribe_config = [t for t in config["tribes"] if t["name"] == tribe][0] - students = get_students(tribe_config["students"]) - options = [ - {"label": t["Nom"], "value": t["Nom"]} - for t in students - ] - value = students[0]["Nom"] - return options, value - - -@app.callback( - [ - dash.dependencies.Output("student-scores", "data"), - ], - [ - dash.dependencies.Input("tribe", "value"), - dash.dependencies.Input("student", "value"), - dash.dependencies.Input("term", "value"), - ], - ) -def update_student_scores(tribe, student, term): - tribe_config = [t for t in config["tribes"] if t["name"] == tribe][0] - - p = Path(tribe_config["name"]) - csvs = list(p.glob("*.csv")) - - dfs = [] - for csv in csvs: - try: - scores = pd.read_csv(csv) - except pd.errors.ParserError: - pass - else: - try: - if scores.iloc[0]["Commentaire"] == "commentaire": - scores.drop([0], inplace=True) - except KeyError: - pass - scores = flat_df_students(scores).dropna(subset=["Score"]) - scores = scores[scores["Eleve"] == student] - scores = scores[scores["Trimestre"] == term] - dfs.append(scores) - - df = pd.concat(dfs) - - return [df.to_dict("records")] - - -@app.callback( - [ - dash.dependencies.Output("exam_scores", "data"), - ], - [ - dash.dependencies.Input("student-scores", "data"), - ], -) -def update_exam_scores(data): - scores = pd.DataFrame.from_records(data) - scores = pp_q_scores(scores) - assessment_scores = scores.groupby(["Nom"]).agg({"Note": "sum", "Bareme": "sum"}) - return [assessment_scores.reset_index().to_dict("records")] - - -@app.callback( - [ - dash.dependencies.Output("competences-viz", "children"), - ], - [ - dash.dependencies.Input("student-scores", "data"), - ], - ) -def update_competences_viz(data): - scores = pd.DataFrame.from_records(data) - scores = pp_q_scores(scores) - pt = pd.pivot_table( - scores, - index=["Competence"], - columns="Score", - aggfunc="size", - fill_value=0, - ) - fig = go.Figure() - bars = [ - {"score": -1, "name": "Pas de réponse", "color": COLORS["."]}, - {"score": 0, "name": "Faux", "color": COLORS[0]}, - {"score": 1, "name": "Peu juste", "color": COLORS[1]}, - {"score": 2, "name": "Presque juste", "color": COLORS[2]}, - {"score": 3, "name": "Juste", "color": COLORS[3]}, - ] - for b in bars: - try: - fig.add_bar( - x=list(config["competences"].keys()), y=pt[b["score"]], name=b["name"], marker_color=b["color"] - ) - except KeyError: - pass - fig.update_layout(barmode="relative") - fig.update_layout( - height=500, - margin=dict(l=5, r=5, b=5, t=5), - ) - return [dcc.Graph(figure=fig)] - -@app.callback( - [ - dash.dependencies.Output("themes-vizz", "children"), - ], - [ - dash.dependencies.Input("student-scores", "data"), - ], - ) -def update_themes_viz(data): - scores = pd.DataFrame.from_records(data) - scores = pp_q_scores(scores) - pt = pd.pivot_table( - scores, - index=["Domaine"], - columns="Score", - aggfunc="size", - fill_value=0, - ) - fig = go.Figure() - bars = [ - {"score": -1, "name": "Pas de réponse", "color": COLORS["."]}, - {"score": 0, "name": "Faux", "color": COLORS[0]}, - {"score": 1, "name": "Peu juste", "color": COLORS[1]}, - {"score": 2, "name": "Presque juste", "color": COLORS[2]}, - {"score": 3, "name": "Juste", "color": COLORS[3]}, - ] - for b in bars: - try: - fig.add_bar( - x=list(pt.index), y=pt[b["score"]], name=b["name"], marker_color=b["color"] - ) - except KeyError: - pass - fig.update_layout(barmode="relative") - fig.update_layout( - height=500, - margin=dict(l=5, r=5, b=5, t=5), - ) - return [dcc.Graph(figure=fig)] - diff --git a/recopytex/df_marks_manip.py b/recopytex/df_marks_manip.py deleted file mode 100644 index 584882b..0000000 --- a/recopytex/df_marks_manip.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import pandas as pd -import numpy as np -from math import ceil, floor -from .config import COLUMNS - -""" -Functions for manipulate score dataframes -""" - - -def round_half_point(val): - try: - return 0.5 * ceil(2.0 * val) - except ValueError: - return val - except TypeError: - return val - - -def score_to_mark(x): - """Compute the mark - - if the item is leveled then the score is multiply by the score_rate - otherwise it copies the score - - :param x: dictionnary with COLUMNS["is_leveled"], COLUMNS["score"] and COLUMNS["score_rate"] keys - :return: the mark - - >>> d = {"Eleve":["E1"]*6 + ["E2"]*6, - ... COLUMNS["score_rate"]:[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, - ... COLUMNS["is_leveled"]:[0]*4+[1]*2 + [0]*4+[1]*2, - ... COLUMNS["score"]:[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3], - ... } - >>> df = pd.DataFrame(d) - >>> score_to_mark(df.loc[0]) - 1.0 - >>> score_to_mark(df.loc[10]) - 1.3333333333333333 - """ - # -1 is no answer - if x[COLUMNS["score"]] == -1: - return 0 - - if x[COLUMNS["is_leveled"]]: - if x[COLUMNS["score"]] not in [0, 1, 2, 3]: - raise ValueError( - f"The evaluation is out of range: {x[COLUMNS['score']]} at {x}" - ) - return round(x[COLUMNS["score"]] * x[COLUMNS["score_rate"]] / 3, 2) - #return round_half_point(x[COLUMNS["score"]] * x[COLUMNS["score_rate"]] / 3) - - if x[COLUMNS["score"]] > x[COLUMNS["score_rate"]]: - raise ValueError( - f"The score ({x['score']}) is greated than the rating scale ({x[COLUMNS['score_rate']]}) at {x}" - ) - return x[COLUMNS["score"]] - - -def score_to_level(x): - """Compute the level (".",0,1,2,3). - - :param x: dictionnary with COLUMNS["is_leveled"], COLUMNS["score"] and COLUMNS["score_rate"] keys - :return: the level - - >>> d = {"Eleve":["E1"]*6 + ["E2"]*6, - ... COLUMNS["score_rate"]:[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, - ... COLUMNS["is_leveled"]:[0]*4+[1]*2 + [0]*4+[1]*2, - ... COLUMNS["score"]:[1, 0.33, np.nan, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3], - ... } - >>> df = pd.DataFrame(d) - >>> score_to_level(df.loc[0]) - 3 - >>> score_to_level(df.loc[1]) - 1 - >>> score_to_level(df.loc[2]) - 'na' - >>> score_to_level(df.loc[3]) - 3 - >>> score_to_level(df.loc[5]) - 3 - >>> score_to_level(df.loc[10]) - 2 - """ - # negatives are no answer or negatives points - if x[COLUMNS["score"]] <= -1: - return np.nan - - if x[COLUMNS["is_leveled"]]: - return int(x[COLUMNS["score"]]) - - return int(ceil(x[COLUMNS["score"]] / x[COLUMNS["score_rate"]] * 3)) - - -# DataFrame columns manipulations - - -def compute_mark(df): - """Compute the mark for the dataframe - - apply score_to_mark to each row - - :param df: DataFrame with COLUMNS["score"], COLUMNS["is_leveled"] and COLUMNS["score_rate"] columns. - - >>> d = {"Eleve":["E1"]*6 + ["E2"]*6, - ... COLUMNS["score_rate"]:[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, - ... COLUMNS["is_leveled"]:[0]*4+[1]*2 + [0]*4+[1]*2, - ... COLUMNS["score"]:[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3], - ... } - >>> df = pd.DataFrame(d) - >>> compute_mark(df) - 0 1.00 - 1 0.33 - 2 2.00 - 3 1.50 - 4 0.67 - 5 2.00 - 6 0.67 - 7 1.00 - 8 1.50 - 9 1.00 - 10 1.33 - 11 2.00 - dtype: float64 - """ - return df[[COLUMNS["score"], COLUMNS["is_leveled"], COLUMNS["score_rate"]]].apply( - score_to_mark, axis=1 - ) - - -def compute_level(df): - """Compute level for the dataframe - - Applies score_to_level to each row - - :param df: DataFrame with COLUMNS["score"], COLUMNS["is_leveled"] and COLUMNS["score_rate"] columns. - :return: Columns with level - - >>> d = {"Eleve":["E1"]*6 + ["E2"]*6, - ... COLUMNS["score_rate"]:[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, - ... COLUMNS["is_leveled"]:[0]*4+[1]*2 + [0]*4+[1]*2, - ... COLUMNS["score"]:[np.nan, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3], - ... } - >>> df = pd.DataFrame(d) - >>> compute_level(df) - 0 na - 1 1 - 2 3 - 3 3 - 4 1 - 5 3 - 6 2 - 7 3 - 8 3 - 9 2 - 10 2 - 11 3 - dtype: object - """ - return df[[COLUMNS["score"], COLUMNS["is_leveled"], COLUMNS["score_rate"]]].apply( - score_to_level, axis=1 - ) - - -def compute_normalized(df): - """Compute the normalized mark (Mark / score_rate) - - :param df: DataFrame with "Mark" and COLUMNS["score_rate"] columns - :return: column with normalized mark - - >>> d = {"Eleve":["E1"]*6 + ["E2"]*6, - ... COLUMNS["score_rate"]:[1]*2+[2]*2+[2]*2 + [1]*2+[2]*2+[2]*2, - ... COLUMNS["is_leveled"]:[0]*4+[1]*2 + [0]*4+[1]*2, - ... COLUMNS["score"]:[1, 0.33, 2, 1.5, 1, 3, 0.666, 1, 1.5, 1, 2, 3], - ... } - >>> df = pd.DataFrame(d) - >>> df["Mark"] = compute_marks(df) - >>> compute_normalized(df) - 0 1.00 - 1 0.33 - 2 1.00 - 3 0.75 - 4 0.33 - 5 1.00 - 6 0.67 - 7 1.00 - 8 0.75 - 9 0.50 - 10 0.67 - 11 1.00 - dtype: float64 - """ - return df[COLUMNS["mark"]] / df[COLUMNS["score_rate"]] - - -# Postprocessing question scores - - -def pp_q_scores(df): - """Postprocessing questions scores dataframe - - Add 3 columns: mark, level and normalized - - :param df: questions-scores dataframe - :return: same data frame with mark, level and normalize columns - """ - assign = { - COLUMNS["mark"]: compute_mark, - COLUMNS["level"]: compute_level, - COLUMNS["normalized"]: compute_normalized, - } - return df.assign(**assign) - - -# ----------------------------- -# Reglages pour 'vim' -# vim:set autoindent expandtab tabstop=4 shiftwidth=4: -# cursor: 16 del diff --git a/recopytex/scripts/__init__.py b/recopytex/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recopytex/scripts/exam.py b/recopytex/scripts/exam.py deleted file mode 100644 index 3593538..0000000 --- a/recopytex/scripts/exam.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -from datetime import datetime -from pathlib import Path -from prompt_toolkit import HTML -from ..config import NO_ST_COLUMNS -import pandas as pd -import yaml -from .getconfig import config - - -def try_parsing_date(text, formats=["%Y-%m-%d", "%Y.%m.%d", "%Y/%m/%d"]): - for fmt in formats: - try: - return datetime.strptime(text[:10], fmt) - except ValueError: - pass - raise ValueError("no valid date format found") - - -def format_question(question): - question["score_rate"] = float(question["score_rate"]) - return question - - -class Exam: - def __init__(self, name, tribename, date, term, **kwrds): - self._name = name - self._tribename = tribename - - self._date = try_parsing_date(date) - - self._term = term - - try: - kwrds["exercices"] - except KeyError: - self._exercises = {} - else: - self._exercises = kwrds["exercices"] - - @property - def name(self): - return self._name - - @property - def tribename(self): - return self._tribename - - @property - def date(self): - return self._date - - @property - def term(self): - return self._term - - def add_exercise(self, name, questions): - """ Add key with questions in ._exercises """ - try: - self._exercises[name] - except KeyError: - self._exercises[name] = format_question(questions) - else: - raise KeyError("The exercise already exsists. Use modify_exercise") - - def modify_exercise(self, name, questions, append=False): - """Modify questions of an exercise - - If append==True, add questions to the exercise questions - - """ - try: - self._exercises[name] - except KeyError: - raise KeyError("The exercise already exsists. Use modify_exercise") - else: - if append: - self._exercises[name] += format_question(questions) - else: - self._exercises[name] = format_question(questions) - - @property - def exercices(self): - return self._exercises - - @property - def tribe_path(self): - return Path(config["source"]) / self.tribename - - @property - def tribe_student_path(self): - return ( - Path(config["source"]) - / [t["students"] for t in config["tribes"] if t["name"] == self.tribename][ - 0 - ] - ) - - @property - def long_name(self): - """ Get exam name with date inside """ - return f"{self.date.strftime('%y%m%d')}_{self.name}" - - def path(self, extention=""): - return self.tribe_path / (self.long_name + extention) - - def to_dict(self): - return { - "name": self.name, - "tribename": self.tribename, - "date": self.date, - "term": self.term, - "exercices": self.exercices, - } - - def to_row(self): - rows = [] - for ex, questions in self.exercices.items(): - for q in questions: - rows.append( - { - "term": self.term, - "assessment": self.name, - "date": self.date.strftime("%d/%m/%Y"), - "exercise": ex, - "question": q["id"], - **q, - } - ) - return rows - - @property - def themes(self): - themes = set() - for questions in self._exercises.values(): - themes.update([q["theme"] for q in questions]) - return themes - - def display_exercise(self, name): - pass - - def display(self, name): - pass - - def write_yaml(self): - print(f"Sauvegarde temporaire dans {self.path('.yml')}") - self.tribe_path.mkdir(exist_ok=True) - with open(self.path(".yml"), "w") as f: - f.write(yaml.dump(self.to_dict())) - - def write_csv(self): - rows = self.to_row() - - base_df = pd.DataFrame.from_dict(rows)[NO_ST_COLUMNS.keys()] - base_df.rename(columns=NO_ST_COLUMNS, inplace=True) - - students = pd.read_csv(self.tribe_student_path)["Nom"] - for student in students: - base_df[student] = "" - - self.tribe_path.mkdir(exist_ok=True) - base_df.to_csv(self.path(".csv"), index=False) - - @property - def score_rate(self): - total = 0 - for ex, questions in self._exercises.items(): - total += sum([q["score_rate"] for q in questions]) - - return total - - @property - def competences_rate(self): - """ Dictionnary with competences as key and total rate as value""" - rates = {} - for ex, questions in self._exercises.items(): - for q in questions: - try: - q["competence"] - except KeyError: - pass - else: - try: - rates[q["competence"]] += q["score_rate"] - except KeyError: - rates[q["competence"]] = q["score_rate"] - return rates - - @property - def themes_rate(self): - """ Dictionnary with themes as key and total rate as value""" - rates = {} - for ex, questions in self._exercises.items(): - for q in questions: - try: - q["theme"] - except KeyError: - pass - else: - if q["theme"]: - try: - rates[q["theme"]] += q["score_rate"] - except KeyError: - rates[q["theme"]] = q["score_rate"] - return rates diff --git a/recopytex/scripts/getconfig.py b/recopytex/scripts/getconfig.py deleted file mode 100644 index f2c4113..0000000 --- a/recopytex/scripts/getconfig.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -import yaml - -CONFIGPATH = "recoconfig.yml" - -with open(CONFIGPATH, "r") as config: - config = yaml.load(config, Loader=yaml.FullLoader) - diff --git a/recopytex/scripts/prompts.py b/recopytex/scripts/prompts.py deleted file mode 100644 index 685c1ba..0000000 --- a/recopytex/scripts/prompts.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - - -from prompt_toolkit import prompt, HTML, ANSI -from prompt_toolkit import print_formatted_text as print -from prompt_toolkit.styles import Style -from prompt_toolkit.validation import Validator -from prompt_toolkit.completion import WordCompleter -from unidecode import unidecode -from datetime import datetime -from functools import wraps -import sys - -from .getconfig import config - - -VALIDATE = [ - "o", - "ok", - "OK", - "oui", - "OUI", - "yes", - "YES", -] -REFUSE = ["n", "non", "NON", "no", "NO"] -CANCEL = ["a", "annuler"] - -STYLE = Style.from_dict( - { - "": "#93A1A1", - "validation": "#884444", - "appending": "#448844", - } -) - - -class CancelError(Exception): - pass - - -def prompt_validate(question, cancelable=False, empty_means=1, style="validation"): - """Prompt for validation - - :param question: Text to print to ask the question. - :param cancelable: enable cancel answer - :param empty_means: result for no answer - :return: - 0 -> Refuse - 1 -> Validate - -1 -> cancel - """ - question_ = question - choices = VALIDATE + REFUSE - - if cancelable: - question_ += "(a ou annuler pour sortir)" - choices += CANCEL - - ans = prompt( - [ - (f"class:{style}", question_), - ], - completer=WordCompleter(choices), - style=STYLE, - ).lower() - - if ans == "": - return empty_means - if ans in VALIDATE: - return 1 - if cancelable and ans in CANCEL: - return -1 - return 0 - - -def prompt_until_validate(question="C'est ok? ", cancelable=False): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwrd): - ans = func(*args, **kwrd) - - confirm = prompt_validate(question, cancelable) - - if confirm == -1: - raise CancelError - - while not confirm: - sys.stdout.flush() - ans = func(*args, **ans, **kwrd) - confirm = prompt_validate(question, cancelable) - if confirm == -1: - raise CancelError - return ans - - return wrapper - - return decorator - - -@prompt_until_validate() -def prompt_exam(**kwrd): - """ Prompt questions to edit an exam """ - print(HTML("Nouvelle évaluation")) - exam = {} - exam["name"] = prompt("Nom de l'évaluation: ", default=kwrd.get("name", "DS")) - - tribes_name = [t["name"] for t in config["tribes"]] - - exam["tribename"] = prompt( - "Nom de la classe: ", - default=kwrd.get("tribename", ""), - completer=WordCompleter(tribes_name), - validator=Validator.from_callable(lambda x: x in tribes_name), - ) - exam["tribe"] = [t for t in config["tribes"] if t["name"] == exam["tribename"]][0] - - exam["date"] = prompt( - "Date de l'évaluation (%y%m%d): ", - default=kwrd.get("date", datetime.today()).strftime("%y%m%d"), - validator=Validator.from_callable(lambda x: (len(x) == 6) and x.isdigit()), - ) - exam["date"] = datetime.strptime(exam["date"], "%y%m%d") - - exam["term"] = prompt( - "Trimestre: ", - validator=Validator.from_callable(lambda x: x.isdigit()), - default=kwrd.get("term", "1"), - ) - - return exam - - -@prompt_until_validate() -def prompt_exercise(number=1, completer={}, **kwrd): - exercise = {} - try: - kwrd["name"] - except KeyError: - print(HTML("Nouvel exercice")) - exercise["name"] = prompt( - "Nom de l'exercice: ", default=kwrd.get("name", f"Exercice {number}") - ) - else: - print(HTML(f"Modification de l'exercice: {kwrd['name']}")) - exercise["name"] = kwrd["name"] - - exercise["questions"] = [] - - try: - kwrd["questions"][0] - except KeyError: - last_question_id = "1a" - except IndexError: - last_question_id = "1a" - else: - for ques in kwrd["questions"]: - try: - exercise["questions"].append( - prompt_question(completer=completer, **ques) - ) - except CancelError: - print("Cette question a été supprimée") - last_question_id = exercise["questions"][-1]["id"] - - appending = prompt_validate( - question="Ajouter un élément de notation? ", style="appending" - ) - while appending: - try: - exercise["questions"].append( - prompt_question(last_question_id, completer=completer) - ) - except CancelError: - print("Cette question a été supprimée") - else: - last_question_id = exercise["questions"][-1]["id"] - appending = prompt_validate( - question="Ajouter un élément de notation? ", style="appending" - ) - - return exercise - - -@prompt_until_validate(cancelable=True) -def prompt_question(last_question_id="1a", completer={}, **kwrd): - try: - kwrd["id"] - except KeyError: - print(HTML("Nouvel élément de notation")) - else: - print( - HTML(f"Modification de l'élément {kwrd['id']} ({kwrd['comment']})") - ) - - question = {} - question["id"] = prompt( - "Identifiant de la question: ", - default=kwrd.get("id", "1a"), - ) - - question["competence"] = prompt( - "Competence: ", - default=kwrd.get("competence", list(config["competences"].keys())[0]), - completer=WordCompleter(config["competences"].keys()), - validator=Validator.from_callable(lambda x: x in config["competences"].keys()), - ) - - question["theme"] = prompt( - "Domaine: ", - default=kwrd.get("theme", ""), - completer=WordCompleter(completer.get("theme", [])), - ) - - question["comment"] = prompt( - "Commentaire: ", - default=kwrd.get("comment", ""), - ) - - question["is_leveled"] = prompt( - "Évaluation par niveau: ", - default=kwrd.get("is_leveled", "1"), - # validate - ) - - question["score_rate"] = prompt( - "Barème: ", - default=kwrd.get("score_rate", "1"), - # validate - ) - - return question diff --git a/recopytex/scripts/recopytex.py b/recopytex/scripts/recopytex.py deleted file mode 100644 index 7ec90de..0000000 --- a/recopytex/scripts/recopytex.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import click -from pathlib import Path -import sys -import papermill as pm -import pandas as pd -from datetime import datetime -import yaml - -from .getconfig import config, CONFIGPATH -from .prompts import prompt_exam, prompt_exercise, prompt_validate -from ..config import NO_ST_COLUMNS -from .exam import Exam -from ..dashboard.index import app as dash - - -@click.group() -def cli(): - pass - - -@cli.command() -def print_config(): - click.echo(f"Config file is {CONFIGPATH}") - click.echo("It contains") - click.echo(config) - - -@cli.command() -def setup(): - """Setup the environnement using recoconfig.yml""" - for tribe in config["tribes"]: - Path(tribe["name"]).mkdir(exist_ok=True) - if not Path(tribe["students"]).exists(): - print(f"The file {tribe['students']} does not exists") - - -@cli.command() -def new_exam(): - """ Create new exam csv file """ - exam = Exam(**prompt_exam()) - - if exam.path(".yml").exists(): - print(f"Fichier sauvegarde trouvé à {exam.path('.yml')} -- importation") - with open(exam.path(".yml"), "r") as f: - for name, questions in yaml.load(f, Loader=yaml.SafeLoader)[ - "exercices" - ].items(): - exam.add_exercise(name, questions) - - print(exam.themes) - # print(yaml.dump(exam.to_dict())) - - exam.write() - - for name, questions in exam.exercices.items(): - exam.modify_exercise( - **prompt_exercise( - name=name, completer={"theme": exam.themes}, questions=questions - ) - ) - exam.write() - - new_exercise = prompt_validate("Ajouter un exercice? ") - while new_exercise: - exam.add_exercise( - **prompt_exercise(len(exam.exercices) + 1, completer={"theme": exam.themes}) - ) - exam.write() - new_exercise = prompt_validate("Ajouter un exercice? ") - - rows = exam.to_row() - - base_df = pd.DataFrame.from_dict(rows)[NO_ST_COLUMNS.keys()] - base_df.rename(columns=NO_ST_COLUMNS, inplace=True) - - students = pd.read_csv(exam.tribe_student_path)["Nom"] - for student in students: - base_df[student] = "" - - exam.tribe_path.mkdir(exist_ok=True) - - base_df.to_csv(exam.path(".csv"), index=False) - print(f"Le fichier note a été enregistré à {exam.path('.csv')}") - - -@cli.command() -@click.option("--debug", default=0, help="Debug mode for dash") -def dashboard(debug): - dash.run_server(debug=bool(debug)) - - -@cli.command() -@click.argument("csv_file") -def report(csv_file): - csv = Path(csv_file) - if not csv.exists(): - click.echo(f"{csv_file} does not exists") - sys.exit(1) - if csv.suffix != ".csv": - click.echo(f"{csv_file} has to be a csv file") - sys.exit(1) - - csv_file = Path(csv_file) - tribe_dir = csv_file.parent - csv_filename = csv_file.name.split(".")[0] - - assessment = str(csv_filename).split("_")[-1].capitalize() - date = str(csv_filename).split("_")[0] - try: - date = datetime.strptime(date, "%y%m%d") - except ValueError: - date = None - - tribe = str(tribe_dir).split("/")[-1] - - template = Path(config["templates"]) / "tpl_evaluation.ipynb" - - dest = Path(config["output"]) / tribe / csv_filename - dest.mkdir(parents=True, exist_ok=True) - - click.echo(f"Building {assessment} ({date:%d/%m/%y}) report") - pm.execute_notebook( - str(template), - str(dest / f"{assessment}.ipynb"), - parameters=dict( - tribe=tribe, - assessment=assessment, - date=f"{date:%d/%m/%y}", - csv_file=str(csv_file.absolute()), - ), - ) diff --git a/recopytex/store/__init__.py b/recopytex/store/__init__.py new file mode 100644 index 0000000..206d3d5 --- /dev/null +++ b/recopytex/store/__init__.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# encoding: utf-8 + +from abc import ABC, abstractmethod +import yaml + +""" + +Adapter to pull data from the filesystem + +# Loader + +# Writer +""" + + +class Loader(ABC): + + """Load data from source""" + + def __init__(self, configfile="recoconfig.yml"): + """Init loader + + :param configfile: yaml file with informations on data source + + """ + with open(CONFIGPATH, "r") as config: + sefl._config = yaml.load(config, Loader=yaml.FullLoader) + + @abstractmethod + def get_tribes(self): + """ Get tribes list """ + pass + + @abstractmethod + def get_exams(self, tribes=[]): + """Get exams list + + :param tribes: get only exams for those tribes + """ + pass + + @abstractmethod + def get_students(self, tribes=[]): + """Get student list + + :param filters: list of filters + """ + pass + + @abstractmethod + def get_exam_questions(self, exams=[]): + """Get questions for the exam + + :param exams: questions for those exams only + """ + pass + + @abstractmethod + def get_questions_scores(self, questions=[]): + """Get scores of those questions + + :param questions: score for those questions + """ + pass + + @abstractmethod + def get_student_scores(self, student): + """Get scores of the student + + :param student: + """ + pass + + +class Writer(ABC): + + """ Write datas to the source """ + + @abstractmethod + def __init__(self): + pass diff --git a/recopytex/store/filesystem/__init__.py b/recopytex/store/filesystem/__init__.py new file mode 100644 index 0000000..eca93b5 --- /dev/null +++ b/recopytex/store/filesystem/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" +Store data using filesystem for organisation, csv for scores + +## Organisation + +- tribe1.csv # list of students for the tribe +- tribe1/ + - exam1.csv # questions and scores for exam1 + - exam1.yml # Extra information about exam1 + - exam2.csv # questions and scores for exam2 +""" + diff --git a/recopytex/store/filesystem/lib.py b/recopytex/store/filesystem/lib.py new file mode 100644 index 0000000..8fa2546 --- /dev/null +++ b/recopytex/store/filesystem/lib.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# encoding: utf-8 + +import pandas as pd +from pathlib import Path + + +__all__ = ["list_csvs", "extract_exam"] + + +def list_csvs(path): + """ list csv files in path """ + return list(Path(path).glob("*.csv")) + + +def extract_fields(csv_filename, fields=[], remove_duplicates=True): + """Extract fields in csv + + :param csv_filename: csv filename (with header) + :param fields: list of fields to extract (all fields if empty list - default) + :param remove_duplicates: keep uniques rows (default True) + + :example: + >>> extract_fields("./example/Tribe1/210122_DS6.csv", ["Trimestre", "Nom", "Date"]) + Trimestre Nom Date + 0 1 DS6 22/01/2021 + """ + df = pd.read_csv(csv_filename) + if fields: + df = df[fields] + if remove_duplicates: + return df.drop_duplicates() + return df diff --git a/recopytex/store/filesystem/loader.py b/recopytex/store/filesystem/loader.py new file mode 100644 index 0000000..e389bc7 --- /dev/null +++ b/recopytex/store/filesystem/loader.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# encoding: utf-8 + +from .. import Loader +import yaml + + +def list_csvs(path): + """ list csv files in path """ + pass + + +class CSVLoader(Loader): + + """Loader when scores and metadatas are stored in csv files + + ## configfile (`recoconfig.yml` by default + + source: ./ # basepath where to start (default value) + templates: # directory where templates are stored + + tribes: # All the tribes (required) + Tribe1: # Tribe name + directory: tribe1 # tribe directory + type: type1 # Type of tribe (2nd, 1, T...) + students: tribe1.csv # csv with infos on students + + competences: # Competences (default values) + Chercher: + name: Chercher + abrv: Cher + Représenter: + name: Représenter + abrv: Rep + Modéliser: + name: Modéliser + abrv: Mod + Raisonner: + name: Raisonner + abrv: Rai + Calculer: + name: Calculer + abrv: Cal + Communiquer: + name: Communiquer + abrv: Com + + valid_scores: # (default values) + BAD: 0 # Everything is bad + FEW: 1 # Few good things + NEARLY: 2 # Nearly good but things are missing + GOOD: 3 # Everything is good + NOTFILLED: # The item is not scored yet + NOANSWER: . # Student gives no answer (count as 0) + ABS: "a" # Student has absent (this score won't be impact the final mark) + + csv_fields: # dataframe_field: csv_field (default values) + term: Trimestre, + exam: Nom, + date: Date, + exercise: Exercice, + question: Question, + competence: Competence, + theme: Domaine, + comment: Commentaire, + score_rate: Bareme, + is_leveled: Est_nivele, + """ + + def get_config(self): + """ Get config""" + return self._config + + def get_tribes(self, only_names=False): + """ Get tribes list """ + if only_names: + return list(self._config["tribes"].keys()) + return self._config["tribes"] + + def get_exams(self, tribes=[]): + """Get exams list + + :param tribes: get only exams for those tribes + :return: list of dictionaries of exams (fields: `["name", "tribe", "date", "term"]) + """ + exams = [] + for tribe in tribes: + csvs = list_csvs() + for csv in csvs: + fields = [ + self._config["csv_fields"][k] for k in ["exam", "date", "term"] + ] + exam = extract_fields(csv, fields) + exam.rename(columns=self._config["csv_fields"], inplace=True).rename( + columns={"exam": "name"}, inplace=True + ) + + return df.concate(exams) + + def get_students(self, tribes=[]): + """Get student list + + :param filters: list of filters + """ + pass + + def get_exam_questions(self, exams=[]): + """Get questions for the exam + + :param exams: questions for those exams only + """ + pass + + def get_questions_scores(self, questions=[]): + """Get scores of those questions + + :param questions: score for those questions + """ + pass + + def get_student_scores(self, student): + """Get scores of the student + + :param student: + """ + pass diff --git a/recopytex/store/filesystem/writer.py b/recopytex/store/filesystem/writer.py new file mode 100644 index 0000000..dc50f39 --- /dev/null +++ b/recopytex/store/filesystem/writer.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# encoding: utf-8 + +""" + +""" +