Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cc054d5
show threads (but not send messages to them yet)
VorontsovIE Jun 22, 2019
ea04a51
finalize message exchange
VorontsovIE Jun 22, 2019
990f41f
update score; ignore empty messages; submit on ctrl+enter
VorontsovIE Jun 22, 2019
13baf45
reuse require_teacher
VorontsovIE Jun 22, 2019
35e74c2
prepods can see who gave an answer in a thread
VorontsovIE Jun 23, 2019
20faf86
fix bug with order field name in a template
VorontsovIE Jun 23, 2019
de4b477
Task description is now rendered in a dialogue. Both task and mesages…
VorontsovIE Jun 23, 2019
ca28b1e
Update ennead/utils.py
VorontsovIE Jun 23, 2019
d66ac3f
shorten thread urls
VorontsovIE Jun 23, 2019
767bd57
refactor and merge endpoints for thread
VorontsovIE Jun 25, 2019
7ba0ef6
added solution to a task
VorontsovIE Jun 25, 2019
c630d84
more explicity about conditions to change score
VorontsovIE Jun 25, 2019
3cf4e69
refine onload callback registration
VorontsovIE Jun 25, 2019
133fe64
teacher messages are now left-aligned, but message containers have di…
VorontsovIE Jun 26, 2019
beac920
rename Task.solution_html_description --> html_solution, Post.html_de…
VorontsovIE Jun 27, 2019
8716cc7
post_klass --> post_class
VorontsovIE Jun 27, 2019
dfab954
canonical jinja doesnt want colon in statements
VorontsovIE Jun 27, 2019
2a3c10f
use number input field for score
VorontsovIE Jun 27, 2019
fd43611
indentation tabs to 4 spaces
VorontsovIE Jun 27, 2019
2104d7d
export require_student from utils
VorontsovIE Jun 27, 2019
5fd4009
change fixtures as now markdown already has newline-to-break extension
VorontsovIE Jun 27, 2019
eac25bc
app.js code moved to editor.js
VorontsovIE Jun 27, 2019
aec51c0
incorporate super-sexy-markdown-editor
VorontsovIE Jun 27, 2019
eaffa6b
use recursive mkdir when creating folders for files
VorontsovIE Jun 27, 2019
d876403
improve style for dialog
VorontsovIE Jun 27, 2019
415ff77
Update ennead/models/task.py
VorontsovIE Jun 27, 2019
0f4c68c
Update ennead/views/dialogue.py
VorontsovIE Jun 27, 2019
6e25e30
create File table in populate-db
VorontsovIE Jun 27, 2019
e31d5d2
treat markdown always correct
VorontsovIE Jun 27, 2019
c9990fa
fix docstrings for dialog
VorontsovIE Jun 27, 2019
9ca3918
PEPify
VorontsovIE Jun 27, 2019
4ca754e
Send message about score change when score is changed
VorontsovIE Jun 27, 2019
986369c
Restore message in a form if it's incorrect.
VorontsovIE Jun 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ venv.bak/
# Rope project settings
.ropeproject

# Sublime project settings
*.sublime-project
*.sublime-workspace

# mkdocs documentation
/site

Expand Down
5 changes: 5 additions & 0 deletions ennead/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ennead.views.admin import adm_task_list_page, task_edit_page, task_edit, task_delete
from ennead.views.system import render_markdown_endpoint
from ennead.views.file import upload_file, uploaded_file, files_page
from ennead.views.dialogue import thread_page, post_to_thread

from ennead.models.base import database
from ennead.models.file import File
Expand Down Expand Up @@ -63,6 +64,10 @@ def create_app(config_path: Optional[str] = None) -> Flask:
app.add_url_rule('/login', 'login', login, methods=['POST'])
app.add_url_rule('/logout', 'logout', logout)

app.add_url_rule('/thread/<int:task_id>/<int:student_id>', 'thread', thread_page)
app.add_url_rule('/thread/<int:task_id>/<int:student_id>',
'post_to_thread', post_to_thread, methods=['POST'])

app.add_url_rule('/adm/tasks', 'adm_task_list_page', adm_task_list_page)
app.add_url_rule('/adm/tasks/<int:task_id>', 'task_edit_page', task_edit_page)
app.add_url_rule('/adm/tasks', 'task_edit', task_edit, methods=['POST'])
Expand Down
2 changes: 1 addition & 1 deletion ennead/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def from_data(cls, directory: str, name: str, data: bytes, user: User) -> 'File'
file_entry.save()

dir_path = os.path.join(os.path.realpath(directory), file_entry.token)
os.mkdir(dir_path)
os.makedirs(dir_path)
full_path = os.path.normpath(os.path.join(dir_path, name))
if not full_path.startswith(dir_path + os.sep):
raise FileCreationError(f"{name} doesn't seems like correct file name")
Expand Down
14 changes: 14 additions & 0 deletions ennead/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,29 @@ class TaskSet(BaseModel):
tasks: List['Task']
threads: List['Thread']

@property
def ordered_tasks(self) -> List["Task"]:
return sorted(self.tasks, key=lambda task: task.order_num)


class Task(BaseModel):
"""One task for student

Attributes:
name: human-readable name of `Task`
description: `Task` description in Markdown
solution: correct `Task` solution and comments about work review process
base_score: basic maximal score for `Task`
task_set: set this `Task` belongs to
order_num: order of this `Task` in a `TaskSet`
threads: list of `Thread`s about this `Task`
"""

name: str = CharField()
description: str = TextField()
solution: str = TextField()
base_score: int = IntegerField()
order_num: int = IntegerField()
task_set: TaskSet = ForeignKeyField(TaskSet, backref='tasks')

threads: List['Thread']
Expand All @@ -51,3 +59,9 @@ def html_description(self):
"""`Task` description in HTML"""

return render_markdown(self.description)

@property
def html_solution(self):
"""`Task` solution in HTML"""

return render_markdown(self.solution)
21 changes: 19 additions & 2 deletions ennead/models/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import datetime
from typing import List

from peewee import DateTimeField, IntegerField, TextField, ForeignKeyField
from peewee import DateTimeField, DecimalField, TextField, BooleanField, ForeignKeyField

from ennead.models.user import User
from ennead.models.task import Task
from ennead.models.base import BaseModel

from ennead.utils import render_markdown


class Thread(BaseModel):
"""Student-with-teachers chat about `Task`
Expand All @@ -21,11 +23,18 @@ class Thread(BaseModel):
"""

task: Task = ForeignKeyField(Task, backref='threads')
score: int = IntegerField()
score: float = DecimalField(default=0)
student: User = ForeignKeyField(User, backref='threads')

posts: List['Post']

def ordered_posts(self, show_hidden=False):
posts = self.posts
if not show_hidden:
posts = filter(lambda post: not post.hide_from_student, posts)
posts = sorted(posts, key=lambda post: post.date)
return posts


class Post(BaseModel):
"""One post in `Thread` with `User` about `Task`
Expand All @@ -35,9 +44,17 @@ class Post(BaseModel):
date: date this `Post` was posted
author: `User` who wrote this post
thread: `Thread` this task belongs to
hide_from_student: marks `Post` as auxiliary (for teacher's use only)
"""

text: str = TextField()
date: datetime.datetime = DateTimeField()
author: User = ForeignKeyField(User, backref='+') # '+' means 'no backref'
hide_from_student: bool = BooleanField(default=False)
thread: Thread = ForeignKeyField(Thread, backref='posts')

@property
def html_text(self):
"""`Post` text in HTML"""

return render_markdown(self.text)
8 changes: 7 additions & 1 deletion ennead/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,20 @@ def check_password(self, password: str) -> bool:
self.password_sha512
)

@property
def is_student(self) -> bool:
"""Check is user a student"""

return self.group == UserGroup.student

@property
def is_teacher(self) -> bool:
"""Check is user a teacher"""

return self.group == UserGroup.teacher

@property
def score(self) -> int:
def score(self) -> float:
"""Get `User`s score in current task set"""

return sum(
Expand Down
166 changes: 90 additions & 76 deletions ennead/static/editor.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,101 @@
window.addEventListener('load', function() {
var editors = document.getElementsByClassName('markdown-editor');
for (let editor of editors) {
var sourceLink = editor.getElementsByClassName('markdown-editor-source-link')[0];
var previewLink = editor.getElementsByClassName('markdown-editor-preview-link')[0];
var editorTextarea = editor.getElementsByClassName('markdown-editor-source')[0];
var preview = editor.getElementsByClassName('markdown-editor-preview')[0];
var fileUploader = editor.getElementsByClassName('file-uploader')[0];
if (fileUploader === undefined) {
continue;
}
var fileUploaderInput = fileUploader.getElementsByClassName('file-uploader-input')[0];
var fileUploaderLink = fileUploader.getElementsByClassName('file-uploader-link')[0];
if (
sourceLink === undefined ||
previewLink === undefined ||
editorTextarea === undefined ||
preview === undefined ||
fileUploaderInput === undefined ||
fileUploaderLink === undefined
) {
continue;
}
var editors = document.getElementsByClassName('markdown-editor');
for (let editor of editors) {
var sourceLink = editor.getElementsByClassName('markdown-editor-source-link')[0];
var previewLink = editor.getElementsByClassName('markdown-editor-preview-link')[0];
var editorTextarea = editor.getElementsByClassName('markdown-editor-source')[0];
var preview = editor.getElementsByClassName('markdown-editor-preview')[0];
var fileUploader = editor.getElementsByClassName('file-uploader')[0];
if (fileUploader === undefined) {
continue;
}
var fileUploaderInput = fileUploader.getElementsByClassName('file-uploader-input')[0];
var fileUploaderLink = fileUploader.getElementsByClassName('file-uploader-link')[0];
if (
sourceLink === undefined ||
previewLink === undefined ||
editorTextarea === undefined ||
preview === undefined ||
fileUploaderInput === undefined ||
fileUploaderLink === undefined
) {
continue;
}

sourceLink.onclick = function() {
if (editorTextarea.classList.contains('d-none')) {
editorTextarea.classList.remove('d-none');
preview.classList.add('d-none');
sourceLink.classList.add('active');
previewLink.classList.remove('active');
fileUploader.classList.remove('d-none');
}
}
sourceLink.onclick = function() {
if (editorTextarea.classList.contains('d-none')) {
editorTextarea.classList.remove('d-none');
preview.classList.add('d-none');
sourceLink.classList.add('active');
previewLink.classList.remove('active');
fileUploader.classList.remove('d-none');
}
}

previewLink.onclick = function() {
if (preview.classList.contains('d-none')) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
preview.innerHTML = xhr.responseText;
preview.querySelectorAll('pre code').forEach(function(block) {
hljs.highlightBlock(block);
});
MathJax.Hub.Queue(["Typeset", MathJax.Hub, preview]);
preview.classList.remove('d-none');
editorTextarea.classList.add('d-none');
previewLink.classList.add('active');
sourceLink.classList.remove('active');
fileUploader.classList.add('d-none');
}
previewLink.onclick = function() {
if (preview.classList.contains('d-none')) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
preview.innerHTML = xhr.responseText;
preview.querySelectorAll('pre code').forEach(function(block) {
hljs.highlightBlock(block);
});
MathJax.Hub.Queue(["Typeset", MathJax.Hub, preview]);
preview.classList.remove('d-none');
editorTextarea.classList.add('d-none');
previewLink.classList.add('active');
sourceLink.classList.remove('active');
fileUploader.classList.add('d-none');
}
}
xhr.open('POST', '/md', true);
xhr.send(editorTextarea.value);
}
}
xhr.open('POST', '/md', true);
xhr.send(editorTextarea.value);
}
}

fileUploaderLink.onclick = function() {
fileUploaderInput.click();
}
fileUploaderLink.onclick = function() {
fileUploaderInput.click();
}

fileUploaderInput.onchange = function() {
var file = fileUploaderInput.files[0];
if (file === undefined) {
return;
}
fileUploaderInput.onchange = function() {
var file = fileUploaderInput.files[0];
if (file === undefined) {
return;
}

var formData = new FormData();
formData.append('file', file);
var formData = new FormData();
formData.append('file', file);

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var fileLink = '[' + file.name + '](' + xhr.responseText + ')';
if (file.type.startsWith('image/')) {
fileLink = '!' + fileLink;
}
if (editorTextarea.value.slice(-1) != '\n') {
editorTextarea.value += '\n';
}
editorTextarea.value += fileLink;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var fileLink = '[' + file.name + '](' + xhr.responseText + ')';
if (file.type.startsWith('image/')) {
fileLink = '!' + fileLink;
}
if (editorTextarea.value.slice(-1) != '\n') {
editorTextarea.value += '\n';
}
editorTextarea.value += fileLink;
}
};
xhr.open('POST', '/upload', true);
xhr.send(formData);
}
}
});

window.addEventListener("load", function() {
let forms = document.getElementsByTagName('form');
for (let form of forms) {
let textareas = form.getElementsByTagName('textarea');
for (let textarea of textareas) {
textarea.addEventListener('keydown', function(event) {
if (event.ctrlKey && event.keyCode == 13) { // Ctrl-Enter pressed
form.submit();
}
});
}
};
xhr.open('POST', '/upload', true);
xhr.send(formData);
}
}
});
Loading