class Slideshow(Model): """Slideshow model This model aims to represent a slideshow. Attributes ---------- title : str Title of the whole slideshow authors : Authors (format to be determined) children : list Slides exactly as they are stored in the database (i.e. without the solutions or the fields' data). """ _collection = "slideshows" title = fields.Field("Title") authors = fields.Field("Authors") children = [] @fields.computed("Slug", field=fields.Field) def slug(self): return slugify.slugify(self.title) if self.title else None
class Vote(Model): _collection = "votes" user = fields.Field("User") survey = fields.Field("Survey name") value = fields.Field("Value", default=False) edit_count = fields.Field("Count")
class FactoriseQuestion(form.Form): """Factorisation Question""" name = fields.Field("Survey name") answer = fields.Expression("Your answer", nosave=True, editable=True) expression = fields.Expression("Expression", required=True) template = """ <div>Factorise `expression`</div> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> Your answer: `answer` </survey> """ @fields.computed("Correct", field=fields.Boolean) def correct(self): if (not self.answer or sympy.simplify(self.answer - self.expression) != 0 or self.answer.func != sympy.Mul): return False for term in self.answer.args: if sympy.factor(term).func == sympy.Mul: return False return True def validate(self): self.expression = sympy.expand(self.expression) if not self.name: self.name = str(uuid.uuid1())
class SimplificationQuestion(form.Form): """Simplification Question""" name = fields.Field("Survey name") answer = fields.Expression("Your answer", nosave=True, editable=True) expression = fields.Expression("Expression") template = """ <div>Simplify `expression`</div> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> Your answer: `answer` </survey> """ @fields.computed("Correct", field=fields.Boolean) def correct(self): if not self.answer: return False return str(self.answer) == str(sympy.simplify(self.expression)) def validate(self): if not self.name: self.name = str(uuid.uuid1())
class Youtube(form.Form): """YouTube""" name = "YouTube" url = fields.Field("URL") zoom = fields.Expression("Zoom", default="1.0") template = """ <div v-if="config.edit"> URL: `url` Zoom: `zoom` </div> `video` """ @property def height(self): return 315 * self.zoom @property def width(self): return 560 * self.zoom @fields.computed("Youtube Video", field=fields.Html, nohide=True) def video(self): """Get HTML code of embedded YouTube video""" url = self.url.replace("watch?v=", "embed/") return f"""
class Survey(form.Form): """Marked question""" name = fields.Field("Survey name") answer = fields.Expression("Your answer", nosave=True, editable=True) correct_answer = fields.Expression("Correct Answer", required=True) max_error = fields.Expression("Maximal error", default=0) template = """ <p v-if="config.edit"> Correct answer: `correct_answer` Tolerated error: `max_error` </p> <survey :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> `answer` </survey> """ @fields.computed("Correct", field=fields.Boolean) def correct(self): if self.answer is None: return False return ( sympy.Abs(sympy.simplify(self.answer - self.correct_answer)) <= self.max_error ) def validate(self): if not self.name: self.name = str(uuid.uuid1())
class CustomForm(form.Form): template = "Solve `equation`" equation = fields.Equation("Equation", default="x^3") required = fields.Field("Required field", required=True) def validate(self): if str(self.equation) != "Eq(x**2, 0)": raise AttributeError("Wrong equation")
class Survey(form.Form): """Marked question""" name = fields.Field("Survey name") answer = fields.Expression("Your answer", nosave=True, editable=True) correct_answer = fields.Expression("Correct Answer", required=True) max_error = fields.Expression("Maximal error", default=0) before = fields.Markdown("Before field text", default="Your answer:") after = fields.Markdown("After field text") question = fields.Markdown("Question") marking_type = fields.Select( "Marking type", options=["Default", "Numerical", "Fraction"], default="Default", ) template = """ <p v-if="config.edit || payload.question">`question`</p> <widget-settings v-if="config.edit"> <config-option name="Correct Answer">`correct_answer`</config-option> <config-option name="Tolerated error">`max_error`</config-option> <config-option name="Marking type">`marking_type`</config-option> </widget-settings> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> <span v-if="config.edit || payload.before">`before`</span> `answer` <span v-if="config.edit || payload.after">`after`</span> </survey> """ _total_marks = 1 @property def _marks(self): return 1 if self.correct else 0 @fields.computed("Correct", field=fields.Boolean) def correct(self): if self.answer is None: return False elif self.marking_type == "Numerical": numbers = sympy.core.numbers if not self.answer.func in [numbers.Float, numbers.Integer]: return False elif self.marking_type == "Fraction": if not re.search(r"^[0-9\s/]*$", self._answer): return False return (sympy.Abs(sympy.nsimplify(self.answer - self.correct_answer)) <= self.max_error) def validate(self): if not self.name: self.name = str(uuid.uuid1())
class Foo(form.Form): field = fields.Field("Field", desc="description") equation = fields.Equation("Equation") real = fields.Expression("Real number") markdown = fields.Markdown("Markdown") matrix = fields.Matrix("Matrix") email = fields.Email("Email") password = fields.Password("Email") protected = fields.Field("Protected", default="Protected", protected=True) select = fields.Select("Select", options=["Hi", "Hello"]) @fields.constraint("Contraint", default=True) def constraint(self): return self.real > 0 @fields.computed("Computed") def computed_field(self): return self.field
class Geogebra(form.Form): """Geogebra""" name = "Geogebra" url = fields.Field("URL", required=True) width = fields.Field("Width", default=800) height = fields.Field("Width", default=600) template = """ <div v-if="config.edit"> `url` (`width`x`height`) </div> `geogebra` """ @fields.computed("Geogebra", field=fields.Html, nohide=True) def geogebra(self): """Get HTML code of embedded YouTube video""" url = self.url.split("/")[-1] url = f"https://www.geogebra.org/material/iframe/id/{url}" url += f"/width/{self.width}/height/{self.height}" url += "/ai/false/smb/false/stb/false" return f"""
class Widget(form.Form): a = fields.RandomNumber("a") b = fields.RandomNumber("b") field = fields.Field("Field") @fields.constraint("Constraint") def constraint(self): return self.a == self.b @fields.range_constraint("Range constraint") def range_constraint(self): self.a = sympy.symbols("a b c") def generator(self): self.field = sympy.Eq(self.a, self.b, evaluate=False)
class Youtube(form.Form): """YouTube""" name = "YouTube" url = fields.Field("URL") template = """ <div v-if="config.edit"> `url` </div> `video` """ @fields.computed("Youtube Video", field=fields.Html, nohide=True) def video(self): """Get HTML code of embedded YouTube video""" url = self.url.replace("watch?v=", "embed/") return f"""
class Image(form.Form): """Image""" src = fields.Field("Image URL", required=True) height = fields.Expression("Original height", default=0) width = fields.Expression("Original width", default=0) zoom = fields.Expression("Zoom", default=1) position = fields.Select("Position", options=["left", "right", "center"], default="center") template = """ <widget-settings v-if="config.edit"> <config-option name="Zoom">`zoom`</config-option> <config-option name="Position">`position`</config-option> </widget-settings> `image` """ @fields.computed("Image", field=fields.Html, nohide=True) def image(self): style = f"float:{self.position}" if self.position != "center" else "" return f""" <img src="{self.src}" width="{self.zoom * self.width}" style="{style}" height="{self.zoom * self.height}"/>""" def validate(self): if "," in self.src: data = self.src.split(",") image_hash = hashlib.sha1(data[1].encode("utf-8")).hexdigest() ext = mimetypes.guess_extension(data[0][5:data[0].find(";")]) folder = os.environ.get("STORAGE", "./storage/") filename = image_hash + ext with open(folder + filename, "wb") as f: image = base64.b64decode(data[1]) f.write(image) self.src = "/storage/" + filename if not self.width: path = os.environ.get("STORAGE", "./storage/") + self.src[9:] self.width, self.height = PIL.Image.open(path).size self.zoom = 1
class Image(form.Form): """Image""" src = fields.Field("Image URL", required=True) height = fields.Expression("Original height", default=0) width = fields.Expression("Original width", default=0) zoom = fields.Expression("Zoom", default=1) template = """ <div v-if="config.edit"> Zoom: `zoom` </div> <div> `image` </div> """ @fields.computed("Image", field=fields.Html, nohide=True) def image(self): return (f"""<img src="{self.src}" """ + f"""width="{self.zoom * self.width}" """ + f"""height="{self.zoom * self.height}"/>""") def validate(self): if "," in self.src: data = self.src.split(",") image_hash = hashlib.sha1(data[1].encode("utf-8")).hexdigest() ext = mimetypes.guess_extension(data[0][5:data[0].find(";")]) folder = os.environ.get("STORAGE", "./storage/") filename = image_hash + ext with open(folder + filename, "wb") as f: image = base64.b64decode(data[1]) f.write(image) self.src = "/storage/" + filename if not self.width: path = os.environ.get("STORAGE", "./storage/") + self.src[9:] self.width, self.height = PIL.Image.open(path).size self.zoom = 1
class StandardFormQuestion(form.Form): """Standard form question""" name = fields.Field("Survey name") answer = fields.Expression("Your answer", nosave=True, editable=True) expression = fields.Expression("Expression") template = """ <p>Convert `expression` to standard form</p> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> `answer` </survey> """ @fields.computed("Correct", field=fields.Boolean) def correct(self): if not self.answer: return False if self.answer.func == sympy.Pow: if self.answer.args[0] != 10: return False else: if self.answer.func != sympy.Mul or len(self.answer.args) != 2: return False x, power = self.answer.args[0], self.answer.args[1] if (x not in sympy.Interval.Ropen(1, 10) or power.func != sympy.Pow or power.args[0] != 10): return False return sympy.simplify(self.answer - self.expression) == 0 def validate(self): if not self.name: self.name = str(uuid.uuid1())
class Triangle(form.Form): """Draw a triangle""" template = """ Vertices labels: `A`, `B`, `C`<br> Lengths: `a`, `b`, `c`<br> Angles: `alpha`, `beta`, `gamma`<br> """ A = fields.Expression("A") B = fields.Expression("B") C = fields.Expression("C") a = fields.Expression("a") b = fields.Expression("b") c = fields.Expression("c") alpha = fields.Expression("alpha") beta = fields.Expression("beta") gamma = fields.Expression("gamma") obtuse = fields.Field("Obtuse (sine law ambiguity)", default=False) @fields.computed("Triangle", field=fields.Html) @fields.figure def triangle(self): # Prepare figure pyplot.axis("off") pyplot.grid(b=None) pyplot.gca().set_aspect("equal") # Draw triangle x, y = zip(*(self.vertices + [numpy.array([0, 0])])) pyplot.plot(x, y) # Labelling m = numpy.sum(self.vertices, axis=0) / 3 edges = zip(self.vertices, self.vertices[1:] + self.vertices[:1]) positions = (self.vertices + [numpy.sum(e, axis=0) / 2 for e in edges] + self.vertices) labels = [ getattr(self, attr) for attr in ["A", "B", "C", "a", "b", "c", "beta", "gamma", "alpha"] ] signs = [1 if i < 6 else -1 for i in range(9)] for label, position, sign in zip(labels, positions, signs): if label: label = fr"${sympy.latex(label)}$" direction = (position - m) / numpy.linalg.norm(position - m) pyplot.text(*(position + 0.3 * sign * direction), label, fontsize=13) @property def vertices(self): lengths = [self.a, self.b, self.c] angles = [self.alpha, self.beta, self.gamma] def missing(expressions): return [ i for i, e in enumerate(expressions) if not e or getattr(e, "func", "") == sympy.Symbol ] missing_quantities = len(missing(lengths + angles)) def cosine_law(lengths, angles, index): (c, a, b), gamma = lengths[index:] + lengths[:index], angles[index] x, find_angle = sympy.Dummy("x"), not bool(gamma) gamma, c = gamma or x, c or x equation = sympy.Eq(c**2, a**2 + b**2 - 2 * a * b * sympy.cos(gamma)) domain = sympy.Interval.open(0, sympy.pi if find_angle else sympy.oo) sol = [x for x in sympy.solve(equation) if x in domain] if not sol: raise ValueError( "The lengths and angles must satisfy the law of cosines") locals()["angles" if find_angle else "lengths"][index] = sol[0] def sine_law(lengths, angles, i, j): a, b, alpha, beta = lengths[i], lengths[j], angles[i], angles[j] x, find_angle = sympy.Dummy("x"), not bool(beta) b, beta = b or x, beta or x equation = sympy.Eq(sympy.sin(alpha) / a, sympy.sin(beta) / b) domain = sympy.Interval.open(0, sympy.pi if find_angle else sympy.oo) sol = [x for x in sympy.solve(equation) if x in domain] if not sol: raise ValueError( "The lengths and angles must satisfy the law of sines") sol = [ x for x in sol if x + sum([a for a in angles if a and not a.free_symbols]) < sympy.pi ] sol = sol[1] if self.obtuse and len(sol) == 2 else sol[0] locals()["angles" if find_angle else "lengths"][j] = sol while missing_quantities: missing_info = [len(missing(el)) for el in zip(lengths, angles)] if len(missing(angles)) == 1: angle = sympy.pi - sum( [a for a in angles if a and not a.free_symbols]) if angle not in sympy.Interval.open(0, sympy.pi): raise ValueError( "The angles sum cannot exceed 180 degrees") angles[missing(angles)[0]] = angle elif not missing(lengths) and missing(angles): cosine_law(lengths, angles, missing(angles)[0]) elif len(missing(lengths)) == 1 and missing( lengths)[0] not in missing(angles): cosine_law(lengths, angles, missing(lengths)[0]) elif {0, 1} <= set(missing_info): sine_law(lengths, angles, missing_info.index(0), missing_info.index(1)) else: raise ValueError("Not enough information to find the vertices") missing_quantities -= 1 equations = [ sympy.Eq( (lengths[2] * sympy.cos(angles[1])).evalf(5), (lengths[0] - lengths[1] * sympy.cos(angles[2])).evalf(5), ), sympy.Eq( (lengths[2] * sympy.sin(angles[1])).evalf(5), (lengths[1] * sympy.sin(angles[2])).evalf(5), ), sympy.Eq(sum(angles).evalf(5), sympy.pi.evalf(5)), ] if not all(equations): raise ValueError( "There are no triangles satisfying the conditions") if self.obtuse and not [a for a in angles if a > sympy.pi / 2]: raise ValueError("Could not find an appropriate obtuse triangle") return [ numpy.array([0, 0]), numpy.array([lengths[0].evalf(), 0], dtype=numpy.float64), numpy.array( [ (lengths[2] * sympy.cos(angles[1])).evalf(), (lengths[2] * sympy.sin(angles[1])).evalf(), ], dtype=numpy.float64, ), ]
class MultipleChoice(form.Form): """Multiple Choice""" name = fields.Field("Survey name") answer = fields.Select("Your answer", nosave=True, options=["", "A", "B", "C", "D", "E"]) correct_answer = fields.Select("Correct Answer", options=["A", "B", "C", "D", "E"], default="A") question = fields.Markdown("Question") option_a = fields.Markdown("Option A") option_b = fields.Markdown("Option B") option_c = fields.Markdown("Option C") option_d = fields.Markdown("Option D") option_e = fields.Markdown("Option E") @property def template(self): buttons = [] for ltr in ["a", "b", "c", "d", "e"]: buttons.append(f""" <span v-if="!config.edit"> <span v-if="payload.option_{ltr}"> <b-button @click="$set(payload, 'answer', '{ltr.upper()}')" type="is-primary" v-if="'{ltr.upper()}' === payload.answer"> `option_{ltr}` </b-button> <b-button @click="$set(payload, 'answer', '{ltr.upper()}')" v-else> `option_{ltr}` </b-button> </span> </span> <span v-else> <b-button>`option_{ltr}`</b-button> </span> """) return f""" <p v-if="config.edit || payload.question">`question`</p> <widget-settings v-if="config.edit"> <config-option name="Correct Answer">`correct_answer`</config-option> </widget-settings> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :max-attempts="1" :value="payload.answer"> <span class="buttons are-large"> {"".join(buttons)} </span> </survey> """ @property def _marks(self): return 1 if self.correct else 0 @fields.computed("Correct", field=fields.Boolean) def correct(self): return self.answer == self.correct_answer def validate(self): if not self.name: self.name = str(uuid.uuid1())
class Identity(Model): _collection = "identity" username = fields.Field("Username") nickname = fields.Field("Nickname")
class Form(Model): _collection = "forms" email = fields.Field("Email adress") url = fields.Field("Resource URL")
class Equation(form.Form): """Equation""" name = fields.Field("Survey name") answer = fields.ExpressionList("Your answer", nosave=True, editable=True) equation = fields.Equation("Equation", required=True) x = fields.Expression("Solve for", default="x", required=True) template = """ Solve `equation` <span v-if="config.edit || payload.x !== 'x'">for `x`</span> <div v-if="!payload.marked_question">`solution`</div> <div v-else> <survey :config="config" :name="payload.name" :showStats="config.authState.loggedIn" :correct="computed.correct" :value="payload.answer"> Solution(s): `x` = `answer` </survey> </div> <div v-if="config.edit"> `show_graph` `marked_question` </div> <div v-if="payload.show_graph">`graph`</div> """ show_graph = fields.Boolean("Show graph", default=False) marked_question = fields.Boolean("Marked question", default=False) h = fields.Expression("h", default="3") @fields.computed("Correct", field=fields.Boolean) def correct(self): if not self.answer: return False # Prevent user from just copying the equation for sol in self.answer: if len(sol.atoms(sympy.Symbol)): return False solution = sympy.solveset(self.equation, self.x) sol_count = len(solution.args) return len(solution.intersect( sympy.FiniteSet(*self.answer)).args) >= sol_count @fields.computed("Solution") def solution(self): answer = sympy.solveset(self.equation, self.x) if answer.func == sympy.FiniteSet and len(answer.args) <= 3: answer = [ sympy.Eq(self.x, answer.args[i]) for i in range(len(answer.args)) ] return answer @fields.computed("Plot", field=fields.Html, nohide=True) def graph(self): if not self.show_graph: return "" args = {"functions": [self.equation.args[0], self.equation.args[1]]} if isinstance(self.solution, list) and len(self.solution) in [1, 2]: if len(self.solution) == 1: solution = self.solution[0].args[1] if len(self.solution) == 2: x1, x2 = self.solution[0].args[1], self.solution[1].args[1] solution = (x1 + x2) / 2 self.h = x2 - solution + 2 args.update({ "x_min": solution - self.h, "x_max": solution + self.h }) return plot.Plot(**args).plot def validate(self): if self.marked_question: if self.show_graph: self.show_graph = False if not self.name: self.name = str(uuid.uuid1())