diff --git a/data/proof_to_quests.py b/data/answer.py similarity index 79% rename from data/proof_to_quests.py rename to data/answer.py index f601159..c15bf84 100644 --- a/data/proof_to_quests.py +++ b/data/answer.py @@ -5,12 +5,11 @@ from datetime import datetime from .db_session import SqlAlchemyBase -class Proofs(SqlAlchemyBase, UserMixin): - __tablename__ = 'proofs' +class Answer(SqlAlchemyBase, UserMixin): + __tablename__ = 'answer' id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True) quest = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey("quests.id"), nullable=True, default=None) - file = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey("files.id"), nullable=True, default=None) text = sqlalchemy.Column(sqlalchemy.Text, nullable=True, default=None) creator = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey("users.id"), nullable=True, default=None) diff --git a/data/proof_file.py b/data/proof_file.py new file mode 100644 index 0000000..bebec26 --- /dev/null +++ b/data/proof_file.py @@ -0,0 +1,12 @@ +import sqlalchemy +from flask_login import UserMixin +from datetime import datetime + +from .db_session import SqlAlchemyBase + + +class FileProof(SqlAlchemyBase, UserMixin): + __tablename__ = 'file_proof' + id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True) + answer = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey("answer.id"), nullable=True, default=None) + file = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey("files.id"), nullable=True, default=None) diff --git a/forms/task.py b/forms/task.py index e6c964e..90acf4b 100644 --- a/forms/task.py +++ b/forms/task.py @@ -1,5 +1,6 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, TextAreaField, DateField, TimeField, FileField +from wtforms import StringField, SubmitField, TextAreaField, DateField, TimeField, MultipleFileField, \ + BooleanField from wtforms.validators import DataRequired @@ -13,5 +14,8 @@ class NewTask(FlaskForm): class AnswerTask(FlaskForm): text = TextAreaField('Письменный ответ') - file = FileField('Файловый ответ') - submit = SubmitField('Ответить') + file = MultipleFileField('Файловый ответ') + realized = BooleanField('Задача решена') + deadline_date = DateField('Дедлайн', validators=[DataRequired()]) + deadline_time = TimeField('', validators=[DataRequired()]) + submit = SubmitField('Сохранить') diff --git a/functions.py b/functions.py index 4f8630b..f489def 100644 --- a/functions.py +++ b/functions.py @@ -1,10 +1,13 @@ import datetime +import os import smtplib from json import loads from email.message import EmailMessage from data.roles import Roles from data.users import User from data.staff_projects import StaffProjects +from data.answer import Answer +from data.files import Files from data import db_session import uuid import pymorphy2 @@ -95,12 +98,14 @@ def save_project_logo(photo): def overdue_quest_project(quest): - if str(quest.deadline.date()) == str(datetime.datetime.now().date()): + if quest.deadline is None: + quest.overdue = '' + elif str(quest.deadline.date()) == str(datetime.datetime.now().date()): quest.overdue = 'today' elif quest.deadline < datetime.datetime.now(): quest.overdue = 'yes' quest.time_left = 'Просрочено на' + round_date(quest.deadline) - else: + elif quest.deadline > datetime.datetime.now(): quest.overdue = 'no' quest.time_left = 'Еще есть: ' + round_date(quest.deadline) return quest @@ -120,3 +125,31 @@ def round_date(date_time): if difference: resp += ', ' if resp else ' ' + f'{difference} {morph.parse("день")[0].make_agree_with_number(difference).word}' return f'{resp}' + + +def save_proof_quest(project, file, user_id): + data_session = db_session.create_session() + path = f'static/app_files/all_projects/{str(project.id)}/{str(file.filename)}' + file_check = data_session.query(Files).filter(Files.path == path).first() + file.save(path) + if file_check: + return file_check.id + file = Files( + path=path, + user=user_id, + up_date=datetime.datetime.now() + ) + data_session.add(file) + data_session.flush() + data_session.refresh(file) + file_id = file.id + data_session.commit() + data_session.close() + return file_id + + +def find_files_answer(file_id): + data_session = db_session.create_session() + file = data_session.query(Files).filter(Files.id == file_id).first() + return {'id': file.id, 'path': file.path, 'user': file.user, 'up_date': file.up_date, + 'current_path': file.path[str(file.path).find('all_projects') + 13:].split('/')} diff --git a/main.py b/main.py index 10b6a01..6802361 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from sqlalchemy import or_ from json import loads from functions import check_password, mail, init_db_default, get_projects_data, get_user_data, save_project_logo, \ - overdue_quest_project + overdue_quest_project, save_proof_quest, find_files_answer from forms.edit_profile import EditProfileForm from forms.login import LoginForm from forms.find_project import FindProjectForm @@ -24,6 +24,8 @@ from forms.task import NewTask, AnswerTask from data.users import User from data.quests import Quests +from data.answer import Answer +from data.proof_file import FileProof from data.files import Files from data.projects import Projects from data.staff_projects import StaffProjects @@ -50,7 +52,35 @@ def base(): return redirect('/projects') -@app.route('/project//quest/') +@app.route('/project//file//delete') +def delete_file(id_project, id_file): + if current_user.is_authenticated: + data_session = db_session.create_session() + current_project = data_session.query(Projects).filter(Projects.id == id_project).first() + current_file = data_session.query(Files).filter(Files.id == id_file).first() + if current_project and current_file: + if current_user.id in map(lambda x: x[0], data_session.query(StaffProjects.user).filter( + StaffProjects.project == current_project.id).all()) or current_user.id == current_project.creator: + current_proof = data_session.query(FileProof).filter(FileProof.file == id_file).all() + os.remove(current_file.path) + data_session.delete(current_file) + if current_proof: + quest = data_session.query(Answer.quest).filter(Answer.id == current_proof[0].answer).first() + for i in current_proof: + data_session.delete(i) + data_session.commit() + return redirect(f'/project/{current_project.id}/quest/{quest[0]}') + data_session.commit() + return redirect(f'/project/{current_project.id}') + else: + abort(403) + else: + abort(404) + else: + return redirect('/login') + + +@app.route('/project//quest/', methods=['GET', 'POST']) def task_project(id_project, id_task): if current_user.is_authenticated: data_session = db_session.create_session() @@ -58,15 +88,75 @@ def task_project(id_project, id_task): current_task = data_session.query(Quests).filter(Quests.id == id_task).first() if current_project and current_task and current_task.project == current_project.id: form = AnswerTask() - return render_template('decision.html', title='Решение', project=current_project, task=current_task, - form=form) + current_answer = data_session.query(Answer).filter(Answer.quest == current_task.id).first() + list_files = None + if form.validate_on_submit(): + if form.deadline_date.data and form.deadline_time.data: + deadline = datetime.datetime.combine(form.deadline_date.data, form.deadline_time.data) + else: + deadline = None + current_task.deadline = deadline + if current_answer: + current_answer.text = form.text.data if form.text.data else None + current_answer.date_edit = datetime.datetime.now() + current_task.realized = form.realized.data + data_session.commit() + if form.file.data[0].filename: + files = list( + map(lambda x: save_proof_quest(current_project, x, current_user.id), form.file.data)) + for i in files: + if not data_session.query(FileProof).filter(FileProof.answer == current_answer.id, + FileProof.file == i).first(): + proof_file = FileProof( + answer=current_answer.id, + file=i + ) + data_session.add(proof_file) + data_session.commit() + else: + if form.file.data[0].filename: + files = list( + map(lambda x: save_proof_quest(current_project, x, current_user.id), form.file.data)) + else: + files = False + current_task.realized = form.realized.data + current_answer = Answer( + quest=current_task.id, + text=form.text.data if form.text.data else None, + creator=current_user.id, + date_create=datetime.datetime.now(), + date_edit=datetime.datetime.now() + ) + data_session.add(current_answer) + data_session.flush() + data_session.refresh(current_answer) + if files: + for i in files: + proof_file = FileProof( + proof=current_answer.id, + file=i + ) + data_session.add(proof_file) + data_session.commit() + return redirect(f'/project/{current_project.id}') + if current_answer: + form.text.data = current_answer.text + form.realized.data = current_task.realized + files = data_session.query(FileProof).filter(FileProof.answer == current_answer.id).all() + if files: + list_files = list(map(lambda x: find_files_answer(x.file), files)) + if current_task.deadline and current_task.deadline: + form.deadline_date.data = current_task.deadline.date() + form.deadline_time.data = current_task.deadline.time() + return render_template('answer.html', title='Решение', project=current_project, task=current_task, + form=form, list_files=list_files) else: abort(404) else: return redirect('/login') -@app.route('/project//task/new', methods=['GET', 'POST']) +@app.route('/project//quest/new', methods=['GET', 'POST']) def new_task_project(id_project): if current_user.is_authenticated: data_session = db_session.create_session() @@ -166,7 +256,11 @@ def project(id_project): User.id.in_(list(map(lambda x: x.user, staff)))).all())) if staff else [] quests = data_session.query(Quests).filter(Quests.project == current_project.id).all() if quests: - quests.sort(key=lambda x: (x.realized, x.deadline)) + quests_sort = sorted(list(filter(lambda x: x.deadline is not None, quests)), + key=lambda x: (x.realized, x.deadline)) + quests = list(filter(lambda x: x.realized == 0, quests_sort)) + list( + filter(lambda x: x.deadline is None, quests)) + list( + filter(lambda x: x.realized == 1, quests_sort)) quests = list(map(lambda x: overdue_quest_project(x), quests)) return render_template('project.html', project=current_project, diff --git a/static/app_files/all_projects/заглушка b/static/app_files/all_projects/заглушка new file mode 100644 index 0000000..e69de29 diff --git a/static/css/answer.css b/static/css/answer.css new file mode 100644 index 0000000..29c54c3 --- /dev/null +++ b/static/css/answer.css @@ -0,0 +1,243 @@ +body { + background-color: #dcb495 !important; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.decision_page { + background-color: #dcb495; + min-height: 100vw; + height: auto; + display: flex; + flex-direction: column; + align-items: center; + margin: 3vw; + margin-bottom: 20vw; +} +.link_back_block { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + flex-wrap: nowrap; +} +.link_back { + background-color: #ffffff; + color: #000000; + width: 15vw; + height: 4.5vw; + vertical-align: middle; + border-radius: 5vw; + display: flex; + align-items: center; + justify-content: center; +} +.link_back:hover { + text-decoration: none; + color: #000000; +} +.link_back_text { + font-size: 1.5vw; + margin-top: 15px; + display: flex; + align-items: center; + justify-content: center; +} +.name_block { + margin-top: 3vw; + width: 90%; + height: auto; + display: flex; + flex-direction: column; + align-items: center; +} +.title_block { + width: 90%; + display: flex; + justify-content: center; +} +.title_task, .files_title { + text-align: center; + color: #000000; + font-size: 4vw; +} +.description_task { + width: 80%; + background-color: #EDCBB0; + height: auto; + max-height: 15vw; + border-radius: 2vw; + display: flex; + overflow-y: auto; +} +.description_task::-webkit-scrollbar { + width: 0.8vw !important; + height: auto; +} +.description_task::-webkit-scrollbar-thumb { + background-color: #d49d51 !important; /* цвет плашки */ + border-radius: 5vw !important; /* закругления плашки */ + border: 0.25vw solid #ffffff !important; +} +.description { + margin: 15px; +} +.description_text { + font-size: 1.5vw; + text-align: justify; +} +.data_block { + width: 100%; + display: flex; + align-items: flex-start; + justify-content: center +} +.bottom_data { + margin: 2vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.form_label { + margin-top: 10px; + font-size: 1.3vw; + color: #000000; + font-weight: bold; +} +.input_data { + color: #000000; + border: 0.1vw solid #595008; + height: 4.5vw; + min-height: 4.5vw; + width: 30vw; + background-color: #dbc3af; + border-radius: 5vw; + font-size: 1.3vw; + display: inline-flex; + align-items: center; +} +.input_button { + width: 10vw; + height: 5vw; + border-radius: 5vw; + vertical-align: middle; +} +.form_data { + display: flex; + flex-direction: column; + margin-left: 2%; +} +.decision_block { + margin-top: 3vw; + width: 90%; + height: 25vw; + display: flex; + flex-direction: column; + align-items: center; +} +.padding_data { + padding: 1vw; +} +.quest_button { + color: #ffffff; + width: 13vw; + height: 5vw; + background-color: #000000; + border: 2px solid #ffffff; + border-radius: 3vw; + margin-left: 2vw; +} +form { + display: flex; + flex-direction: column; + align-items: center; +} +.form_data_button { + margin-top: 20px; + display: flex; + width: 30vw; + align-items: center; + justify-content: space-between; +} +.deadline { + margin-top: 5px; +} +.text_data { + width: 80%; + border-radius: 2vw !important; + min-height: 10vw; + max-height: 20vw; +} +.form_text_one { + width: 100%; +} +.files_block { + width: 100%; + margin: 2vw; + background-color: #dbc3af; + display: flex; + flex-direction: column; + align-items: stretch; + border-radius: 2vw; + min-height: 25vw; +} +.files_list { + margin: 2vw; + height: auto; + overflow-y: auto; + overflow-x: hidden; +} +.files { + width: 80%; + margin: 2vw; + display: flex; + flex-direction: column; + align-items: center; + min-height: 25vw; + max-height: 30vw; +} +.file { + width: 98%; + display: flex; + background-color: #694a2d; + margin: 0.5vw; + align-items: center; + justify-content: space-between; + flex-direction: row; + height: 4.5vw; + border-radius: 2vw; +} +.file_head { + width: 30vw; + margin-left: 1vw; + height: 4vw; + background-color: #694a2d !important; + overflow-y: hidden; + overflow-x: auto; +} +.file_head_path, .file_path { + font-size: 1.5vw; + color: #ffffff !important; + font-weight: bold; + height: 3vw; + display: flex; + align-items: flex-start; + background-color: #694a2d !important; +} +.file_buttons { + margin-right: 2vw; +} +.file_delete, .file_download { + border-radius: 1vw !important; + margin: 1vw; + width: 8vw; + height: 3vw; +} +.file_delete { + background-color: hsla(0, 100%, 62%, 0.785) !important; + border-color: hsla(0, 100%, 62%, 0.785) !important; +} +.button_text { + font-size: 1.3vw; +} \ No newline at end of file diff --git a/static/css/decision.css b/static/css/decision.css deleted file mode 100644 index 06aa82f..0000000 --- a/static/css/decision.css +++ /dev/null @@ -1,124 +0,0 @@ -.decision_page { - height: 90vw; - background-color: #dcb495; - display: flex; - flex-direction: column; - align-items: center; -} -.link_back_block { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - flex-wrap: nowrap; -} -.link_back { - background-color: #ffffff; - color: #000000; - width: 15vw; - height: 4.5vw; - vertical-align: middle; - border-radius: 5vw; - display: flex; - align-items: center; - justify-content: center; -} -.link_back:hover { - text-decoration: none; - color: #000000; -} -.link_back_text { - font-size: 1.5vw; - margin-top: 15px; - display: flex; - align-items: center; - justify-content: center; -} -.name_block { - margin-top: 3vw; - width: 90%; - height: 25vw; - display: flex; - flex-direction: column; - align-items: center; -} -.title_block { - width: 90%; - display: flex; - justify-content: center; -} -.title_task { - text-align: center; - color: #000000; - font-size: 4vw; -} -.description_task { - width: 80%; - background-color: #EDCBB0; - height: auto; - max-height: 15vw; - border-radius: 2vw; - display: flex; - overflow-y: auto; -} -.description { - margin: 15px; -} -.description_text { - font-size: 1.5vw; - text-align: justify; -} -.data_block { - width: 90%; - display: flex; - align-items: flex-start; - justify-content: center -} -.bottom_data { - margin: 2vw; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} -.form_label { - margin-top: 10px; - font-size: 1.3vw; - color: #000000; - font-weight: bold; -} -.input_data { - color: #000000; - border: 0.1vw solid #595008; - height: 4.5vw; - min-height: 4.5vw; - width: 30vw; - background-color: #dbc3af; - border-radius: 5vw; - font-size: 1.3vw; - display: inline-flex; - align-items: center; -} -.input_button { - width: 10vw; - height: 5vw; - border-radius: 5vw; - vertical-align: middle; -} -.text_data { - border-radius: 2vw !important; - width: 35vw; -} -.form_data { - display: flex; - flex-direction: column; - margin-left: 2%; -} -.decision_block { - margin-top: 3vw; - width: 90%; - height: 25vw; - display: flex; - flex-direction: column; - align-items: center; -} \ No newline at end of file diff --git a/static/js/project.js b/static/js/project.js index 67de248..d3669ef 100644 --- a/static/js/project.js +++ b/static/js/project.js @@ -2,6 +2,7 @@ var edit_button = document.getElementById("edit_button"), new_task_link = document.getElementById("new_task_link"), quest_solve_link = document.getElementById("quest_solve_link"), quest_solve_link_id = document.getElementById("quest_solve_link_id"); + edit_button.href = String(window.location.href) + '/edit'; -new_task_link.href = String(window.location.href) + '/task/new'; +new_task_link.href = String(window.location.href) + '/quest/new'; quest_solve_link.href = String(window.location.href) + '/quest/' + quest_solve_link_id.className; \ No newline at end of file diff --git a/templates/answer.html b/templates/answer.html new file mode 100644 index 0000000..fb196a5 --- /dev/null +++ b/templates/answer.html @@ -0,0 +1,94 @@ + +{% extends "base.html" %} {% block content %} +
+ +
+
+

{{ task.name }}

+
+
+
+

{{ task.description }}

+
+
+ {% if list_files %} +
+

Файлы

+
+
+ {% for file in list_files %} +
+
+ +
+
+
+ {% if current_user.id == project.creator or task.creator == current_user.id or file.user == current_user.id %} +

Удалить

+ {% endif %} +

Скачать

+
+
+
+ {% endfor %} +
+
+
+ {% endif %} +
+ + {{ form.text(class="input_data text_data", type="text", id="text_data", placeholder='your answer') }} + {% for error in form.text.errors %} + + {% endfor %} +
+
+
+
+ {{ form.hidden_tag() }} +
+
+ + {{ form.file(class="input_data padding_data", type="file") }} + {% for error in form.file.errors %} + + {% endfor %} +
+
+ {% if current_user.id == project.creator %} +
+ + {{ form.deadline_date(class="input_data deadline padding_data", type="date") }} + {% for error in form.deadline_date.errors %} + + {% endfor %} + {{ form.deadline_time(class="input_data deadline padding_data", type="time") }} + {% for error in form.deadline_time.errors %} + + {% endfor %} +
+ {% endif %} +
+ {{ form.submit(type="submit", class="quest_button") }} +
+ {{ form.realized(class="realized")}} + {{form.realized.label }}
+ {% for error in form.realized.errors %} + + {% endfor %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/decision.html b/templates/decision.html deleted file mode 100644 index b5e5c71..0000000 --- a/templates/decision.html +++ /dev/null @@ -1,41 +0,0 @@ - -{% extends "base.html" %} {% block content %} -
- -
-
-

{{ task.name }}

-
-
-
-

{{ task.description }}

-
-
-
-
-
- {{ form.hidden_tag() }} -
-
- - {{ form.text(class="input_data label_data text_data", type="text", placeholder='your answer') }} - {% for error in form.text.errors %} - - {% endfor %} -
-
- - {{ form.file(class="input_data padding_data", type="file") }} - {% for error in form.file.errors %} - - {% endfor %} -
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index c92d79e..24fb22b 100644 --- a/templates/login.html +++ b/templates/login.html @@ -32,8 +32,7 @@
- {{ form.remember_me(class="remember")}} {{ - form.remember_me.label }}
+ {{ form.remember_me(class="remember")}} {{form.remember_me.label }}
{% for error in form.remember_me.errors %} {% endfor %} diff --git a/templates/project.html b/templates/project.html index f374ca4..b29b347 100644 --- a/templates/project.html +++ b/templates/project.html @@ -72,6 +72,10 @@ + {% elif quest.overdue == '' and quest.realized != 1 %} + {% else %}
{% else %} - +
+
+

Описание

+
+

{{ quest.description }}

+
+
+ +
{% endif %}