initial
117
.gitignore
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
# Django #
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
media
|
||||
data
|
||||
|
||||
# Backup files #
|
||||
*.bak
|
||||
|
||||
# If you are using PyCharm #
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
*.iws /out/
|
||||
|
||||
# Python #
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
.Python build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery
|
||||
celerybeat-schedule.*
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Sublime Text #
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
# Package control specific files Package
|
||||
Control.last-run
|
||||
Control.ca-list
|
||||
Control.ca-bundle
|
||||
Control.system-ca-bundle
|
||||
GitHub.sublime-settings
|
||||
|
||||
# Visual Studio Code #
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history
|
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/../../../../../../:\Users\79175\Desktop\Sprint_server\.idea/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
32
.idea/Sprint_server.iml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="django" name="Django">
|
||||
<configuration>
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="Sprint/settings.py" />
|
||||
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="migrations" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/venv/lib/python3.7/site-packages/django/forms/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
28
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<DBN-PSQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false" />
|
||||
</DBN-PSQL>
|
||||
<DBN-SQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false">
|
||||
<option name="STATEMENT_SPACING" value="one_line" />
|
||||
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||
</formatting-settings>
|
||||
</DBN-SQL>
|
||||
</code_scheme>
|
||||
</component>
|
456
.idea/dbnavigator.xml
Normal file
@ -0,0 +1,456 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DBNavigator.Project.DataEditorManager">
|
||||
<record-view-column-sorting-type value="BY_INDEX" />
|
||||
<value-preview-text-wrapping value="true" />
|
||||
<value-preview-pinned value="false" />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.DataExportManager">
|
||||
<export-instructions>
|
||||
<create-header value="true" />
|
||||
<friendly-headers value="false" />
|
||||
<quote-values-containing-separator value="true" />
|
||||
<quote-all-values value="false" />
|
||||
<value-separator value="" />
|
||||
<file-name value="" />
|
||||
<file-location value="" />
|
||||
<scope value="GLOBAL" />
|
||||
<destination value="FILE" />
|
||||
<format value="EXCEL" />
|
||||
<charset value="UTF-8" />
|
||||
</export-instructions>
|
||||
</component>
|
||||
<component name="DBNavigator.Project.DatabaseBrowserManager">
|
||||
<autoscroll-to-editor value="false" />
|
||||
<autoscroll-from-editor value="true" />
|
||||
<show-object-properties value="true" />
|
||||
<loaded-nodes />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.DatabaseFileManager">
|
||||
<open-files />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.EditorStateManager">
|
||||
<last-used-providers />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.ExecutionManager">
|
||||
<retain-sticky-names value="false" />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.MethodExecutionManager">
|
||||
<method-browser />
|
||||
<execution-history>
|
||||
<group-entries value="true" />
|
||||
<execution-inputs />
|
||||
</execution-history>
|
||||
<argument-values-cache />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.ObjectDependencyManager">
|
||||
<last-used-dependency-type value="INCOMING" />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.ObjectQuickFilterManager">
|
||||
<last-used-operator value="EQUAL" />
|
||||
<filters />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
|
||||
<recently-used-interfaces />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.Settings">
|
||||
<connections />
|
||||
<browser-settings>
|
||||
<general>
|
||||
<display-mode value="TABBED" />
|
||||
<navigation-history-size value="100" />
|
||||
<show-object-details value="false" />
|
||||
</general>
|
||||
<filters>
|
||||
<object-type-filter>
|
||||
<object-type name="SCHEMA" enabled="true" />
|
||||
<object-type name="USER" enabled="true" />
|
||||
<object-type name="ROLE" enabled="true" />
|
||||
<object-type name="PRIVILEGE" enabled="true" />
|
||||
<object-type name="CHARSET" enabled="true" />
|
||||
<object-type name="TABLE" enabled="true" />
|
||||
<object-type name="VIEW" enabled="true" />
|
||||
<object-type name="MATERIALIZED_VIEW" enabled="true" />
|
||||
<object-type name="NESTED_TABLE" enabled="true" />
|
||||
<object-type name="COLUMN" enabled="true" />
|
||||
<object-type name="INDEX" enabled="true" />
|
||||
<object-type name="CONSTRAINT" enabled="true" />
|
||||
<object-type name="DATASET_TRIGGER" enabled="true" />
|
||||
<object-type name="DATABASE_TRIGGER" enabled="true" />
|
||||
<object-type name="SYNONYM" enabled="true" />
|
||||
<object-type name="SEQUENCE" enabled="true" />
|
||||
<object-type name="PROCEDURE" enabled="true" />
|
||||
<object-type name="FUNCTION" enabled="true" />
|
||||
<object-type name="PACKAGE" enabled="true" />
|
||||
<object-type name="TYPE" enabled="true" />
|
||||
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
|
||||
<object-type name="ARGUMENT" enabled="true" />
|
||||
<object-type name="DIMENSION" enabled="true" />
|
||||
<object-type name="CLUSTER" enabled="true" />
|
||||
<object-type name="DBLINK" enabled="true" />
|
||||
</object-type-filter>
|
||||
</filters>
|
||||
<sorting>
|
||||
<object-type name="COLUMN" sorting-type="NAME" />
|
||||
<object-type name="FUNCTION" sorting-type="NAME" />
|
||||
<object-type name="PROCEDURE" sorting-type="NAME" />
|
||||
<object-type name="ARGUMENT" sorting-type="POSITION" />
|
||||
</sorting>
|
||||
<default-editors>
|
||||
<object-type name="VIEW" editor-type="SELECTION" />
|
||||
<object-type name="PACKAGE" editor-type="SELECTION" />
|
||||
<object-type name="TYPE" editor-type="SELECTION" />
|
||||
</default-editors>
|
||||
</browser-settings>
|
||||
<navigation-settings>
|
||||
<lookup-filters>
|
||||
<lookup-objects>
|
||||
<object-type name="SCHEMA" enabled="true" />
|
||||
<object-type name="USER" enabled="false" />
|
||||
<object-type name="ROLE" enabled="false" />
|
||||
<object-type name="PRIVILEGE" enabled="false" />
|
||||
<object-type name="CHARSET" enabled="false" />
|
||||
<object-type name="TABLE" enabled="true" />
|
||||
<object-type name="VIEW" enabled="true" />
|
||||
<object-type name="MATERIALIZED VIEW" enabled="true" />
|
||||
<object-type name="INDEX" enabled="true" />
|
||||
<object-type name="CONSTRAINT" enabled="true" />
|
||||
<object-type name="DATASET TRIGGER" enabled="true" />
|
||||
<object-type name="DATABASE TRIGGER" enabled="true" />
|
||||
<object-type name="SYNONYM" enabled="false" />
|
||||
<object-type name="SEQUENCE" enabled="true" />
|
||||
<object-type name="PROCEDURE" enabled="true" />
|
||||
<object-type name="FUNCTION" enabled="true" />
|
||||
<object-type name="PACKAGE" enabled="true" />
|
||||
<object-type name="TYPE" enabled="true" />
|
||||
<object-type name="DIMENSION" enabled="false" />
|
||||
<object-type name="CLUSTER" enabled="false" />
|
||||
<object-type name="DBLINK" enabled="true" />
|
||||
</lookup-objects>
|
||||
<force-database-load value="false" />
|
||||
<prompt-connection-selection value="true" />
|
||||
<prompt-schema-selection value="true" />
|
||||
</lookup-filters>
|
||||
</navigation-settings>
|
||||
<dataset-grid-settings>
|
||||
<general>
|
||||
<enable-zooming value="true" />
|
||||
<enable-column-tooltip value="true" />
|
||||
</general>
|
||||
<sorting>
|
||||
<nulls-first value="true" />
|
||||
<max-sorting-columns value="4" />
|
||||
</sorting>
|
||||
<tracking-columns>
|
||||
<columnNames value="" />
|
||||
<visible value="true" />
|
||||
<editable value="false" />
|
||||
</tracking-columns>
|
||||
</dataset-grid-settings>
|
||||
<dataset-editor-settings>
|
||||
<text-editor-popup>
|
||||
<active value="false" />
|
||||
<active-if-empty value="false" />
|
||||
<data-length-threshold value="100" />
|
||||
<popup-delay value="1000" />
|
||||
</text-editor-popup>
|
||||
<values-actions-popup>
|
||||
<show-popup-button value="true" />
|
||||
<element-count-threshold value="1000" />
|
||||
<data-length-threshold value="250" />
|
||||
</values-actions-popup>
|
||||
<general>
|
||||
<fetch-block-size value="100" />
|
||||
<fetch-timeout value="30" />
|
||||
<trim-whitespaces value="true" />
|
||||
<convert-empty-strings-to-null value="true" />
|
||||
<select-content-on-cell-edit value="true" />
|
||||
<large-value-preview-active value="true" />
|
||||
</general>
|
||||
<filters>
|
||||
<prompt-filter-dialog value="true" />
|
||||
<default-filter-type value="BASIC" />
|
||||
</filters>
|
||||
<qualified-text-editor text-length-threshold="300">
|
||||
<content-types>
|
||||
<content-type name="Text" enabled="true" />
|
||||
<content-type name="Properties" enabled="true" />
|
||||
<content-type name="XML" enabled="true" />
|
||||
<content-type name="DTD" enabled="true" />
|
||||
<content-type name="HTML" enabled="true" />
|
||||
<content-type name="XHTML" enabled="true" />
|
||||
<content-type name="CSS" enabled="true" />
|
||||
<content-type name="SQL" enabled="true" />
|
||||
<content-type name="PL/SQL" enabled="true" />
|
||||
<content-type name="JavaScript" enabled="true" />
|
||||
<content-type name="JSON" enabled="true" />
|
||||
<content-type name="JSON5" enabled="true" />
|
||||
<content-type name="YAML" enabled="true" />
|
||||
</content-types>
|
||||
</qualified-text-editor>
|
||||
<record-navigation>
|
||||
<navigation-target value="VIEWER" />
|
||||
</record-navigation>
|
||||
</dataset-editor-settings>
|
||||
<code-editor-settings>
|
||||
<general>
|
||||
<show-object-navigation-gutter value="false" />
|
||||
<show-spec-declaration-navigation-gutter value="true" />
|
||||
<enable-spellchecking value="true" />
|
||||
<enable-reference-spellchecking value="false" />
|
||||
</general>
|
||||
<confirmations>
|
||||
<save-changes value="false" />
|
||||
<revert-changes value="true" />
|
||||
</confirmations>
|
||||
</code-editor-settings>
|
||||
<code-completion-settings>
|
||||
<filters>
|
||||
<basic-filter>
|
||||
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||
<filter-element type="OBJECT" id="role" selected="true" />
|
||||
<filter-element type="OBJECT" id="user" selected="true" />
|
||||
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||
<user-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</user-schema>
|
||||
<public-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="false" />
|
||||
<filter-element type="OBJECT" id="view" selected="false" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="false" />
|
||||
<filter-element type="OBJECT" id="index" selected="false" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="false" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="false" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="false" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="false" />
|
||||
<filter-element type="OBJECT" id="function" selected="false" />
|
||||
<filter-element type="OBJECT" id="package" selected="false" />
|
||||
<filter-element type="OBJECT" id="type" selected="false" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="false" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="false" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="false" />
|
||||
</public-schema>
|
||||
<any-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</any-schema>
|
||||
</basic-filter>
|
||||
<extended-filter>
|
||||
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||
<filter-element type="OBJECT" id="user" selected="true" />
|
||||
<filter-element type="OBJECT" id="role" selected="true" />
|
||||
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||
<user-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</user-schema>
|
||||
<public-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</public-schema>
|
||||
<any-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</any-schema>
|
||||
</extended-filter>
|
||||
</filters>
|
||||
<sorting enabled="true">
|
||||
<sorting-element type="RESERVED_WORD" id="keyword" />
|
||||
<sorting-element type="RESERVED_WORD" id="datatype" />
|
||||
<sorting-element type="OBJECT" id="column" />
|
||||
<sorting-element type="OBJECT" id="table" />
|
||||
<sorting-element type="OBJECT" id="view" />
|
||||
<sorting-element type="OBJECT" id="materialized view" />
|
||||
<sorting-element type="OBJECT" id="index" />
|
||||
<sorting-element type="OBJECT" id="constraint" />
|
||||
<sorting-element type="OBJECT" id="trigger" />
|
||||
<sorting-element type="OBJECT" id="synonym" />
|
||||
<sorting-element type="OBJECT" id="sequence" />
|
||||
<sorting-element type="OBJECT" id="procedure" />
|
||||
<sorting-element type="OBJECT" id="function" />
|
||||
<sorting-element type="OBJECT" id="package" />
|
||||
<sorting-element type="OBJECT" id="type" />
|
||||
<sorting-element type="OBJECT" id="dimension" />
|
||||
<sorting-element type="OBJECT" id="cluster" />
|
||||
<sorting-element type="OBJECT" id="dblink" />
|
||||
<sorting-element type="OBJECT" id="schema" />
|
||||
<sorting-element type="OBJECT" id="role" />
|
||||
<sorting-element type="OBJECT" id="user" />
|
||||
<sorting-element type="RESERVED_WORD" id="function" />
|
||||
<sorting-element type="RESERVED_WORD" id="parameter" />
|
||||
</sorting>
|
||||
<format>
|
||||
<enforce-code-style-case value="true" />
|
||||
</format>
|
||||
</code-completion-settings>
|
||||
<execution-engine-settings>
|
||||
<statement-execution>
|
||||
<fetch-block-size value="100" />
|
||||
<execution-timeout value="20" />
|
||||
<debug-execution-timeout value="600" />
|
||||
<focus-result value="false" />
|
||||
<prompt-execution value="false" />
|
||||
</statement-execution>
|
||||
<script-execution>
|
||||
<command-line-interfaces />
|
||||
<execution-timeout value="300" />
|
||||
</script-execution>
|
||||
<method-execution>
|
||||
<execution-timeout value="30" />
|
||||
<debug-execution-timeout value="600" />
|
||||
<parameter-history-size value="10" />
|
||||
</method-execution>
|
||||
</execution-engine-settings>
|
||||
<operation-settings>
|
||||
<transactions>
|
||||
<uncommitted-changes>
|
||||
<on-project-close value="ASK" />
|
||||
<on-disconnect value="ASK" />
|
||||
<on-autocommit-toggle value="ASK" />
|
||||
</uncommitted-changes>
|
||||
<multiple-uncommitted-changes>
|
||||
<on-commit value="ASK" />
|
||||
<on-rollback value="ASK" />
|
||||
</multiple-uncommitted-changes>
|
||||
</transactions>
|
||||
<session-browser>
|
||||
<disconnect-session value="ASK" />
|
||||
<kill-session value="ASK" />
|
||||
<reload-on-filter-change value="false" />
|
||||
</session-browser>
|
||||
<compiler>
|
||||
<compile-type value="KEEP" />
|
||||
<compile-dependencies value="ASK" />
|
||||
<always-show-controls value="false" />
|
||||
</compiler>
|
||||
<debugger>
|
||||
<debugger-type value="JDBC" />
|
||||
<use-generic-runners value="true" />
|
||||
</debugger>
|
||||
</operation-settings>
|
||||
<ddl-file-settings>
|
||||
<extensions>
|
||||
<mapping file-type-id="VIEW" extensions="vw" />
|
||||
<mapping file-type-id="TRIGGER" extensions="trg" />
|
||||
<mapping file-type-id="PROCEDURE" extensions="prc" />
|
||||
<mapping file-type-id="FUNCTION" extensions="fnc" />
|
||||
<mapping file-type-id="PACKAGE" extensions="pkg" />
|
||||
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
|
||||
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
|
||||
<mapping file-type-id="TYPE" extensions="tpe" />
|
||||
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
|
||||
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
|
||||
</extensions>
|
||||
<general>
|
||||
<lookup-ddl-files value="true" />
|
||||
<create-ddl-files value="false" />
|
||||
<synchronize-ddl-files value="true" />
|
||||
<use-qualified-names value="false" />
|
||||
<make-scripts-rerunnable value="true" />
|
||||
</general>
|
||||
</ddl-file-settings>
|
||||
<general-settings>
|
||||
<regional-settings>
|
||||
<date-format value="MEDIUM" />
|
||||
<number-format value="UNGROUPED" />
|
||||
<locale value="SYSTEM_DEFAULT" />
|
||||
<use-custom-formats value="false" />
|
||||
</regional-settings>
|
||||
<environment>
|
||||
<environment-types>
|
||||
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
|
||||
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
|
||||
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
|
||||
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
|
||||
</environment-types>
|
||||
<visibility-settings>
|
||||
<connection-tabs value="true" />
|
||||
<dialog-headers value="true" />
|
||||
<object-editor-tabs value="true" />
|
||||
<script-editor-tabs value="false" />
|
||||
<execution-result-tabs value="true" />
|
||||
</visibility-settings>
|
||||
</environment>
|
||||
</general-settings>
|
||||
</component>
|
||||
<component name="DBNavigator.Project.StatementExecutionManager">
|
||||
<execution-variables />
|
||||
</component>
|
||||
</project>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Sprint_server.iml" filepath="$PROJECT_DIR$/.idea/Sprint_server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
28
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": ".\\manage.py",
|
||||
"args": [
|
||||
"runserver"
|
||||
],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
"name": "Python: Текущий файл",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"runserver"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
243
Main/Tester.py
Normal file
@ -0,0 +1,243 @@
|
||||
from os import listdir, mkdir
|
||||
from os.path import basename, isdir
|
||||
from shutil import rmtree, copyfile
|
||||
from threading import Thread
|
||||
from xml.dom.minidom import parse
|
||||
|
||||
from Main.models import *
|
||||
from .main import solution_path
|
||||
|
||||
|
||||
def start_new(host):
|
||||
in_queue = list(Solution.objects.filter(result='IN QUEUE'))
|
||||
if in_queue:
|
||||
sol = in_queue[0]
|
||||
for s in in_queue:
|
||||
dif = s.task.block.priority * 10 + s.task.priority - sol.task.block.priority * 10 - sol.task.priority
|
||||
if dif > 0:
|
||||
sol = s
|
||||
elif dif == 0 and s.id < sol.id:
|
||||
sol = s
|
||||
Tester(sol, host).test()
|
||||
|
||||
|
||||
def is_project(path):
|
||||
return any([x.endswith('.csproj') for x in listdir(path)])
|
||||
|
||||
|
||||
def get_node_value(element):
|
||||
return element[0].firstChild.nodeValue
|
||||
|
||||
|
||||
def nunit_path(working_dir):
|
||||
return '..{}'.format(sep) * len(working_dir.split(sep)) + 'nunit_console{}nunit3-console.exe'.format(sep)
|
||||
|
||||
|
||||
class Tester:
|
||||
|
||||
def __init__(self, solution, host):
|
||||
self.solution = solution
|
||||
self.host = host
|
||||
self.working_dir = ''
|
||||
self.files = []
|
||||
|
||||
# функция компиляции
|
||||
def build(self, path):
|
||||
# решение для UNIX
|
||||
# shell('msbuild ' + path + ' /p:Configuration=Debug')
|
||||
|
||||
# решение для Windows
|
||||
cmd = 'dotnet build {} -o {}\\bin\\Debug'.format(path, path)
|
||||
with self.solution.log_fs as fs:
|
||||
shell(cmd, fs)
|
||||
|
||||
def build_and_copy(self, path, working_dir):
|
||||
if exists(join(path, 'bin', 'Debug')):
|
||||
rmtree(join(path, 'bin', 'Debug'))
|
||||
self.build(path)
|
||||
name = basename(path)
|
||||
if not exists(join(path, 'bin', 'Debug')) or not any(
|
||||
x.endswith('.exe') for x in listdir(join(path, 'bin', 'Debug'))):
|
||||
return False
|
||||
self.files.append(basename(path))
|
||||
for file in listdir(join(path, 'bin', 'Debug')):
|
||||
if exists(join(path, 'bin', 'Debug', file)):
|
||||
new_file = join(working_dir, basename(file))
|
||||
try:
|
||||
copyfile(join(path, 'bin', 'Debug', file), new_file)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def push(self):
|
||||
solution = self.solution
|
||||
if solution.result == 'SOLUTION ERROR':
|
||||
return
|
||||
solution.result = 'IN QUEUE'
|
||||
solution.save()
|
||||
from Main.models import System
|
||||
if len(Solution.objects.filter(result='TESTING')) < int(System.objects.get(key='queue_size').value):
|
||||
self.test()
|
||||
|
||||
def delete_everything(self):
|
||||
ssp = solution_path(self.solution.path())
|
||||
sln_path = join(ssp, '.idea')
|
||||
if exists(sln_path):
|
||||
rmtree(sln_path)
|
||||
sln_path = join(ssp, '.vs')
|
||||
if exists(sln_path):
|
||||
rmtree(sln_path)
|
||||
sln_path = ssp
|
||||
for p in listdir(sln_path):
|
||||
if isdir(join(sln_path, p)):
|
||||
if exists(join(sln_path, p, 'bin')):
|
||||
rmtree(join(sln_path, p, 'bin'))
|
||||
if exists(join(sln_path, p, 'obj')):
|
||||
rmtree(join(sln_path, p, 'obj'))
|
||||
if exists(self.working_dir):
|
||||
rmtree(self.working_dir)
|
||||
if exists(join(self.solution.path(), 'solution.zip')):
|
||||
remove(join(self.solution.path(), 'solution.zip'))
|
||||
if exists(join(self.solution.path(), '__MACOSX')):
|
||||
rmtree(join(self.solution.path(), '__MACOSX'))
|
||||
if exists(join(sln_path, '.DS_Store')):
|
||||
remove(join(sln_path, '.DS_Store'))
|
||||
if exists(join(sln_path, 'test_folder')):
|
||||
rmtree(join(sln_path, 'test_folder'))
|
||||
|
||||
def nunit_testing(self):
|
||||
solution = self.solution
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Building image\n')
|
||||
shell('docker build -t solution_{} {}'.format(self.solution.id, self.working_dir))
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Image built successfully\n')
|
||||
|
||||
def execute():
|
||||
with self.solution.log_fs as fs:
|
||||
shell('docker run --name solution_container_{} solution_{}'.format(self.solution.id, self.solution.id),
|
||||
output=fs)
|
||||
|
||||
solution.write_log('Running container')
|
||||
t = Thread(target=execute)
|
||||
t.start()
|
||||
t.join(self.solution.task.time_limit / 1000)
|
||||
solution.write_log('Running finished')
|
||||
with self.solution.log_fs as fs:
|
||||
shell('docker cp solution_container_{}:/app/TestResults.xml {}'.format(self.solution.id, self.working_dir),
|
||||
fs)
|
||||
with self.solution.log_fs as fs:
|
||||
shell('docker rm --force solution_container_{}'.format(self.solution.id), fs)
|
||||
with self.solution.log_fs as fs:
|
||||
shell('docker image rm solution_{}'.format(self.solution.id), fs)
|
||||
if not exists(join(self.working_dir, 'TestResults.xml')):
|
||||
self.solution.set_result('Time limit')
|
||||
solution.write_log('Result file not found in container')
|
||||
return
|
||||
solution.write_log('Result file found in container')
|
||||
try:
|
||||
doc = parse(join(self.working_dir, 'TestResults.xml'))
|
||||
res = get_node_value(doc.getElementsByTagName('Passed')) + '/' + get_node_value(
|
||||
doc.getElementsByTagName('Total'))
|
||||
self.solution.details = ''
|
||||
for el in doc.getElementsByTagName('Result'):
|
||||
self.solution.details += '<h5><b>' + get_node_value(el.getElementsByTagName('MethodName')) + '</b></h5>'
|
||||
r = get_node_value(el.getElementsByTagName('Successful'))
|
||||
if r == 'true':
|
||||
self.solution.details += '<div style="color: green;">Passed</div>'
|
||||
else:
|
||||
self.solution.details += '<div style="color: red;">Failed</div>'
|
||||
mes = get_node_value(el.getElementsByTagName('Message'))
|
||||
self.solution.details += '<pre>{}</pre>'.format(mes)
|
||||
except:
|
||||
solution.write_log('Unknown error')
|
||||
res = 'TEST ERROR'
|
||||
self.solution.set_result(res)
|
||||
|
||||
def test(self):
|
||||
solution = self.solution
|
||||
solution.result = 'TESTING'
|
||||
solution.save()
|
||||
try:
|
||||
if not exists(self.solution.task.tests_path()):
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'No test file found\n')
|
||||
solution.set_result('TEST ERROR')
|
||||
solution.save()
|
||||
self.delete_everything()
|
||||
start_new(self.host)
|
||||
return
|
||||
sln_path = solution_path(join(MEDIA_ROOT, 'solutions', str(solution.id)))
|
||||
if sln_path == '':
|
||||
solution.set_result('TEST ERROR')
|
||||
solution.save()
|
||||
self.delete_everything()
|
||||
start_new(self.host)
|
||||
return
|
||||
working_dir = join(sln_path, 'test_folder')
|
||||
if exists(working_dir):
|
||||
try:
|
||||
rmtree(working_dir)
|
||||
except:
|
||||
remove(working_dir)
|
||||
mkdir(working_dir)
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Testing directory created\n')
|
||||
for project in listdir(sln_path):
|
||||
solution.write_log('Checking if {} is project'.format(project))
|
||||
prj = project
|
||||
project = join(sln_path, project)
|
||||
if isdir(project) and is_project(project) and basename(project) != 'TestsProject':
|
||||
if not self.build_and_copy(project, working_dir):
|
||||
solution.set_result('Compilation error')
|
||||
solution.write_log('Failed to compile project {}'.format(prj))
|
||||
solution.save()
|
||||
self.delete_everything()
|
||||
start_new(self.host)
|
||||
return
|
||||
dll_path = solution.task.tests_path()
|
||||
solution.write_log('Copying test file to working directory')
|
||||
copyfile(dll_path, join(working_dir, str(solution.task.id) + '.cs'))
|
||||
solution.write_log('Test file copied')
|
||||
for file in listdir('SprintTest'):
|
||||
try:
|
||||
copyfile(join('SprintTest', file), join(working_dir, file))
|
||||
except:
|
||||
pass
|
||||
self.working_dir = working_dir
|
||||
build_tests_cmd = 'csc -out:{} -t:library /r:{} /r:{} /r:{} '.format(join(self.working_dir, 'tests.dll'),
|
||||
join(self.working_dir,
|
||||
'SprintTest.dll'),
|
||||
join(working_dir,
|
||||
'System.Runtime.dll'),
|
||||
join(working_dir,
|
||||
'System.Reflection.dll'))
|
||||
for file in self.files:
|
||||
build_tests_cmd += '/r:{}.dll '.format(join(self.working_dir, file))
|
||||
build_tests_cmd += self.solution.task.tests_path()
|
||||
if exists(join(self.working_dir, 'tests.dll')):
|
||||
remove(join(self.working_dir, 'tests.dll'))
|
||||
solution.write_log('Building tests file started')
|
||||
with self.solution.log_fs as fs:
|
||||
shell(build_tests_cmd, fs)
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Building tests file finished\n')
|
||||
if exists(join(self.working_dir, 'tests.dll')):
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Got .dll tests file\n')
|
||||
for file in ExtraFile.objects.filter(task=self.solution.task):
|
||||
copyfile(file.path, join(working_dir, file.filename))
|
||||
self.nunit_testing()
|
||||
else:
|
||||
solution.set_result('TEST ERROR')
|
||||
solution.write_log('Failed to compile tests')
|
||||
except:
|
||||
solution.set_result('TEST ERROR')
|
||||
raise
|
||||
with self.solution.log_fs as fs:
|
||||
fs.write(b'Unknown error\n')
|
||||
solution.save()
|
||||
self.delete_everything()
|
||||
start_new(self.host)
|
29
Main/Timer.py
Normal file
@ -0,0 +1,29 @@
|
||||
class Method:
|
||||
|
||||
def __init__(self, meth, name):
|
||||
self.meth = meth
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name
|
||||
|
||||
def execute(self):
|
||||
self.meth()
|
||||
|
||||
|
||||
class Timer:
|
||||
methods = []
|
||||
|
||||
def push(self, meth):
|
||||
methods.append(math)
|
||||
|
||||
def polling(self):
|
||||
for i in range(len(self.methods)):
|
||||
methods[i].execute()
|
||||
|
||||
def remove(method_name):
|
||||
for method in self.methods:
|
||||
if method.name == method_name:
|
||||
self.methods.remove(method)
|
||||
return
|
||||
raise IndexError("No method in list")
|
0
Main/__init__.py
Normal file
14
Main/admin.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django.contrib import admin
|
||||
from Main.models import *
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Course)
|
||||
admin.site.register(Block)
|
||||
admin.site.register(Task)
|
||||
admin.site.register(Subscribe)
|
||||
admin.site.register(Restore)
|
||||
admin.site.register(UserInfo)
|
||||
admin.site.register(Solution)
|
||||
admin.site.register(System)
|
||||
admin.site.register(ExtraFile)
|
5
Main/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MainConfig(AppConfig):
|
||||
name = 'Main'
|
8
Main/commands.py
Normal file
@ -0,0 +1,8 @@
|
||||
from subprocess import Popen
|
||||
from sys import stdout
|
||||
|
||||
|
||||
def shell(cmd, output=stdout):
|
||||
p = Popen(cmd, shell=True, stdout=output)
|
||||
p.wait()
|
||||
p.kill()
|
8
Main/context_processors.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .main import check_admin
|
||||
|
||||
|
||||
def attributes(request):
|
||||
return {
|
||||
'current_page': 'settings' if '/settings' == request.path else 'admin' if '/admin/' in request.path else 'main',
|
||||
'is_admin': check_admin(request.user)
|
||||
}
|
32
Main/forms.py
Normal file
@ -0,0 +1,32 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class PasswordField(forms.CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.widget = forms.PasswordInput()
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
email = forms.EmailField(widget=forms.TextInput(attrs={"class": "input_simple"}))
|
||||
password = forms.CharField(widget=forms.PasswordInput(attrs={"class": "input_simple"}))
|
||||
|
||||
|
||||
class FileForm(forms.Form):
|
||||
file = forms.FileField(widget=forms.FileInput(attrs={'class': 'input_simple'}), required=False)
|
||||
|
||||
|
||||
class TestsForm(forms.Form):
|
||||
tests = forms.FileField(widget=forms.FileInput(), required=False)
|
||||
|
||||
|
||||
class ChangePasswordForm(forms.Form):
|
||||
old = PasswordField(label='Старый пароль')
|
||||
new = PasswordField(label='Новый пароль')
|
||||
again = PasswordField(label='Еще раз')
|
||||
|
||||
|
||||
class ResetPasswordForm(forms.Form):
|
||||
new = PasswordField(label='Новый пароль')
|
||||
again = PasswordField(label='Еще раз')
|
339
Main/main.py
Normal file
@ -0,0 +1,339 @@
|
||||
import smtplib
|
||||
from contextlib import contextmanager
|
||||
from json import dumps
|
||||
from os import listdir, mkdir
|
||||
from os.path import isdir, basename, dirname, join, exists
|
||||
from random import choice
|
||||
from shutil import copyfile, rmtree
|
||||
from string import ascii_letters
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import copydetect
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.transaction import atomic
|
||||
from django.utils import timezone
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
|
||||
from Main.models import Course, Block, Solution, ThreadSafe, Restore, System, Subscribe, UserInfo
|
||||
from Sprint.settings import MEDIA_ROOT
|
||||
|
||||
base_dir = 'data'
|
||||
|
||||
|
||||
@contextmanager
|
||||
def lock(key):
|
||||
pk = ThreadSafe.objects.get_or_create(key=key)[0].pk
|
||||
try:
|
||||
objs = ThreadSafe.objects.filter(pk=pk).select_for_update()
|
||||
with atomic():
|
||||
list(objs)
|
||||
yield None
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
def get_in_html_tag(full, tag_name):
|
||||
try:
|
||||
return full.split('<div class="{}">'.format(tag_name))[1].split('</div>')[0]
|
||||
except IndexError:
|
||||
return ''
|
||||
|
||||
|
||||
def random_string():
|
||||
letters = ascii_letters
|
||||
return ''.join(choice(letters) for _ in range(20))
|
||||
|
||||
|
||||
def get_restore_hash():
|
||||
available = [r.code for r in Restore.objects.all()]
|
||||
while True:
|
||||
s = random_string()
|
||||
if s not in available:
|
||||
break
|
||||
return s
|
||||
|
||||
|
||||
def send(subject, to_addr, body_text):
|
||||
from_addr = System.objects.get(key='email_address').value
|
||||
body = "\r\n".join((
|
||||
"From: %s" % from_addr,
|
||||
"To: %s" % to_addr,
|
||||
"Subject: %s" % subject,
|
||||
"",
|
||||
body_text
|
||||
))
|
||||
|
||||
server = smtplib.SMTP('SMTP.Office365.com', 587)
|
||||
server.starttls()
|
||||
server.login(System.objects.get(key='email_address').value, System.objects.get(key='email_password').value)
|
||||
server.sendmail(from_addr, [to_addr], body)
|
||||
server.quit()
|
||||
|
||||
|
||||
def send_email(subject, to_addr, body_text):
|
||||
Thread(target=lambda: send(subject, to_addr, body_text)).start()
|
||||
|
||||
|
||||
def check_login(user):
|
||||
return user.is_authenticated
|
||||
|
||||
|
||||
def check_admin(user):
|
||||
if check_teacher(user):
|
||||
return True
|
||||
if not check_login(user):
|
||||
return False
|
||||
return len(Subscribe.objects.filter(user=user, is_assistant=True)) > 0
|
||||
|
||||
|
||||
def check_teacher(user):
|
||||
return user.is_staff and check_login(user)
|
||||
|
||||
|
||||
def check_god(user):
|
||||
return user.is_superuser and check_login(user)
|
||||
|
||||
|
||||
def courses_available(user):
|
||||
if user.is_superuser:
|
||||
return Course.objects.all()
|
||||
else:
|
||||
return [s.course for s in Subscribe.objects.filter(user=user)]
|
||||
|
||||
|
||||
def blocks_available(user):
|
||||
courses = courses_available(user)
|
||||
blocks = {}
|
||||
is_admin = check_admin(user)
|
||||
for course in courses:
|
||||
if is_admin:
|
||||
blocks[course] = Block.objects.filter(
|
||||
course=course
|
||||
)
|
||||
else:
|
||||
blocks[course] = Block.objects.filter(
|
||||
opened=True,
|
||||
time_start__lte=timezone.now(),
|
||||
course=course
|
||||
)
|
||||
return blocks
|
||||
|
||||
|
||||
def can_send_solution(user, task):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
try:
|
||||
s = Subscribe.objects.get(course=task.block.course, user=user)
|
||||
except ObjectDoesNotExist:
|
||||
return False
|
||||
if s.is_assistant:
|
||||
return True
|
||||
return task.block.time_start <= timezone.now() <= task.block.time_end and task.max_solutions_count > len(Solution.objects.filter(user=user, task=task)) and task.block.opened
|
||||
|
||||
|
||||
def check_permission_block(user, block):
|
||||
blocks = blocks_available(user)
|
||||
for course in blocks.keys():
|
||||
if block in blocks[course]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_integer(x):
|
||||
try:
|
||||
int(x)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def check_admin_on_course(user, course):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
try:
|
||||
s = Subscribe.objects.get(user=user, course=course)
|
||||
except ObjectDoesNotExist:
|
||||
return False
|
||||
return s.is_assistant or user.is_staff
|
||||
|
||||
|
||||
def comparer(value1, value2):
|
||||
if value1 < value2:
|
||||
return 1
|
||||
elif value1 == value2:
|
||||
return 0
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def result_comparer(result1, result2):
|
||||
verdicts = ['IN QUEUE', 'TESTING', 'TEST ERROR', 'SOLUTION ERROR', 'Compilation error', 'Time limit']
|
||||
if result1 in verdicts and result2 in verdicts:
|
||||
return comparer(verdicts.index(result1), verdicts.index(result2))
|
||||
if result1 in verdicts and result2 not in verdicts:
|
||||
return 1
|
||||
if result1 not in verdicts and result2 in verdicts:
|
||||
return -1
|
||||
return comparer(int(result1.split('/')[0]), int(result2.split('/')[0]))
|
||||
|
||||
|
||||
def solutions_filter(request):
|
||||
try:
|
||||
solutions = list(reversed(Solution.objects.filter(task__block_id=request['block_id'])))
|
||||
except MultiValueDictKeyError as e:
|
||||
return [Solution.objects.get(id=request['id'])]
|
||||
if 'solution_id' in request.keys():
|
||||
solutions = [solution for solution in solutions if any([solution.id == int(i) for i in request['solution_id'].strip().split()])]
|
||||
if 'task_name' in request.keys():
|
||||
solutions = [solution for solution in solutions if solution.task.name == request['task_name']]
|
||||
if 'user' in request.keys():
|
||||
solutions = [solution for solution in solutions if str(solution.userinfo) == request['user']]
|
||||
if 'group' in request.keys():
|
||||
solutions = [solution for solution in solutions if solution.userinfo.group == request['group']]
|
||||
if 'best_result' in request.keys():
|
||||
sols = {}
|
||||
for solution in solutions:
|
||||
if (solution.user.username, solution.task.id) in sols.keys():
|
||||
comp = result_comparer(sols[(solution.user.username, solution.task.id)][0].result, solution.result)
|
||||
if comp == 1:
|
||||
sols[(solution.user.username, solution.task.id)] = [solution]
|
||||
elif comp == 0:
|
||||
sols[(solution.user.username, solution.task.id)].append(solution)
|
||||
else:
|
||||
sols[(solution.user.username, solution.task.id)] = [solution]
|
||||
solutions = []
|
||||
for sol in sols.values():
|
||||
for val in sol:
|
||||
solutions.append(val)
|
||||
solutions = list(sorted(solutions, key=lambda s: s.id, reverse=True))
|
||||
if 'last_solution' in request.keys():
|
||||
visited = []
|
||||
new_solutions = []
|
||||
for solution in solutions:
|
||||
if (solution.user.username, solution.task.id) not in visited:
|
||||
visited.append((solution.user.username, solution.task.id))
|
||||
new_solutions.append(solution)
|
||||
solutions = new_solutions
|
||||
if 'only_students' in request.keys():
|
||||
solutions = [solution for solution in solutions if not check_admin_on_course(solution.user, solution.task.block.course)]
|
||||
if 'not_seen' in request.keys():
|
||||
solutions = [solution for solution in solutions if solution.mark == None]
|
||||
return sorted(solutions, key=lambda s: s.id, reverse=True)
|
||||
|
||||
|
||||
def re_test(solutions_request, request):
|
||||
from .Tester import Tester
|
||||
for sol in solutions_request:
|
||||
sol.details = ''
|
||||
with open(sol.log_file, 'wb') as fs:
|
||||
fs.write(b'')
|
||||
sol.save()
|
||||
Thread(target=lambda: Tester(sol, request.META['HTTP_HOST']).push()).start()
|
||||
sleep(.1)
|
||||
|
||||
|
||||
def block_solutions_info(block):
|
||||
all_solutions = Solution.objects.filter(task__block=block)
|
||||
all_users = [solution.userinfo for solution in all_solutions]
|
||||
return {
|
||||
'tasks': sorted(list(set([solution.task for solution in all_solutions])), key=lambda x: x.name),
|
||||
'users': sorted(list(set(all_users)), key=lambda x: str(x)),
|
||||
'groups': sorted(list(set([userinfo.group for userinfo in all_users])), key=lambda x: str(x))
|
||||
}
|
||||
|
||||
|
||||
def delete_folder(path):
|
||||
flag = True
|
||||
while flag:
|
||||
try:
|
||||
rmtree(dirname(path))
|
||||
flag = False
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def solution_path(path):
|
||||
files = [x for x in listdir(path) if x.endswith('.sln') and not x.startswith('.')]
|
||||
if files:
|
||||
return path
|
||||
return ''.join([solution_path(join(path, file)) for file in listdir(path) if isdir(join(path, file))])
|
||||
|
||||
|
||||
def register_user(u):
|
||||
password = random_string()
|
||||
user = User.objects.create_user(username=u['email'], email=u['email'], password=password)
|
||||
UserInfo.objects.create(
|
||||
surname=u['surname'],
|
||||
name=u['name'],
|
||||
middle_name=u['middle_name'],
|
||||
group=u['group'],
|
||||
user=user
|
||||
)
|
||||
send_email('You have been registered in Sprint!', u['email'],
|
||||
'Your password is: {}\nPlease change it after login in settings!\nhttps://sprint.cshse.ru/'.format(password))
|
||||
return user
|
||||
|
||||
|
||||
def check_cheating(solutions, block, cheating_percent):
|
||||
block.cheating_checking = True
|
||||
block.save()
|
||||
try:
|
||||
cheating_data = {}
|
||||
cheating_path = join(MEDIA_ROOT, 'cheating', str(block.id))
|
||||
if exists(cheating_path):
|
||||
rmtree(cheating_path)
|
||||
mkdir(cheating_path)
|
||||
for solution in solutions:
|
||||
for file in solution.user_files.keys():
|
||||
user_file = join(MEDIA_ROOT, 'solutions', str(solution.id), file)
|
||||
dest_file = join(cheating_path, '_'.join([str(solution.id), basename(file)]))
|
||||
copyfile(user_file, dest_file)
|
||||
files_len = len(solutions)
|
||||
files = listdir(cheating_path)
|
||||
for i in range(len(files) - 1):
|
||||
for j in range(i + 1, len(files)):
|
||||
file1 = files[i]
|
||||
file2 = files[j]
|
||||
s1 = file1.split('_')
|
||||
s2 = file2.split('_')
|
||||
sol1 = Solution.objects.get(id=int(s1[0]))
|
||||
sol2 = Solution.objects.get(id=int(s2[0]))
|
||||
filename1 = '_'.join(s1[1:])
|
||||
filename2 = '_'.join(s2[1:])
|
||||
if sol1.user == sol2.user or sol1.task != sol2.task or filename1 != filename2:
|
||||
continue
|
||||
fp1 = copydetect.CodeFingerprint(join(cheating_path, file1), 25, 1)
|
||||
fp2 = copydetect.CodeFingerprint(join(cheating_path, file2), 25, 1)
|
||||
token_overlap, similarities, slices = copydetect.compare_files(fp1, fp2)
|
||||
similarity = (similarities[0] + similarities[1]) / 2
|
||||
if similarity >= cheating_percent / 100:
|
||||
if sol1.user.id not in cheating_data.keys():
|
||||
cheating_data[sol1.user.id] = []
|
||||
if sol2.user.id not in cheating_data.keys():
|
||||
cheating_data[sol2.user.id] = []
|
||||
cheating_data[sol1.user.id].append({
|
||||
'source': True,
|
||||
'solution': sol1.id,
|
||||
'file': filename1,
|
||||
'similar': sol2.id,
|
||||
'similarity': round(similarity * 100, 2)
|
||||
})
|
||||
cheating_data[sol2.user.id].append({
|
||||
'source': False,
|
||||
'solution': sol2.id,
|
||||
'file': filename2,
|
||||
'similar': sol1.id,
|
||||
'similarity': round(similarity * 100, 2)
|
||||
})
|
||||
finally:
|
||||
if exists(cheating_path):
|
||||
rmtree(cheating_path)
|
||||
with open(block.cheating_results_path, 'w') as fs:
|
||||
fs.write(dumps(cheating_data))
|
||||
block = Block.objects.get(id=block.id)
|
||||
block.cheating_checking = False
|
||||
block.save()
|
||||
print('finished')
|
||||
|
98
Main/migrations/0001_initial.py
Normal file
@ -0,0 +1,98 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-25 20:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Block',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.TextField()),
|
||||
('time_start', models.DateTimeField()),
|
||||
('time_end', models.DateTimeField()),
|
||||
('opened', models.IntegerField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Course',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserInfo',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('surname', models.TextField()),
|
||||
('name', models.TextField()),
|
||||
('middle_name', models.TextField()),
|
||||
('group_name', models.TextField()),
|
||||
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.TextField()),
|
||||
('legend', models.TextField()),
|
||||
('input', models.TextField()),
|
||||
('output', models.TextField()),
|
||||
('specifications', models.TextField()),
|
||||
('time_limit', models.IntegerField()),
|
||||
('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Block')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subscribe',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_assistant', models.IntegerField()),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Course')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Solution',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('result', models.TextField()),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Task')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Restore',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.TextField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Mark',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mark', models.IntegerField()),
|
||||
('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Block')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='block',
|
||||
name='course',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.Course'),
|
||||
),
|
||||
]
|
18
Main/migrations/0002_auto_20200626_0946.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-26 09:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='userinfo',
|
||||
old_name='group_name',
|
||||
new_name='group',
|
||||
),
|
||||
]
|
18
Main/migrations/0003_auto_20200627_1959.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-27 19:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0002_auto_20200626_0946'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='subscribe',
|
||||
name='is_assistant',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
38
Main/migrations/0004_auto_20200628_0917.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-28 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0003_auto_20200627_1959'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='input',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='legend',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='output',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='specifications',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='time_limit',
|
||||
field=models.IntegerField(default=10000),
|
||||
),
|
||||
]
|
18
Main/migrations/0005_solution_time_sent.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-28 10:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0004_auto_20200628_0917'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='time_sent',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
19
Main/migrations/0006_auto_20200628_1315.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-28 13:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0005_solution_time_sent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='solution',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.UserInfo'),
|
||||
),
|
||||
]
|
21
Main/migrations/0007_auto_20200629_0833.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.0.2 on 2020-06-29 08:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('Main', '0006_auto_20200628_1315'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='solution',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
23
Main/migrations/0008_auto_20200702_2140.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.2 on 2020-07-02 18:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0007_auto_20200629_0833'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userinfo',
|
||||
name='mark_notification',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userinfo',
|
||||
name='new_block_notification',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
23
Main/migrations/0009_auto_20200704_1703.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.2 on 2020-07-04 14:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0008_auto_20200702_2140'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='comment',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='mark',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
]
|
21
Main/migrations/0010_system.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.0.2 on 2020-07-24 10:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0009_auto_20200704_1703'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='System',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.TextField()),
|
||||
('value', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
23
Main/migrations/0011_auto_20200814_2035.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.0.2 on 2020-08-14 17:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0010_system'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ThreadSafe',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=80, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Mark',
|
||||
),
|
||||
]
|
27
Main/migrations/0012_auto_20200901_1154.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1 on 2020-09-01 08:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0011_auto_20200814_2035'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='weight',
|
||||
field=models.FloatField(default=1.0),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExtraFile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to='')),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')),
|
||||
],
|
||||
),
|
||||
]
|
16
Main/migrations/0013_delete_extrafile.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.1 on 2020-09-01 09:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0012_auto_20200901_1154'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='ExtraFile',
|
||||
),
|
||||
]
|
23
Main/migrations/0014_extrafile.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1 on 2020-09-01 09:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0013_delete_extrafile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExtraFile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to='')),
|
||||
('filename', models.TextField()),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')),
|
||||
],
|
||||
),
|
||||
]
|
23
Main/migrations/0015_auto_20200902_1555.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1 on 2020-09-02 12:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0014_extrafile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='max_mark',
|
||||
field=models.IntegerField(default=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='extrafile',
|
||||
name='file',
|
||||
field=models.FileField(upload_to='data\\extra_files'),
|
||||
),
|
||||
]
|
18
Main/migrations/0016_task_max_solutions_count.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-09-02 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0015_auto_20200902_1555'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='max_solutions_count',
|
||||
field=models.IntegerField(default=10),
|
||||
),
|
||||
]
|
18
Main/migrations/0017_solution_details.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-09-05 12:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0016_task_max_solutions_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='details',
|
||||
field=models.TextField(default=''),
|
||||
),
|
||||
]
|
17
Main/migrations/0018_remove_extrafile_file.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1 on 2020-09-17 08:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0017_solution_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='extrafile',
|
||||
name='file',
|
||||
),
|
||||
]
|
18
Main/migrations/0019_task_show_details.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-09-17 09:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0018_remove_extrafile_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='show_details',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
18
Main/migrations/0020_task_solution_type.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-10-08 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0019_task_show_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='solution_type',
|
||||
field=models.TextField(default='Решение'),
|
||||
),
|
||||
]
|
17
Main/migrations/0021_remove_task_solution_type.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1 on 2020-10-08 10:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0020_task_solution_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='solution_type',
|
||||
),
|
||||
]
|
18
Main/migrations/0022_task_full_solution.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-10-22 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0021_remove_task_solution_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='full_solution',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
18
Main/migrations/0023_extrafile_for_compilation.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-11-01 19:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0022_task_full_solution'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='extrafile',
|
||||
name='for_compilation',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
18
Main/migrations/0024_extrafile_sample.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1 on 2020-11-06 08:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0023_extrafile_for_compilation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='extrafile',
|
||||
name='sample',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
51
Main/migrations/0025_auto_20201106_1848.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Generated by Django 3.1 on 2020-11-06 15:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0024_extrafile_sample'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='userinfo',
|
||||
name='mark_notification',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='userinfo',
|
||||
name='new_block_notification',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='block',
|
||||
name='opened',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='extrafile',
|
||||
name='for_compilation',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='extrafile',
|
||||
name='sample',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subscribe',
|
||||
name='is_assistant',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='full_solution',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='show_details',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
18
Main/migrations/0026_block_show_rating.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-01 08:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0025_auto_20201106_1848'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='block',
|
||||
name='show_rating',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
18
Main/migrations/0027_task_mark_formula.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-26 13:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0026_block_show_rating'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='mark_formula',
|
||||
field=models.TextField(default='None'),
|
||||
),
|
||||
]
|
18
Main/migrations/0028_task_show_result.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-01 09:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0027_task_mark_formula'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='show_result',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
23
Main/migrations/0029_auto_20210130_1950.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.3 on 2021-01-30 16:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0028_task_show_result'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='block',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='priority',
|
||||
field=models.IntegerField(default=5),
|
||||
),
|
||||
]
|
27
Main/migrations/0030_message.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.3 on 2021-02-06 21:17
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('Main', '0029_auto_20210130_1950'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('for_all', models.BooleanField()),
|
||||
('text', models.TextField()),
|
||||
('reply_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='Main.message')),
|
||||
('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Main.task')),
|
||||
],
|
||||
),
|
||||
]
|
18
Main/migrations/0031_block_cheating_checking.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2021-03-13 08:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0030_message'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='block',
|
||||
name='cheating_checking',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
18
Main/migrations/0032_block_cheating_data.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2021-03-14 12:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0031_block_cheating_checking'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='block',
|
||||
name='cheating_data',
|
||||
field=models.TextField(default='[]'),
|
||||
),
|
||||
]
|
17
Main/migrations/0033_remove_block_cheating_data.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.3 on 2021-03-14 13:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Main', '0032_block_cheating_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='block',
|
||||
name='cheating_data',
|
||||
),
|
||||
]
|
0
Main/migrations/__init__.py
Normal file
471
Main/models.py
Normal file
@ -0,0 +1,471 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_delete
|
||||
from os.path import sep, join, exists
|
||||
from os import remove
|
||||
|
||||
from Main.commands import shell
|
||||
from Sprint.settings import MEDIA_ROOT
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from json import loads
|
||||
|
||||
|
||||
base_dir = 'data'
|
||||
|
||||
|
||||
class ThreadSafe(models.Model):
|
||||
key = models.CharField(max_length=80, unique=True)
|
||||
|
||||
|
||||
class Course(models.Model):
|
||||
name = models.TextField()
|
||||
|
||||
@property
|
||||
def teachers(self):
|
||||
return [UserInfo.objects.get(user=s.user) for s in Subscribe.objects.filter(user__is_staff=1, course=self)]
|
||||
|
||||
@property
|
||||
def subscribes(self):
|
||||
return sorted(Subscribe.objects.filter(course=self), key=lambda s: s.user.email)
|
||||
|
||||
|
||||
@property
|
||||
def students(self):
|
||||
userinfo = lambda sub: sub.user.userinfo
|
||||
return sorted(Subscribe.objects.filter(course=self, is_assistant=False, user__is_staff=False), key=lambda s: userinfo(s).surname + userinfo(s).name + userinfo(s).middle_name)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Block(models.Model):
|
||||
name = models.TextField()
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE)
|
||||
time_start = models.DateTimeField()
|
||||
time_end = models.DateTimeField()
|
||||
opened = models.BooleanField(default=False)
|
||||
show_rating = models.BooleanField(default=True)
|
||||
priority = models.IntegerField(default=5)
|
||||
cheating_checking = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
return Message.objects.filter(task__block=self)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def tasks(self):
|
||||
return Task.objects.filter(block=self)
|
||||
|
||||
@property
|
||||
def time_start_chrome(self):
|
||||
return self.time_start.strftime("%Y-%m-%dT%H:%M")
|
||||
|
||||
@property
|
||||
def time_end_chrome(self):
|
||||
return self.time_end.strftime("%Y-%m-%dT%H:%M")
|
||||
|
||||
@property
|
||||
def is_opened(self):
|
||||
return 'checked' if self.opened else ''
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
return reversed(Solution.objects.filter(task__block=self))
|
||||
|
||||
@property
|
||||
def subscribed_users(self):
|
||||
return [UserInfo.objects.get(user=s.user) for s in Subscribe.objects.filter(course=self.course)]
|
||||
|
||||
@property
|
||||
def cheating_results_path(self):
|
||||
return join(MEDIA_ROOT, 'cheating_results', str(self.id))
|
||||
|
||||
@property
|
||||
def cheating_checked(self):
|
||||
return self.cheating_results != {}
|
||||
|
||||
@property
|
||||
def cheating_results(self):
|
||||
return loads(open(self.cheating_results_path, 'r').read()) if exists(self.cheating_results_path) else {}
|
||||
|
||||
@property
|
||||
def cheating_status(self):
|
||||
if self.cheating_checking:
|
||||
return 'Идет проверка'
|
||||
if not exists(self.cheating_results_path):
|
||||
return 'Еще не проверено'
|
||||
return 'Проверка завершена'
|
||||
|
||||
|
||||
class Restore(models.Model):
|
||||
code = models.TextField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class Subscribe(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE)
|
||||
is_assistant = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username + '|' + self.course.name
|
||||
|
||||
@property
|
||||
def userinfo(self):
|
||||
return UserInfo.objects.get(user=self.user)
|
||||
|
||||
@property
|
||||
def role(self):
|
||||
if self.user.is_superuser:
|
||||
return 'Администратор'
|
||||
if self.user.is_staff:
|
||||
return 'Преподаватель'
|
||||
return 'Ассистент' if self.is_assistant else 'Студент'
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
name = models.TextField()
|
||||
block = models.ForeignKey(Block, on_delete=models.CASCADE)
|
||||
legend = models.TextField(default='')
|
||||
input = models.TextField(default='')
|
||||
output = models.TextField(default='')
|
||||
specifications = models.TextField(default='')
|
||||
time_limit = models.IntegerField(default=10000)
|
||||
weight = models.FloatField(default=1.0)
|
||||
max_mark = models.IntegerField(default=10)
|
||||
max_solutions_count = models.IntegerField(default=10)
|
||||
show_result = models.BooleanField(default=True)
|
||||
show_details = models.BooleanField(default=False)
|
||||
full_solution = models.BooleanField(default=False)
|
||||
mark_formula = models.TextField(default='None')
|
||||
priority = models.IntegerField(default=5)
|
||||
|
||||
@property
|
||||
def students_solutions(self):
|
||||
students = [sub.user for sub in Subscribe.objects.filter(course=self.block.course)]
|
||||
solutions = Solution.objects.filter(task=self)
|
||||
return [sol for sol in solutions if sol.user in students]
|
||||
|
||||
@property
|
||||
def correct_count(self):
|
||||
solutions = self.students_solutions
|
||||
count = 0
|
||||
for sol in solutions:
|
||||
res = sol.result.split('/')
|
||||
if len(res) == 2 and res[0] == res[1]:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@property
|
||||
def solutions_count(self):
|
||||
return len(self.students_solutions)
|
||||
|
||||
@property
|
||||
def partially_passed(self):
|
||||
solutions = self.students_solutions
|
||||
count = 0
|
||||
for sol in solutions:
|
||||
res = sol.result.split('/')
|
||||
if len(res) == 2 and res[0] != res[1]:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@property
|
||||
def solutions_with_error(self):
|
||||
return self.solutions_count - self.correct_count - self.partially_passed
|
||||
|
||||
@property
|
||||
def samples(self):
|
||||
return [{
|
||||
'input': file,
|
||||
'output': file.answer
|
||||
} for file in ExtraFile.objects.filter(task=self, sample=True).order_by('filename')]
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def showable(self):
|
||||
return 'checked' if self.show_details else ''
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def tests_path(self):
|
||||
return join(base_dir, 'tests', str(self.id) + '.cs')
|
||||
|
||||
@property
|
||||
def tests_text(self):
|
||||
try:
|
||||
return open(self.tests_path(), 'r').read()
|
||||
except FileNotFoundError:
|
||||
with open(self.tests_path(), 'w') as fs:
|
||||
pass
|
||||
return ''
|
||||
|
||||
@property
|
||||
def tests_uploaded(self):
|
||||
from os.path import exists
|
||||
return exists(self.tests_path())
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
return ExtraFile.objects.filter(task=self).order_by('filename')
|
||||
|
||||
@property
|
||||
def files_for_compilation(self):
|
||||
return ExtraFile.objects.filter(task=self, for_compilation=True)
|
||||
|
||||
|
||||
@property
|
||||
def is_full_solution(self):
|
||||
return 'checked' if self.full_solution else ''
|
||||
|
||||
|
||||
class UserInfo(models.Model):
|
||||
surname = models.TextField()
|
||||
name = models.TextField()
|
||||
middle_name = models.TextField()
|
||||
group = models.TextField()
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True)
|
||||
|
||||
def __eq__(self, obj):
|
||||
return str(self) == str(obj)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return "{} {} {}".format(self.surname, self.name, self.middle_name)
|
||||
|
||||
|
||||
class Solution(models.Model):
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
result = models.TextField()
|
||||
details = models.TextField(default='')
|
||||
time_sent = models.DateTimeField(null=True)
|
||||
mark = models.IntegerField(null=True)
|
||||
comment = models.TextField(default='')
|
||||
|
||||
def set_result(self, result):
|
||||
self.result = result
|
||||
if len(result.split('/')) != 1:
|
||||
result = int(result.split('/')[0])
|
||||
try:
|
||||
self.mark = eval(self.task.mark_formula)
|
||||
except:
|
||||
self.mark = None
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
def path(self):
|
||||
return join(base_dir, 'solutions', str(self.id))
|
||||
|
||||
def write_log(self, text):
|
||||
with self.log_fs as fs:
|
||||
fs.write(bytes(text + '\n', 'cp866'))
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return join(MEDIA_ROOT, 'logs', str(self.id) + '.log')
|
||||
|
||||
@property
|
||||
def log_text(self):
|
||||
try:
|
||||
return open(self.log_file, 'rb').read().decode('cp866')
|
||||
except FileNotFoundError:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def log_fs(self):
|
||||
return open(self.log_file, 'ab')
|
||||
|
||||
@property
|
||||
def userinfo(self):
|
||||
return UserInfo.objects.get(user=self.user)
|
||||
|
||||
@property
|
||||
def mark_property(self):
|
||||
return str(self.mark) if self.mark is not None else 'нет оценки'
|
||||
|
||||
@property
|
||||
def mark_select(self):
|
||||
line = ''
|
||||
if self.mark:
|
||||
line += '<option value="нет оценки">нет оценки</option>'
|
||||
else:
|
||||
line += '<option value="нет оценки" selected>нет оценки</option>'
|
||||
for mark in range(self.task.max_mark + 1):
|
||||
if mark == self.mark:
|
||||
line += '<option value="{}" selected>{}</option>'.format(mark, mark)
|
||||
else:
|
||||
line += '<option value="{}">{}</option>'.format(mark, mark)
|
||||
return line
|
||||
|
||||
@property
|
||||
def comment_property(self):
|
||||
return self.comment if self.comment else 'нет комментария'
|
||||
|
||||
@staticmethod
|
||||
def get_files(path):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
files_dict = {}
|
||||
for file in listdir(path):
|
||||
if file == '__MACOSX' or file == 'test_folder' or file == 'bin' or file == 'obj' or file == '.vs':
|
||||
continue
|
||||
current_file = join(path, file)
|
||||
if isfile(current_file):
|
||||
if not current_file.endswith('.csproj') and not current_file.endswith('.sln'):
|
||||
try:
|
||||
files_dict[sep.join(current_file.split('solutions' + sep)[1].split(sep)[1:])] \
|
||||
= open(current_file, 'rb').read().decode('UTF-8')
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
else:
|
||||
files_dict = {**files_dict, **Solution.get_files(current_file)}
|
||||
return files_dict
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
return Solution.get_files(self.path())
|
||||
|
||||
@property
|
||||
def user_files(self):
|
||||
f = {}
|
||||
comp_files = [ef.filename for ef in ExtraFile.objects.filter(task=self.task, for_compilation=True)]
|
||||
for fi in self.files.keys():
|
||||
if not fi in comp_files:
|
||||
f[fi] = self.files[fi]
|
||||
return f
|
||||
|
||||
@property
|
||||
def passed_all_tests(self):
|
||||
spl = self.result.split('/')
|
||||
return len(spl) == 2 and spl[0] == spl[1]
|
||||
|
||||
class System(models.Model):
|
||||
key = models.TextField()
|
||||
value = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
|
||||
class ExtraFile(models.Model):
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||
filename = models.TextField()
|
||||
for_compilation = models.BooleanField(default=False)
|
||||
sample = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def answer(self):
|
||||
try:
|
||||
return ExtraFile.objects.get(task=self.task, filename=self.filename + '.a')
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@property
|
||||
def num(self):
|
||||
try:
|
||||
return int(self.filename.split('.')[0])
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def is_for_compilation(self):
|
||||
return 'checked' if self.for_compilation else ''
|
||||
|
||||
@property
|
||||
def is_sample(self):
|
||||
return 'checked' if self.sample else ''
|
||||
|
||||
@property
|
||||
def can_be_sample(self):
|
||||
try:
|
||||
int(self.filename)
|
||||
except:
|
||||
return False
|
||||
try:
|
||||
ans = ExtraFile.objects.get(task=self.task, filename=self.filename + '.a')
|
||||
except ObjectDoesNotExist:
|
||||
return False
|
||||
return self.readable and ans.readable
|
||||
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return join(MEDIA_ROOT, 'extra_files', str(self.id))
|
||||
|
||||
|
||||
@property
|
||||
def readable(self):
|
||||
try:
|
||||
open(self.path, 'rb').read().decode('UTF-8')
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return open(self.path, 'rb').read().decode('UTF-8')
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.filename
|
||||
|
||||
|
||||
def write(self, data):
|
||||
with open(self.path, 'wb') as fs:
|
||||
fs.write(data)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
|
||||
reply_to = models.ForeignKey('Message', on_delete=models.CASCADE, null=True)
|
||||
for_all = models.BooleanField()
|
||||
text = models.TextField()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Task)
|
||||
def delete_task_hook(sender, instance, using, **kwargs):
|
||||
if exists(instance.tests_path()):
|
||||
from os import remove
|
||||
remove(instance.tests_path())
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Solution)
|
||||
def delete_solution_hook(sender, instance, using, **kwargs):
|
||||
if exists(instance.path()):
|
||||
from shutil import rmtree
|
||||
rmtree(instance.path())
|
||||
shell('docker rm --force solution_container_{}'.format(instance.id))
|
||||
shell('docker image rm solution_{}'.format(instance.id))
|
||||
|
||||
|
||||
@receiver(post_delete, sender=ExtraFile)
|
||||
def delete_file_hook(sender, instance, using, **kwargs):
|
||||
try:
|
||||
if exists(instance.path):
|
||||
remove(instance.path)
|
||||
except ValueError:
|
||||
pass
|
||||
if instance.filename.endswith('.a'):
|
||||
try:
|
||||
t = ExtraFile.objects.get(task=instance.task, filename=instance.filename[:-2])
|
||||
except ObjectDoesNotExist:
|
||||
return
|
||||
t.sample = False
|
||||
t.save()
|
11
Main/static/css/styles.css
Normal file
@ -0,0 +1,11 @@
|
||||
.main_div {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
.top-buffer {
|
||||
margin-bottom:15px;
|
||||
}
|
||||
.top-button {
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
}
|
BIN
Main/static/files/Darwin.dll
Normal file
BIN
Main/static/img/admin_block_page.png
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
Main/static/img/admin_page.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
Main/static/img/block_page.png
Normal file
After Width: | Height: | Size: 103 KiB |
1
Main/static/img/icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="random" class="svg-inline--fa fa-random fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.971 359.029c9.373 9.373 9.373 24.569 0 33.941l-80 79.984c-15.01 15.01-40.971 4.49-40.971-16.971V416h-58.785a12.004 12.004 0 0 1-8.773-3.812l-70.556-75.596 53.333-57.143L352 336h32v-39.981c0-21.438 25.943-31.998 40.971-16.971l80 79.981zM12 176h84l52.781 56.551 53.333-57.143-70.556-75.596A11.999 11.999 0 0 0 122.785 96H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12zm372 0v39.984c0 21.46 25.961 31.98 40.971 16.971l80-79.984c9.373-9.373 9.373-24.569 0-33.941l-80-79.981C409.943 24.021 384 34.582 384 56.019V96h-58.785a12.004 12.004 0 0 0-8.773 3.812L96 336H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h110.785c3.326 0 6.503-1.381 8.773-3.812L352 176h32z"></path></svg>
|
After Width: | Height: | Size: 904 B |
BIN
Main/static/img/main_page.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
Main/static/img/solution_page.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
Main/static/img/solutions_page.png
Normal file
After Width: | Height: | Size: 245 KiB |
BIN
Main/static/img/task_page.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
Main/static/img/task_settings_page_1.png
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
Main/static/img/task_settings_page_2.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
Main/static/img/test_datails.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Main/static/img/upload_rules.png
Normal file
After Width: | Height: | Size: 448 KiB |
BIN
Main/static/img/users_settings_page.png
Normal file
After Width: | Height: | Size: 282 KiB |
12
Main/static/js/scripts.js
Normal file
@ -0,0 +1,12 @@
|
||||
function main() {
|
||||
window.location.href = "/main"
|
||||
}
|
||||
function logout() {
|
||||
window.location.href = "/exit"
|
||||
}
|
||||
function settings() {
|
||||
window.location.href = "/settings"
|
||||
}
|
||||
function admin() {
|
||||
window.location.href = '../..'
|
||||
}
|
0
Main/templatetags/__init__.py
Normal file
112
Main/templatetags/filters.py
Normal file
@ -0,0 +1,112 @@
|
||||
from django import template
|
||||
from Main.models import Solution, Task, UserInfo
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter('mark_for_task')
|
||||
def mark_for_task(task, user):
|
||||
try:
|
||||
return round(list(Solution.objects.filter(task=task, user=user, mark__isnull=False))[-1].mark * 10 / task.max_mark)
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
|
||||
@register.filter('mark_for_block')
|
||||
def mark_for_block(block, user):
|
||||
tasks = Task.objects.filter(block=block)
|
||||
mark = 0
|
||||
for task in tasks:
|
||||
mft = mark_for_task(task, user)
|
||||
mark += mft * task.weight
|
||||
return round(mark)
|
||||
|
||||
|
||||
@register.filter('marked')
|
||||
def marked(mark):
|
||||
return mark != -1
|
||||
|
||||
|
||||
@register.filter('mark_color')
|
||||
def mark_color(mark):
|
||||
mark = round(mark)
|
||||
if mark > 7:
|
||||
return '#00FF00'
|
||||
elif mark > 5:
|
||||
return '#FFFF00'
|
||||
elif mark > 3:
|
||||
return '#FAD7A0'
|
||||
elif mark > 0:
|
||||
return '#F1948A'
|
||||
else:
|
||||
return '#FFFFFF'
|
||||
|
||||
|
||||
@register.filter('in_dict')
|
||||
def in_dict(value, dict):
|
||||
return value in dict.keys()
|
||||
|
||||
|
||||
@register.filter('last_attempts')
|
||||
def last_attempts(user, task):
|
||||
return task.max_solutions_count - len(Solution.objects.filter(task=task, user=user))
|
||||
|
||||
|
||||
@register.filter('userinfo_by_user')
|
||||
def userinfo_by_user(user):
|
||||
return UserInfo.objects.get(user=user)
|
||||
|
||||
|
||||
@register.filter('mark_status')
|
||||
def mark_status(user, task):
|
||||
sols = Solution.objects.filter(user=user, task=task)
|
||||
if len(sols) == 0:
|
||||
return '-'
|
||||
return sols.last().result
|
||||
|
||||
|
||||
@register.filter('fully_marked')
|
||||
def fully_marked(user, task):
|
||||
return len(Solution.objects.filter(user=user, task=task, mark=None)) == 0
|
||||
|
||||
|
||||
@register.filter('is_code')
|
||||
def is_code(path):
|
||||
return path.endswith('.cs')
|
||||
|
||||
|
||||
@register.filter('num_range')
|
||||
def num_range(n):
|
||||
return range(1, n + 1)
|
||||
|
||||
|
||||
@register.filter('length')
|
||||
def length(collection):
|
||||
return len(collection)
|
||||
|
||||
|
||||
@register.filter('user_by_id')
|
||||
def user_by_id(user_id):
|
||||
return User.objects.get(id=user_id)
|
||||
|
||||
|
||||
@register.filter('dict_key')
|
||||
def dict_key(d, k):
|
||||
return d[k]
|
||||
|
||||
|
||||
@register.filter('solution_by_id')
|
||||
def solution_by_id(solution_id):
|
||||
return Solution.objects.get(id=solution_id)
|
||||
|
||||
|
||||
@register.filter('solution_file_text')
|
||||
def solution_file_text(solution, filename):
|
||||
files = solution.user_files
|
||||
for key in files.keys():
|
||||
value = files[key]
|
||||
if key.endswith(filename):
|
||||
return value
|
||||
raise Exception(f'No such file for solution {solution.id} and filename {filename}')
|
3
Main/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
787
Main/views.py
Normal file
@ -0,0 +1,787 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from json import load, dumps, loads
|
||||
from os import remove, mkdir, listdir, rename
|
||||
from os.path import sep, join, exists, isfile, dirname
|
||||
from shutil import rmtree, copytree, make_archive, copyfile
|
||||
from threading import Thread
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth import login, authenticate, logout
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from Main.templatetags.filters import *
|
||||
from Sprint.settings import MEDIA_ROOT
|
||||
from .Tester import Tester
|
||||
from .forms import *
|
||||
from .main import solutions_filter, check_admin_on_course, re_test, check_admin, check_teacher, random_string, \
|
||||
send_email, check_permission_block, is_integer, check_god, blocks_available, check_login, \
|
||||
get_restore_hash, block_solutions_info, solution_path, can_send_solution, get_in_html_tag, register_user, \
|
||||
check_cheating, result_comparer
|
||||
from .models import Block, Subscribe, Course, Restore, ExtraFile, Message
|
||||
|
||||
|
||||
def download_rating(request):
|
||||
block = Block.objects.get(id=request.GET['block_id'])
|
||||
if not check_admin_on_course(request.user, block.course):
|
||||
return HttpResponseRedirect('/main')
|
||||
s = 'ФИО,'
|
||||
tasks = block.tasks
|
||||
users = [UserInfo.objects.get(user=user.user) for user in Subscribe.objects.filter(course=block.course, is_assistant=False, user__is_staff=False)]
|
||||
for task in tasks:
|
||||
try:
|
||||
s += task.name.split('.')[0] + ','
|
||||
except IndexError:
|
||||
s += task.name + ','
|
||||
s += 'Score,Summ\n'
|
||||
for user in users:
|
||||
s += str(user) + ','
|
||||
for task in tasks:
|
||||
s += str(mark_for_task(task, user.user)) + ','
|
||||
s += '0,' + str(mark_for_block(block, user.user)) + '\n'
|
||||
response = HttpResponse(bytes(s, encoding='utf-8'), content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'inline; filename=rating.csv'
|
||||
return response
|
||||
|
||||
|
||||
def download(request):
|
||||
if not check_admin(request.user) or not check_admin_on_course(request.user, Block.objects.get(id=request.GET['block_id']).course):
|
||||
return HttpResponseRedirect('/main')
|
||||
sols = solutions_filter(request.GET)
|
||||
if len(sols) == 0:
|
||||
return HttpResponseRedirect('/admin/solutions?block_id=' + request.GET['block_id'])
|
||||
new_folder = join(MEDIA_ROOT, request.user.username)
|
||||
if exists(new_folder):
|
||||
try:
|
||||
rmtree(new_folder)
|
||||
except:
|
||||
remove(new_folder)
|
||||
mkdir(new_folder)
|
||||
cur_folder = join(new_folder, 'solutions')
|
||||
mkdir(cur_folder)
|
||||
for sol in sols:
|
||||
uinfo = UserInfo.objects.get(user=sol.user)
|
||||
folder = join(cur_folder, str(uinfo) + '-' + str(uinfo.user.id))
|
||||
if not exists(folder):
|
||||
mkdir(folder)
|
||||
files = sol.files
|
||||
files_for_compilation = [f.filename for f in sol.task.files_for_compilation]
|
||||
for f in files.copy().keys():
|
||||
if f.split(sep)[-1] in files_for_compilation:
|
||||
del files[f]
|
||||
if len(files.keys()) == 1:
|
||||
dest = join(folder, "-".join([sol.task.name.split('.')[0], str(sol.id), 'dotnet', sol.result.replace('/', 'from')])) + '.cs'
|
||||
source = join(sol.path(), list(files.keys())[0])
|
||||
with open(dest, 'wb') as fs:
|
||||
fs.write(open(source, 'rb').read())
|
||||
else:
|
||||
newname = join(folder, '-'.join([sol.task.name.split('.')[0], str(sol.id), 'dotnet', sol.result])).replace('/', 'from')
|
||||
mkdir(newname)
|
||||
for f in files.keys():
|
||||
with open(join(newname, f.split(sep)[-1]), 'wb') as fs:
|
||||
fs.write(open(join(sol.path(), f), 'rb').read())
|
||||
make_archive(newname, 'zip', newname)
|
||||
rmtree(newname)
|
||||
zip_folder = join(dirname(cur_folder), 'solutions')
|
||||
make_archive(zip_folder, 'zip', cur_folder)
|
||||
response = HttpResponse(open(zip_folder + '.zip', 'rb').read(), content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'inline; filename=solutions.zip'
|
||||
rmtree(dirname(cur_folder))
|
||||
return response
|
||||
|
||||
|
||||
def queue(request):
|
||||
block = Block.objects.get(id=request.GET['block_id'])
|
||||
if not check_admin_on_course(request.user, block.course):
|
||||
return HttpResponseRedirect('/main')
|
||||
return render(request, 'queue.html', {'Block': block})
|
||||
|
||||
|
||||
def docs(request):
|
||||
if not check_admin(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
return render(request, "docs.html", context={'is_teacher': check_teacher(request.user)})
|
||||
|
||||
|
||||
def retest(request):
|
||||
solutions_request = solutions_filter(request.GET)
|
||||
if not check_admin_on_course(request.user, Block.objects.get(id=request.GET['block_id']).course):
|
||||
return HttpResponseRedirect('/main')
|
||||
req = '?block_id=' + str(request.GET['block_id'])
|
||||
for key in request.GET.keys():
|
||||
if key != 'block_id':
|
||||
req += '&{}={}'.format(key, request.GET[key])
|
||||
Thread(target=lambda: re_test(solutions_request, request)).start()
|
||||
if 'next' in request.GET.keys():
|
||||
return HttpResponseRedirect(request.GET['next'])
|
||||
return HttpResponseRedirect('/admin/solutions%s' % req)
|
||||
|
||||
|
||||
def solution(request):
|
||||
current_solution = Solution.objects.get(id=request.GET['id'])
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect('/main')
|
||||
if current_solution.user != request.user:
|
||||
try:
|
||||
Subscribe.objects.get(user=request.user, is_assistant=True, course=current_solution.task.block.course)
|
||||
except:
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect('/main')
|
||||
can_edit = check_admin_on_course(request.user, current_solution.task.block.course)
|
||||
if not can_edit:
|
||||
# тут по хорошему надо использовать регулярки, но я что-то не разобрался
|
||||
while True:
|
||||
i = current_solution.details.find('<pre>')
|
||||
if i == -1:
|
||||
break
|
||||
j = current_solution.details.find('</pre>') + 6
|
||||
current_solution.details = current_solution.details.replace(current_solution.details[i:j], '')
|
||||
if current_solution.user != request.user:
|
||||
return HttpResponseRedirect('/main')
|
||||
solutions_request = solutions_filter(request.GET)
|
||||
if request.path == '/admin/solution':
|
||||
from_admin = True
|
||||
else:
|
||||
from_admin = False
|
||||
if request.method == 'POST' and can_edit:
|
||||
if request.POST['action'] == 'Зачесть':
|
||||
current_solution.mark = None if request.POST['mark'] == 'нет оценки' else int(request.POST['mark'])
|
||||
elif request.POST['action'] == 'Незачесть':
|
||||
current_solution.mark = 0
|
||||
else:
|
||||
current_solution.mark = current_solution.task.max_mark
|
||||
current_solution.comment = request.POST['comment']
|
||||
current_solution.save()
|
||||
return render(request, 'solution.html', context={'solution': current_solution,
|
||||
'from_admin': from_admin,
|
||||
'can_edit': can_edit,
|
||||
'path': request.path})
|
||||
|
||||
|
||||
def solutions(request):
|
||||
current_block = Block.objects.get(id=request.GET['block_id'])
|
||||
try:
|
||||
if not request.user.is_superuser:
|
||||
s = Subscribe.objects.get(user=request.user, course=current_block.course)
|
||||
if not s.is_assistant and not s.user.is_staff:
|
||||
return HttpResponseRedirect('/main')
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponseRedirect('/main')
|
||||
req = ''
|
||||
sols = solutions_filter(request.GET)
|
||||
for key in request.GET.keys():
|
||||
req += '&{}={}'.format(key, request.GET[key])
|
||||
if request.method == 'POST':
|
||||
Solution.objects.get(id=request.POST['DELETE_SOLUTION']).delete()
|
||||
return HttpResponseRedirect('/admin/solutions?block_id={}{}'.format(current_block.id, req))
|
||||
return render(request, 'solutions.html', context={'Block': current_block,
|
||||
'filter': ' '.join([str(sol.id) for sol in sols]),
|
||||
'solutions': sols,
|
||||
'req': req,
|
||||
'options': {key: request.GET[key] for key in request.GET.keys()},
|
||||
'solutions_info': block_solutions_info(current_block)})
|
||||
|
||||
|
||||
def messages(request):
|
||||
return HttpResponseRedirect('/main')
|
||||
current_block = Block.objects.get(id=request.GET['block_id'])
|
||||
return render(request, 'messages.html', context={'Block': current_block})
|
||||
|
||||
|
||||
def users_settings(request):
|
||||
current_course = Course.objects.get(id=request.GET['course_id'])
|
||||
if not check_admin_on_course(request.user, current_course):
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect('/main')
|
||||
if request.method == 'POST':
|
||||
if 'input' in request.POST.keys():
|
||||
line = request.POST['input']
|
||||
if '@' in line:
|
||||
users = UserInfo.objects.filter(user__email=line)
|
||||
elif any(c.isdigit() for c in line):
|
||||
users = UserInfo.objects.filter(group=line)
|
||||
else:
|
||||
try:
|
||||
s, n, m = line.split(' ')
|
||||
except ValueError:
|
||||
s, n, m = '', '', ''
|
||||
users = list(UserInfo.objects.filter(surname=s, name=n, middle_name=m)) + list(UserInfo.objects.filter(group=line))
|
||||
for user in users:
|
||||
try:
|
||||
Subscribe.objects.get(user=user.user, course=current_course)
|
||||
except ObjectDoesNotExist:
|
||||
Subscribe.objects.create(user=user.user, course=current_course)
|
||||
elif 'user_delete' in request.POST.keys():
|
||||
username = request.POST['user_delete']
|
||||
course_id = request.GET['course_id']
|
||||
Subscribe.objects.get(user__email=username, course_id=course_id).delete()
|
||||
elif 'file' in request.FILES.keys():
|
||||
users = load(request.FILES['file'])
|
||||
for u in users:
|
||||
password = random_string()
|
||||
flag = False
|
||||
try:
|
||||
user = User.objects.get(email=u['email'])
|
||||
except ObjectDoesNotExist:
|
||||
flag = True
|
||||
if flag:
|
||||
user = register_user(u)
|
||||
try:
|
||||
Subscribe.objects.get(user=user, course=current_course)
|
||||
except ObjectDoesNotExist:
|
||||
Subscribe.objects.create(user=user, course=current_course, is_assistant=False)
|
||||
else:
|
||||
key = list(request.POST.keys())[1]
|
||||
role = request.POST[key]
|
||||
username = '_'.join(key.split('_')[1:])
|
||||
s = Subscribe.objects.get(user__email=username, course=current_course)
|
||||
s.is_assistant = role == 'Ассистент'
|
||||
s.save()
|
||||
return HttpResponseRedirect('/admin/users_settings?course_id=' + str(current_course.id))
|
||||
return render(request, 'users_settings.html', context={'course': current_course})
|
||||
|
||||
|
||||
def task(request):
|
||||
current_task = Task.objects.get(id=request.GET['id'])
|
||||
user = request.user
|
||||
if not check_permission_block(user, current_task.block):
|
||||
return HttpResponseRedirect('/main')
|
||||
can_send = can_send_solution(user, current_task)
|
||||
if request.method == 'POST':
|
||||
if 'message' in request.POST.keys():
|
||||
Message.objects.create(
|
||||
task=current_task,
|
||||
sender=request.user,
|
||||
reply_to=None,
|
||||
for_all=False,
|
||||
text=request.POST['message']
|
||||
)
|
||||
return HttpResponseRedirect('/task?id=' + str(current_task.id))
|
||||
if 'file' in request.FILES.keys() and can_send:
|
||||
current_solution = Solution.objects.create(
|
||||
task=current_task,
|
||||
user=request.user,
|
||||
result='IN QUEUE',
|
||||
time_sent=timezone.now()
|
||||
)
|
||||
log_file_path = current_solution.log_file
|
||||
with open(log_file_path, 'wb') as fs:
|
||||
pass
|
||||
solution_dir = current_solution.path() + sep
|
||||
if exists(solution_dir):
|
||||
rmtree(solution_dir)
|
||||
mkdir(solution_dir)
|
||||
with open(solution_dir + 'solution.zip', 'wb') as fs:
|
||||
for chunk in request.FILES['file'].chunks():
|
||||
fs.write(chunk)
|
||||
flag = False
|
||||
solution_created = False
|
||||
try:
|
||||
with ZipFile(solution_dir + 'solution.zip') as obj:
|
||||
obj.extractall(solution_dir)
|
||||
except BadZipFile:
|
||||
rename(solution_dir + 'solution.zip', solution_dir + request.FILES['file'].name)
|
||||
sln_path = solution_path(solution_dir)
|
||||
if current_task.full_solution != bool(sln_path):
|
||||
current_solution.result = 'TEST ERROR'
|
||||
with open(log_file_path, 'ab') as fs:
|
||||
fs.write(b'Can\'t find sln file in solution' if current_task.full_solution else b'Sln file in path')
|
||||
current_solution.save()
|
||||
return HttpResponseRedirect('/task?id=' + str(current_task.id))
|
||||
if not bool(sln_path):
|
||||
copytree('SampleSolution', join(solution_dir, 'Solution'))
|
||||
for file in listdir(solution_dir):
|
||||
if file == 'solution.zip' or file == 'Solution':
|
||||
continue
|
||||
cur_file = join(solution_dir, file)
|
||||
if isfile(cur_file):
|
||||
copyfile(cur_file, join(solution_dir, 'Solution', 'SampleProject', file))
|
||||
remove(cur_file)
|
||||
else:
|
||||
rmtree(cur_file)
|
||||
if not current_task.full_solution:
|
||||
for file in current_task.files_for_compilation:
|
||||
copyfile(file.path, join(solution_dir, 'Solution', 'SampleProject', file.filename))
|
||||
#Tester(current_solution, request.META['HTTP_HOST']).push()
|
||||
Thread(target=lambda: Tester(current_solution, request.META['HTTP_HOST']).push()).start()
|
||||
return HttpResponseRedirect('/task?id=' + str(current_task.id))
|
||||
return render(request, 'task.html', context={'task': current_task,
|
||||
'solutions': reversed(Solution.objects.filter(task=current_task, user=user)),
|
||||
'can_send': can_send,
|
||||
'can_edit': check_admin_on_course(request.user, current_task.block.course)})
|
||||
|
||||
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def task_test(request):
|
||||
current_task = Task.objects.get(id=request.GET['id'])
|
||||
user = request.user
|
||||
if not check_permission_block(user, current_task.block):
|
||||
return HttpResponseRedirect('/main')
|
||||
can_send = can_send_solution(user, current_task)
|
||||
if request.method == 'POST':
|
||||
if 'file' in request.FILES.keys() and can_send:
|
||||
current_solution = Solution.objects.create(
|
||||
task=current_task,
|
||||
user=request.user,
|
||||
result='IN QUEUE',
|
||||
time_sent=timezone.now()
|
||||
)
|
||||
log_file_path = current_solution.log_file
|
||||
with open(log_file_path, 'wb') as fs:
|
||||
pass
|
||||
solution_dir = current_solution.path() + sep
|
||||
if exists(solution_dir):
|
||||
rmtree(solution_dir)
|
||||
mkdir(solution_dir)
|
||||
with open(solution_dir + 'solution.zip', 'wb') as fs:
|
||||
for chunk in request.FILES['file'].chunks():
|
||||
fs.write(chunk)
|
||||
flag = False
|
||||
solution_created = False
|
||||
try:
|
||||
with ZipFile(solution_dir + 'solution.zip') as obj:
|
||||
obj.extractall(solution_dir)
|
||||
except BadZipFile:
|
||||
rename(solution_dir + 'solution.zip', solution_dir + request.FILES['file'].name)
|
||||
sln_path = solution_path(solution_dir)
|
||||
if current_task.full_solution != bool(sln_path):
|
||||
current_solution.result = 'TEST ERROR'
|
||||
with open(log_file_path, 'ab') as fs:
|
||||
fs.write(b'Can\'t find sln file in solution' if current_task.full_solution else b'Sln file in path')
|
||||
current_solution.save()
|
||||
return HttpResponseRedirect('/task?id=' + str(current_task.id))
|
||||
if not bool(sln_path):
|
||||
copytree('SampleSolution', join(solution_dir, 'Solution'))
|
||||
for file in listdir(solution_dir):
|
||||
if file == 'solution.zip' or file == 'Solution':
|
||||
continue
|
||||
cur_file = join(solution_dir, file)
|
||||
if isfile(cur_file):
|
||||
copyfile(cur_file, join(solution_dir, 'Solution', 'SampleProject', file))
|
||||
remove(cur_file)
|
||||
else:
|
||||
rmtree(cur_file)
|
||||
if not current_task.full_solution:
|
||||
for file in current_task.files_for_compilation:
|
||||
copyfile(file.path, join(solution_dir, 'Solution', 'SampleProject', file.filename))
|
||||
#Tester(current_solution, request.META['HTTP_HOST']).push()
|
||||
Thread(target=lambda: Tester(current_solution, request.META['HTTP_HOST']).push()).start()
|
||||
return HttpResponseRedirect('/task?id=' + str(current_task.id))
|
||||
|
||||
|
||||
def block(request):
|
||||
current_block = Block.objects.get(id=request.GET['id'])
|
||||
if not check_permission_block(request.user, current_block):
|
||||
return HttpResponseRedirect('/main')
|
||||
return render(request, 'block.html', context={'Block': current_block,
|
||||
'is_admin': check_admin(request.user),
|
||||
'can_edit': check_admin_on_course(request.user, current_block.course),
|
||||
'user': request.user})
|
||||
|
||||
|
||||
def task_settings(request):
|
||||
if not check_admin(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
current_task = Task.objects.get(id=request.GET['id'])
|
||||
if request.method == 'POST':
|
||||
action = request.POST['ACTION']
|
||||
if action == 'DELETE':
|
||||
t = Task.objects.get(id=request.GET['id'])
|
||||
block_id = t.block.id
|
||||
t.delete()
|
||||
return HttpResponseRedirect('/admin/block?id=' + str(block_id))
|
||||
elif action.startswith('SAVE_EXTRA_FILE_'):
|
||||
i = action.split('_')[-1]
|
||||
ef = ExtraFile.objects.get(id=int(i))
|
||||
with open(ef.path, 'wb') as fs:
|
||||
file_text = request.POST['extra_file_text_' + i]
|
||||
fs.write(bytes(file_text, encoding='utf-8'))
|
||||
ef.for_compilation = '{}_for_compilation'.format(ef.id) in request.POST.keys()
|
||||
ef.save()
|
||||
elif action == 'SAVE':
|
||||
current_task.legend, current_task.input, current_task.output, current_task.specifications = \
|
||||
request.POST['legend'], request.POST['input'], request.POST['output'], request.POST['specifications']
|
||||
current_task.time_limit = int(request.POST['time_limit']) if is_integer(request.POST['time_limit']) else 10000
|
||||
current_task.show_details = 'show_details' in request.POST.keys()
|
||||
current_task.full_solution = 'full_solution' in request.POST.keys()
|
||||
current_task.mark_formula = request.POST['mark_formula']
|
||||
current_task.show_result = 'show_result' in request.POST.keys()
|
||||
current_task.priority = request.POST['priority']
|
||||
for ef in ExtraFile.objects.filter(task=current_task):
|
||||
ef.sample = 'sample_' + str(ef.id) in request.POST.keys()
|
||||
ef.save()
|
||||
try:
|
||||
current_task.weight = float(request.POST['weight'].replace(',', '.'))
|
||||
except ValueError:
|
||||
current_task.weight = 1.0
|
||||
try:
|
||||
current_task.max_mark = int(request.POST['max_mark'])
|
||||
if current_task.max_mark == 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
current_task.max_mark = 10
|
||||
try:
|
||||
current_task.max_solutions_count = int(request.POST['max_solutions_count'])
|
||||
except ValueError:
|
||||
current_task.max_solutions_count = 10
|
||||
|
||||
elif action == 'UPLOAD_EXTRA_FILE':
|
||||
if request.FILES['file'].name.endswith('.zip'):
|
||||
try:
|
||||
wdir = join(MEDIA_ROOT, 'extra_files', 'files' + str(current_task.id))
|
||||
if exists(wdir):
|
||||
rmtree(wdir)
|
||||
mkdir(wdir)
|
||||
with open(join(wdir, 'file.zip'), 'wb') as fs:
|
||||
for chunk in request.FILES['file'].chunks():
|
||||
fs.write(chunk)
|
||||
with ZipFile(join(wdir, 'file.zip')) as obj:
|
||||
obj.extractall(wdir)
|
||||
remove(join(wdir, 'file.zip'))
|
||||
for file in listdir(wdir):
|
||||
if isfile(join(wdir, file)):
|
||||
try:
|
||||
ef = ExtraFile.objects.get(filename=file, task=current_task)
|
||||
except ObjectDoesNotExist:
|
||||
ef = ExtraFile.objects.create(filename=file, task=current_task)
|
||||
ef.write(open(join(wdir, file), 'rb').read())
|
||||
rmtree(wdir)
|
||||
except BadZipFile:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
ef = ExtraFile.objects.get(filename=request.FILES['file'].name, task=current_task)
|
||||
except ObjectDoesNotExist:
|
||||
ef = ExtraFile.objects.create(filename=request.FILES['file'].name, task=current_task)
|
||||
with open(ef.path, 'wb') as fs:
|
||||
for chunk in request.FILES['file'].chunks():
|
||||
fs.write(chunk)
|
||||
elif action == 'CREATE_EXTRA_FILE':
|
||||
try:
|
||||
ExtraFile.objects.get(task=current_task, filename=request.POST['newfile_name'])
|
||||
except ObjectDoesNotExist:
|
||||
ef = ExtraFile.objects.create(task=current_task, filename=request.POST['newfile_name'])
|
||||
f = open(join(MEDIA_ROOT, 'extra_files', str(ef.id)), 'w')
|
||||
f.close()
|
||||
elif action.startswith('DELETE_FILE_'):
|
||||
ExtraFile.objects.get(id=int(action.split('_')[-1])).delete()
|
||||
elif action == 'SAVE_TESTS':
|
||||
tt = request.POST['tests_text']
|
||||
cs_file = current_task.tests_path()
|
||||
with open(cs_file, 'wb') as fs:
|
||||
fs.write(bytes(tt, encoding='utf-8'))
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
current_task.save()
|
||||
return HttpResponseRedirect('/admin/task?id=' + str(current_task.id))
|
||||
return render(request, 'task_settings.html', context={'task': current_task,
|
||||
'tests': TestsForm(),
|
||||
'is_superuser': check_teacher(request.user)})
|
||||
|
||||
|
||||
def block_settings(request):
|
||||
if not check_admin(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
current_block = Block.objects.get(id=request.GET['id'])
|
||||
if not check_permission_block(request.user, current_block):
|
||||
return HttpResponseRedirect('/main')
|
||||
if request.method == 'POST':
|
||||
if 'name' in request.POST.keys():
|
||||
current_block = Block.objects.get(id=request.POST['block_id'])
|
||||
if not check_teacher(request.user) or not check_permission_block(request.user, current_block):
|
||||
return HttpResponseRedirect('/main')
|
||||
task_name = request.POST['name']
|
||||
current_task = Task.objects.create(
|
||||
name=task_name,
|
||||
block=current_block
|
||||
)
|
||||
with open(current_task.tests_path(), 'w') as fs:
|
||||
pass
|
||||
return HttpResponseRedirect('/admin/task?id=' + str(current_task.id))
|
||||
if 'file' in request.FILES.keys():
|
||||
if exists(request.user.username):
|
||||
rmtree(request.user.username)
|
||||
mkdir(request.user.username)
|
||||
with open(join(request.user.username, 'task.zip'), 'wb') as fs:
|
||||
for chunk in request.FILES['file'].chunks():
|
||||
fs.write(chunk)
|
||||
try:
|
||||
with ZipFile(join(request.user.username, 'task.zip')) as obj:
|
||||
obj.extractall(request.user.username)
|
||||
except BadZipFile:
|
||||
rmtree(request.user.username)
|
||||
return HttpResponseRedirect('/admin/block?id={}'.format(current_block.id))
|
||||
task = Task.objects.create(name='Новый таск', block=current_block)
|
||||
root = request.user.username
|
||||
if exists(join(root, 'meta.json')):
|
||||
data = loads(open(join(root, 'meta.json'), 'r', encoding='utf-8').read())
|
||||
task.name = data['localizedNames']['ru']
|
||||
task.time_limit = data['invocationLimits']['idlenessLimitMillis']
|
||||
for f in sorted(listdir(join(root, 'tests'))):
|
||||
e = ExtraFile.objects.create(
|
||||
task=task,
|
||||
filename=f,
|
||||
for_compilation=False
|
||||
)
|
||||
try:
|
||||
e.sample=data['testSets'][str(int(f.split('.')[0]))]['example'] and not f.endswith('.a')
|
||||
except KeyError:
|
||||
e.sample = False
|
||||
e.save()
|
||||
copyfile(join(root, 'tests', f), join(MEDIA_ROOT, 'extra_files', str(e.id)))
|
||||
statements = open(join(root, 'statements', 'ru', 'html', 'statement.html'), 'r', encoding='utf-8').read()
|
||||
task.legend = get_in_html_tag(statements, 'legend')
|
||||
task.input = get_in_html_tag(statements, 'input-specification')
|
||||
task.output = get_in_html_tag(statements, 'output-specification')
|
||||
task.specifications = get_in_html_tag(statements, 'notes')
|
||||
task.save()
|
||||
with open(join(MEDIA_ROOT, 'tests', str(task.id) + '.cs'), 'w') as fs:
|
||||
pass
|
||||
rmtree(root)
|
||||
return HttpResponseRedirect('/admin/task?id={}'.format(task.id))
|
||||
if 'block_delete' in request.POST.keys():
|
||||
Block.objects.get(id=request.POST['block_delete']).delete()
|
||||
return HttpResponseRedirect('/admin/main')
|
||||
else:
|
||||
time_start = make_aware(datetime.strptime(request.POST['time_start'], "%Y-%m-%dT%H:%M") + timedelta(hours=3))
|
||||
time_end = make_aware(datetime.strptime(request.POST['time_end'], "%Y-%m-%dT%H:%M") + timedelta(hours=3))
|
||||
current_block.opened = 'opened' in request.POST.keys()
|
||||
current_block.time_start = time_start
|
||||
current_block.time_end = time_end
|
||||
current_block.show_rating = "rating" in request.POST.keys()
|
||||
current_block.priority = request.POST['priority']
|
||||
current_block.save()
|
||||
return HttpResponseRedirect('/admin/block?id={}'.format(current_block.id))
|
||||
return render(request, 'block_settings.html', context={'is_superuser': check_teacher(request.user),
|
||||
'Block': current_block})
|
||||
|
||||
|
||||
def solutions_table(request):
|
||||
current_task = Task.objects.get(id=request.GET['id'])
|
||||
user = request.user
|
||||
if not check_permission_block(user, current_task.block):
|
||||
return HttpResponse("done")
|
||||
sols = Solution.objects.filter(task=current_task, user=user)
|
||||
can_edit = check_admin_on_course(request.user, current_task.block.course)
|
||||
# тут по хорошему надо использовать регулярки, но я что-то не разобрался
|
||||
if not can_edit:
|
||||
for sol in sols:
|
||||
while True:
|
||||
i = sol.details.find('<pre>')
|
||||
if i == -1:
|
||||
break
|
||||
j = sol.details.find('</pre>') + 6
|
||||
sol.details = sol.details.replace(sol.details[i:j], '')
|
||||
if any(sol.result == 'TESTING' or sol.result == 'IN QUEUE' for sol in sols) or 'render' in request.GET.keys():
|
||||
return render(request, 'solutions_table.html', context={
|
||||
'solutions': reversed(sols),
|
||||
'can_edit': can_edit,
|
||||
'task': current_task})
|
||||
return HttpResponse('done')
|
||||
|
||||
|
||||
def queue_table(request):
|
||||
block = Block.objects.get(id=request.GET['block_id'])
|
||||
if not check_admin_on_course(request.user, block):
|
||||
return HttpResponse('get away from here')
|
||||
sols = list(Solution.objects.filter(task__block=block, result='TESTING')) + list(Solution.objects.filter(task__block=block, result='IN QUEUE'))
|
||||
return render(request, 'queue_table.html', {'solutions': sorted(sols, key=lambda x: -x.task.block.priority * 10 - x.task.priority)})
|
||||
|
||||
|
||||
def get_result_data(request):
|
||||
solution = Solution.objects.get(id=request.GET['id'])
|
||||
if not check_admin_on_course(request.user, solution.task.block.course):
|
||||
return HttpResponse(dumps({'success': False}))
|
||||
return HttpResponse(dumps({
|
||||
'success': True,
|
||||
'results_text': solution.details,
|
||||
'tests_text': solution.task.tests_text,
|
||||
'log_text': solution.log_text
|
||||
}))
|
||||
|
||||
|
||||
def get_comment_data(request):
|
||||
solution = Solution.objects.get(id=request.GET['id'])
|
||||
if not check_admin_on_course(request.user, solution.task.block.course):
|
||||
return HttpResponse(dumps({'success': False}))
|
||||
return HttpResponse(dumps({
|
||||
'success': True,
|
||||
'comment_text': solution.comment
|
||||
}))
|
||||
|
||||
|
||||
def rating(request):
|
||||
current_block = Block.objects.get(id=request.GET['block_id'])
|
||||
if not check_admin_on_course(request.user, current_block.course) and not current_block.show_rating:
|
||||
return HttpResponseRedirect('/main')
|
||||
return render(request, 'rating.html', context={'Block': Block.objects.get(id=request.GET['block_id']), 'admin_course': check_admin_on_course(request.user, current_block.course)})
|
||||
|
||||
|
||||
def cheating(request):
|
||||
current_block = Block.objects.get(id=request.GET['block_id'])
|
||||
if not check_admin_on_course(request.user, current_block.course):
|
||||
return HttpResponseRedirect('/main')
|
||||
if request.method == 'POST':
|
||||
if not current_block.cheating_checking:
|
||||
req = ['_'.join(elem.split('_')[1:]) for elem in request.POST.keys() if elem.startswith('check_')]
|
||||
tasks = Task.objects.filter(id__in=[int(elem.split('_')[1]) for elem in req if elem.startswith('task')])
|
||||
users = User.objects.filter(id__in=[int(elem.split('_')[1]) for elem in req if elem.startswith('user')])
|
||||
solutions = Solution.objects.filter(user__in=users).filter(task__in=tasks)
|
||||
if 'all_tests' in request.POST.keys():
|
||||
solutions = [sol for sol in solutions if sol.passed_all_tests]
|
||||
if 'best_result' in request.POST.keys():
|
||||
sols = {}
|
||||
for solution in solutions:
|
||||
if (solution.user.username, solution.task.id) in sols.keys():
|
||||
comp = result_comparer(sols[(solution.user.username, solution.task.id)][0].result, solution.result)
|
||||
if comp == 1:
|
||||
sols[(solution.user.username, solution.task.id)] = [solution]
|
||||
elif comp == 0:
|
||||
sols[(solution.user.username, solution.task.id)].append(solution)
|
||||
else:
|
||||
sols[(solution.user.username, solution.task.id)] = [solution]
|
||||
solutions = [val for sol in sols.values() for val in sol]
|
||||
solutions = list(sorted(solutions, key=lambda s: s.id))
|
||||
if 'last_solution' in request.POST.keys():
|
||||
sols = {}
|
||||
for sol in solutions:
|
||||
pair = sol.user, sol.task
|
||||
if pair not in sols.keys():
|
||||
sols[pair] = []
|
||||
sols[pair].append(sol)
|
||||
solutions = [sols[key][len(sols[key]) - 1] for key in sols.keys()]
|
||||
Thread(target=check_cheating, args=(solutions, current_block, int(request.POST['cheating_percent']))).start()
|
||||
return HttpResponseRedirect('/admin/cheating?block_id=' + str(current_block.id))
|
||||
return render(request, 'cheating.html', {'Block': current_block})
|
||||
|
||||
|
||||
def admin(request):
|
||||
if not check_admin(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
if request.method == 'POST':
|
||||
if 'invite' in request.POST.keys():
|
||||
register_user(request.POST)
|
||||
return HttpResponseRedirect('/admin/main')
|
||||
name = request.POST['name']
|
||||
course = Course.objects.get(id=request.POST['course_id'])
|
||||
current_block = Block.objects.create(name=name,
|
||||
course=course,
|
||||
opened=False,
|
||||
time_start=timezone.now(),
|
||||
time_end=timezone.now())
|
||||
if not check_teacher(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
try:
|
||||
Subscribe.objects.get(user=request.user, course=course)
|
||||
except ObjectDoesNotExist:
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect('/main')
|
||||
return HttpResponseRedirect('/admin/block?id=' + str(current_block.id))
|
||||
return render(request, "admin.html", context={"blocks": blocks_available(request.user),
|
||||
'is_superuser': check_god(request.user),
|
||||
'is_teacher': check_teacher(request.user)})
|
||||
|
||||
|
||||
def reset_password(request):
|
||||
code = request.GET['code']
|
||||
try:
|
||||
res = Restore.objects.get(code=code)
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponseRedirect('/enter')
|
||||
context = {'form': ResetPasswordForm()}
|
||||
if request.method == 'GET':
|
||||
return render(request, 'reset_password.html', context=context)
|
||||
else:
|
||||
if request.POST['new'] != request.POST['again']:
|
||||
context['error'] = 'Пароли не совпадают'
|
||||
return render(request, 'reset_password.html', context=context)
|
||||
else:
|
||||
res.user.set_password(request.POST['new'])
|
||||
res.user.save()
|
||||
res.delete()
|
||||
return HttpResponseRedirect('/enter')
|
||||
|
||||
|
||||
def settings(request):
|
||||
if not check_login(request.user):
|
||||
return HttpResponseRedirect('/enter')
|
||||
context = {'is_admin': check_admin(request.user), 'form': ChangePasswordForm()}
|
||||
if request.method == 'POST':
|
||||
old = request.POST['old']
|
||||
new = request.POST['new'].strip()
|
||||
again = request.POST['again'].strip()
|
||||
username = request.user.username
|
||||
user = request.user
|
||||
if user is None:
|
||||
context['error'] = 'Неверный пароль'
|
||||
if len(new) < 8 or not any([a.isdigit() for a in new]) or new.lower() == new:
|
||||
context['error'] = 'Пароль слишком слабый'
|
||||
elif new != again:
|
||||
context['error'] = 'Пароли не совпадают'
|
||||
elif new == '' or new.replace(' ', '') == '':
|
||||
context['error'] = 'Некорректный пароль'
|
||||
else:
|
||||
user.set_password(new)
|
||||
user.save()
|
||||
context['error'] = 'Пароль успешно изменен'
|
||||
login(request, user)
|
||||
return render(request, 'settings.html', context=context)
|
||||
|
||||
|
||||
def exit(request):
|
||||
logout(request)
|
||||
return HttpResponseRedirect('/enter')
|
||||
|
||||
|
||||
def redirect(request):
|
||||
return HttpResponseRedirect('/main')
|
||||
|
||||
|
||||
def main(request):
|
||||
if not check_login(request.user):
|
||||
return HttpResponseRedirect('/enter')
|
||||
return render(request, 'main.html', context={'blocks': blocks_available(request.user)})
|
||||
|
||||
|
||||
def restore(request):
|
||||
if check_login(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
elif request.method == 'GET':
|
||||
return render(request, 'restore.html')
|
||||
else:
|
||||
email = request.POST['email']
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except ObjectDoesNotExist:
|
||||
return HttpResponseRedirect('/enter')
|
||||
h = get_restore_hash()
|
||||
try:
|
||||
r = Restore.objects.get(user__email=email)
|
||||
r.code = h
|
||||
r.save()
|
||||
except ObjectDoesNotExist:
|
||||
Restore.objects.create(user=user, code=h)
|
||||
send_email('Reset password',
|
||||
email,
|
||||
'Restore your password using this link:\nhttp://{}/reset_password?code={}'
|
||||
.format(request.META['HTTP_HOST'], h))
|
||||
return HttpResponseRedirect('/enter')
|
||||
|
||||
|
||||
def enter(request):
|
||||
if check_login(request.user):
|
||||
return HttpResponseRedirect('/main')
|
||||
if request.method == 'POST':
|
||||
user = authenticate(username=request.POST['email'].strip(), password=request.POST['password'].strip())
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return HttpResponseRedirect('/enter')
|
||||
else:
|
||||
return render(request, "enter.html", context={"form": LoginForm()})
|
0
Sprint/__init__.py
Normal file
16
Sprint/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for Sprint project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings')
|
||||
|
||||
application = get_asgi_application()
|
153
Sprint/settings.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
Django settings for Sprint project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '-w#*mn6*fa8a=(-c0@klx&$vl%hpiy&l(u*3%0a#2)wdt##(z2'
|
||||
|
||||
DEPLOY = False
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = not DEPLOY
|
||||
|
||||
SECURE_SSL_REDIRECT = DEPLOY
|
||||
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'*'
|
||||
]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'grappelli',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'Main.apps.MainConfig'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'Sprint.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')]
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'Main.context_processors.attributes'
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'Sprint.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
_ = lambda s: s
|
||||
|
||||
LANGUAGES = (
|
||||
('en', _('English')),
|
||||
('ru', _('Russian'))
|
||||
)
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'Europe/Moscow'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, "Main/static"),
|
||||
]
|
||||
|
||||
|
||||
# Authentication backends
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
38
Sprint/urls.py
Normal file
@ -0,0 +1,38 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path, include
|
||||
from Main import views
|
||||
|
||||
urlpatterns = [
|
||||
path('grappelli/', include('grappelli.urls')), # grappelli URLS
|
||||
path('main', views.main),
|
||||
path('settings', views.settings),
|
||||
path('enter', views.enter, name='enter'),
|
||||
path('restore', views.restore, name='restore'),
|
||||
path('reset_password', views.reset_password),
|
||||
path('exit', views.exit),
|
||||
path('block', views.block),
|
||||
path('task', views.task),
|
||||
path('solution', views.solution),
|
||||
path('rating', views.rating),
|
||||
path('messages', views.messages),
|
||||
path('admin/rating', views.rating),
|
||||
path('admin/download_rating', views.download_rating),
|
||||
path('admin/solution', views.solution),
|
||||
path('admin/retest', views.retest),
|
||||
path('admin/docs', views.docs),
|
||||
path('admin/block', views.block_settings),
|
||||
path('admin/task', views.task_settings),
|
||||
path('admin/main', views.admin),
|
||||
path('admin/solutions', views.solutions),
|
||||
path('admin/users_settings', views.users_settings),
|
||||
path('admin/download', views.download),
|
||||
path('admin/queue', views.queue),
|
||||
path('admin/cheating', views.cheating),
|
||||
path('queue_table', views.queue_table),
|
||||
path('task_test', views.task_test),
|
||||
path('solutions_table', views.solutions_table),
|
||||
path('get_result_data', views.get_result_data),
|
||||
path('get_comment_data', views.get_comment_data),
|
||||
path('admin/', admin.site.urls),
|
||||
re_path('^', views.redirect)
|
||||
]
|
16
Sprint/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for Sprint project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings')
|
||||
|
||||
application = get_wsgi_application()
|
21
manage.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Sprint.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
90
templates/admin.html
Normal file
@ -0,0 +1,90 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Админка{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
.form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function change() {
|
||||
let surname = document.getElementById('surname').value;
|
||||
let name = document.getElementById('name').value;
|
||||
let middle_name = document.getElementById('middle_name').value;
|
||||
let group = document.getElementById('group').value;
|
||||
let email = document.getElementById('email').value;
|
||||
let invite = document.getElementById('inviteButton');
|
||||
console.log(surname + ' ' + name + ' ' + middle_name + ' ' + group + ' ' + email);
|
||||
let dis = surname == '' || name == '' || middle_name == '' || group == '' || email == '' || email.indexOf('@') == -1 || email.indexOf('.') == -1 || email.indexOf('.') < email.indexOf('@');
|
||||
console.log(dis);
|
||||
invite.disabled = dis;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Доступные курсы</h2>
|
||||
{% for key, value in blocks.items %}
|
||||
<h5>{{ key.name }}</h5>
|
||||
{% for block in value %}
|
||||
<a href="/admin/block?id={{ block.id }}">{{ block.name }}</a><br>
|
||||
{% endfor %}
|
||||
{% if is_teacher %}
|
||||
<button type="button" class="btn btn-dark" data-toggle="modal" data-target="#example_{{ key.id }}" style="margin-top: 20px;">
|
||||
<i class="fa fa-plus-circle"></i> Новый блок
|
||||
</button>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="example_{{ key.id }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form method="POST">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLongTitle">Новый блок в курсе {{ key.name }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="name" placeholder="Имя блока">
|
||||
<input type="hidden" name="course_id" value="{{ key.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-times-circle"></i> Закрыть</button>
|
||||
<button type="submit" class="btn btn-success"><i class="fa fa-plus-circle"></i> Создать</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-dark" onclick="window.location.href = '/admin/users_settings?course_id={{ key.id }}'" style="margin-top: 20px;"><i class="fa fa-users"></i> Участники курса</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if is_superuser %}
|
||||
<h3 style="margin-top: 30px;">Пригласить пользователя</h3>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<input name="surname" placeholder="Фамилия" class="form" id="surname" onchange="change();"><br>
|
||||
<input name="name" placeholder="Имя" class="form" id="name" onchange="change();"><br>
|
||||
<input name="middle_name" placeholder="Отчество" class="form" id="middle_name" onchange="change();"><br>
|
||||
<input name="group" placeholder="Группа" class="form" id="group" onchange="change();"><br>
|
||||
<input name="email" placeholder="Почта" class="form" id="email" onchange="change();"><br>
|
||||
<button type="submit" id='inviteButton' name="invite" value="Пригласить" class="btn btn-dark" disabled>Пригласить</button>
|
||||
</form>
|
||||
<div>
|
||||
<center>
|
||||
<button target="_blank" rel="noopener noreferrer" type="submit" class="btn btn-dark" onclick="window.location.href = '/admin/'" value="Django admin"><i class="fa fa-shield"></i> Django admin</button>
|
||||
</center>
|
||||
</div>
|
||||
{% endif %}
|
||||
<center>
|
||||
<button class="btn btn-dark" style="margin-top: 20px;" onclick="window.location.href='/admin/docs'"><i class="fa fa-file"></i> Методичка</button>
|
||||
</center>
|
||||
{% endblock %}
|
62
templates/base.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
{% load static %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>
|
||||
Sprint | {% block title %}{% endblock %}
|
||||
</title>
|
||||
{% block links %}
|
||||
{% endblock %}
|
||||
<link rel="shortcut icon" href="{% static "img/icon.svg" %}" />
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href={% static "css/styles.css" %}>
|
||||
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
||||
<script src="https://use.fontawesome.com/49b98aaeb5.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script type="text/javascript" id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
|
||||
</script>
|
||||
<script type="text/javascript" src={% static "js/scripts.js" %}></script>
|
||||
<style type="text/css">
|
||||
{% block styles %}{% endblock %}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="{% block onload %}{% endblock %}">
|
||||
<div class="main_div">
|
||||
<header class="mt-2 mb-2" role="banner">
|
||||
<div class="row no-gutters top-buffer">
|
||||
<div class="col" style="min-width: 100px;">
|
||||
<button type="button" class="btn {% if current_page == 'main' %}btn-dark{% else %}btn-light{% endif %} btn-block top-button" onclick="main()"><i class="fa fa-random"></i> Sprint</button>
|
||||
</div>
|
||||
{% if is_admin %}
|
||||
<div class="col" style="min-width: 100px;">
|
||||
<button type="button" class="btn {% if current_page == 'admin' %}btn-dark{% else %}btn-light{% endif %} btn-block top-button" onclick="admin()"><i class="fa fa-user"></i> Админка</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col" style="min-width: 100px;">
|
||||
<button type="button" class="btn {% if current_page == 'settings' %}btn-dark{% else %}btn-light{% endif %} btn-block top-button" onclick="settings()"><i class="fa fa-cog"></i> Настройки</button>
|
||||
</div>
|
||||
<div class="col-{% if is_admin %}8{% else %}9{% endif %}">
|
||||
|
||||
</div>
|
||||
<div class="col" style="min-width: 100px;">
|
||||
<button type="button" style="max-width: 200px;" class="btn btn-light btn-block top-button" onclick="logout()"><i class="fa fa-sign-out"></i> Выход</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
39
templates/block.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ Block.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{{ Block.name }} {% if can_edit %}<a style="color: black;" href="/admin/block?id={{ Block.id }}"><i class="fa fa-pencil"></i></a> {% endif %}</h3>
|
||||
<h5>Таски</h5>
|
||||
<table>
|
||||
{% for task in Block.tasks %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/task?id={{ task.id }}">{{ task.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% with mark=task|mark_for_task:user %}
|
||||
{% if mark|marked %}
|
||||
<div style="margin-left: 20px; border: 1px solid black; background: {{ mark|mark_color }}; width: 25px; text-align: center;">
|
||||
{{ mark }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if Block.show_rating %}
|
||||
<a href="/rating?block_id={{ Block.id }}" class="btn btn-dark" style="margin-top: 15px;"><i class="fa fa-star"></i> Рейтинг</a>
|
||||
{% endif %}
|
||||
<a href="/messages?block_id={{ Block.id }}" class="btn btn-dark" style="margin-top: 15px;"><i class="fa fa-comments"></i> Сообщения</a>
|
||||
<hr>
|
||||
{% if Block.opened %}
|
||||
Открыто для просмотра
|
||||
{% else %}
|
||||
Закрыто для просмотра
|
||||
{% endif %}
|
||||
<br>Доступно с <b>{{ Block.time_start }}</b> до <b>{{ Block.time_end }}</b>
|
||||
{% endblock %}
|
126
templates/block_settings.html
Normal file
@ -0,0 +1,126 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ Block.name }}|настройки{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function uploaded() {
|
||||
document.getElementById('is_uploaded').style.display = 'block';
|
||||
document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{{ Block.name }} <a style="color: black;" href="/block?id={{ Block.id }}"><i class="fa fa-eye"></i></a></h3>
|
||||
<h5>Таски</h5>
|
||||
{% for task in Block.tasks %}
|
||||
<a href="/admin/task?id={{ task.id }}">{{ task.name }}</a><br>
|
||||
{% endfor %}
|
||||
{% if is_superuser %}
|
||||
<button type="button" class="btn btn-dark" data-toggle="modal" data-target="#example" style="margin-top: 20px;">
|
||||
<i class="fa fa-plus-circle"></i> Новый таск
|
||||
</button>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="example" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form method="POST">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLongTitle">Новый таск в блоке {{ Block.name }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="name" placeholder="Имя таска">
|
||||
<input type="hidden" name="block_id" value="{{ Block.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-times-circle"></i> Закрыть</button>
|
||||
<button type="submit" class="btn btn-success"><i class="fa fa-plus-circle"></i> Создать</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h5>Импортировать задачу из Я.Контест</h5>
|
||||
<form action="" method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<label for="file-upload" class="btn btn-dark" style="margin-top: 20px;">
|
||||
<i class="fa fa-upload"></i> Загрузить архив
|
||||
</label>
|
||||
<span id="is_uploaded" style="display: none;">Архив загружен</span>
|
||||
<input type="file" class="btn form-control-file" id="file-upload" value="Выбрать файл" name="file" onchange="uploaded()">
|
||||
<br><button type="submit" value="Импортировать" class="btn btn-outline-dark"><i class="fa fa-play-circle"></i> Импортировать</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% if is_superuser %}
|
||||
<h3>Ограничения по времени</h3>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="datetime-local" name="time_start" value="{{ Block.time_start_chrome }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="datetime-local" name="time_end" value="{{ Block.time_end_chrome }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Открыто для просмотра<input type="checkbox" name="opened" style="margin-left:15px;" {{ Block.is_opened }}>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Показывать рейтинг участникам<input type="checkbox" name="rating" style="margin-left: 15px;" {% if Block.show_rating %}checked{% endif %}>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Приоритет <select name="priority">
|
||||
{% for i in 10|num_range %}
|
||||
<option {% if i == Block.priority %}selected{% endif %}>{{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<button class="btn btn-dark" value="Установить" type="submit" style="margin-top: 20px;"><i class="fa fa-save"></i> Установить</button>
|
||||
</form>
|
||||
<hr>
|
||||
{% endif %}
|
||||
<div>
|
||||
<a class="btn btn-dark" href="/admin/rating?block_id={{ Block.id }}"><i class="fa fa-star"></i> Рейтинг</a>
|
||||
<button type="button" class="btn btn-dark" onclick="window.location.href = '/admin/solutions?block_id=' + {{ Block.id }}" value="Посмотреть решения"><i class="fa fa-list-ul"></i> Посмотреть решения</button>
|
||||
<a class="btn btn-dark" href="/admin/queue?block_id={{ Block.id }}"><i class="fa fa-align-left"></i> Очередь тестирования</a>
|
||||
<a class="btn btn-dark" href="/admin/cheating?block_id={{ Block.id }}"><i class="fa fa-bomb"></i> Проверка на списывание</a>
|
||||
{% if is_superuser %}
|
||||
<form method="POST" onsubmit="return confirm('Сейчас ты пытаешься сделать то, что может привести к серьезным последствиям. Если ты удалишь этот блок, то вместе с ним удалятся все таски, условия, тесты, дополнительные файлы и решения, сдаваемые в этот блок. Все данные будут безвозвратно утеряны. Оно нам надо?');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="block_delete" value="{{ Block.id }}">
|
||||
<button type="submit" value="Удалить блок" class="btn btn-dark" style="margin-top: 10px;"><i class="fa fa-trash"></i> Удалить блок</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
{% endblock %}
|
133
templates/cheating.html
Normal file
@ -0,0 +1,133 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ Block.name }} | списывание{% endblock %}
|
||||
|
||||
{% block links %}
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.4.0/build/styles/default.min.css">
|
||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.4.0/build/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function check_list(entity_type) {
|
||||
var cbox = document.getElementById('check_' + entity_type + '_all');
|
||||
var boxes = document.getElementsByClassName('check_' + entity_type);
|
||||
for (var i = 0; i < boxes.length; i++) {
|
||||
boxes[i].checked = cbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_for_submit() {
|
||||
var conf = confirm('Точно запускаем проверку? Новую проверку можно будет запустить только после завершения данной');
|
||||
if (conf) {
|
||||
document.getElementById('main_form').submit();
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Списывание в <a href="/admin/block?id={{ Block.id }}">{{ Block.name }}</a></h3>
|
||||
<hr>
|
||||
<form method="POST" id="main_form">
|
||||
{% csrf_token %}
|
||||
<h5>Проверить таски</h5>
|
||||
<b><input type="checkbox" id="check_task_all" onchange="check_list('task')" checked> Все</b><br>
|
||||
{% for task in Block.tasks %}
|
||||
<input type="checkbox" class="check_task" name="check_task_{{ task.id }}" checked> {{ task.name }}<br>
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<h5>Проверить пользователей</h5>
|
||||
<b><input type="checkbox" id="check_user_all" onchange="check_list('user')" checked> Все</b><br>
|
||||
{% for sub in Block.course.students %}
|
||||
<input type="checkbox" class="check_user" name="check_user_{{ sub.user.id }}" checked> {{ sub.user.userinfo }}<br>
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<h5>Дополнительно</h5>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="best_result">
|
||||
</td>
|
||||
<td>
|
||||
Проверять только решения с лучшим результатом
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="last_solution">
|
||||
</td>
|
||||
<td>
|
||||
Проверять только последнее решение
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="all_tests">
|
||||
</td>
|
||||
<td>
|
||||
Проверять только прошедшие все тесты
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="cheating_percent">
|
||||
{% for i in 100|num_range %}
|
||||
<option>{{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
Какой процент схожести считать списыванием
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<button type="button" {% if Block.cheating_checking %}class="btn btn-secondary"{% else %}class="btn btn-dark" onclick="prepare_for_submit()"{% endif %}><i class="fa fa-rocket"></i> Запустить проверку</button>
|
||||
</form>
|
||||
<hr>
|
||||
<center><h1>{{ Block.cheating_status }}</h1></center>
|
||||
{% if Block.cheating_checked %}
|
||||
<h1>Результаты проверки</h1>
|
||||
{% for data in Block.cheating_results %}
|
||||
{% with user=data|user_by_id cheating_data=Block.cheating_results|dict_key:data %}
|
||||
<br><h3>{{ user.userinfo }} [{{ cheating_data|length }}]</h3>
|
||||
<button class="btn btn-link" onclick="var content = document.getElementById('div_{{ user.id }}'); content.hidden = !content.hidden;">Отчет</button>
|
||||
<div id="div_{{ user.id }}" hidden>
|
||||
{% for cheat in cheating_data %}
|
||||
{% with solution=cheat.solution|solution_by_id %}
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<h5><a href="/solution?id={{ solution.id }}">{{ solution.id }}</a> | {{ solution.user.userinfo }} | {{ cheat.file }}</h5>
|
||||
<pre>
|
||||
<code class="c# border border-dark">
|
||||
{{ solution|solution_file_text:cheat.file }}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<center>
|
||||
<a href="/task?id={{ solution.task.id }}">{{ solution.task.name }}</a><br>
|
||||
<b>{{ cheat.similarity }}%</b><br>
|
||||
<i class="fa fa-arrow-{% if cheat.source %}right{% else %}left{% endif %}"></i>
|
||||
</center>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
{% with solution2=cheat.similar|solution_by_id %}
|
||||
<h5><a href="/solution?id={{ solution2.id }}">{{ solution2.id }}</a> | {{ solution2.user.userinfo }} | {{ cheat.file }}</h5>
|
||||
<pre>
|
||||
<code class="c# border border-dark">
|
||||
{{ solution2|solution_file_text:cheat.file }}
|
||||
</code>
|
||||
</pre>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
235
templates/docs.html
Normal file
@ -0,0 +1,235 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Методичка{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
.screenshot {
|
||||
width: 100%;
|
||||
}
|
||||
.screenshot_div {
|
||||
margin-top: 20px;
|
||||
width: 80%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.code {
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Методичка</h1>
|
||||
<hr>
|
||||
<h2><b>О системе</b></h2>
|
||||
<h3>Структура сервиса</h3>
|
||||
В сервисе существует два режима работы:
|
||||
<ul>
|
||||
<li>Режим студента</li>
|
||||
<li>Режим администратора</li>
|
||||
</ul>
|
||||
И одна компанента:
|
||||
<ul>
|
||||
<li>Darwin</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3>Режим студента</h3>
|
||||
Режим студента показывает курсы, на которые подписан пользователь, а также блоки, содержащиеся в них.<br>
|
||||
Напротив каждого блока стоит (или отсутствует) оценка за данный блок.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/main_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на блок пользователь попадает в настройки выбранного блока.<br>
|
||||
На странице отображаются таски и (опционально) оценки за них.<br>
|
||||
Под списком тасков отображается информация о возможности отправки решений в блок, а также временной интервал активности блока.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/block_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
Страница таска представляет из себя подробное описание задания, предлагаемого к решению.<br>
|
||||
Описание таска содержит в себе следующие атрибуты:
|
||||
<ul>
|
||||
<li>Легена</li>
|
||||
<li>Формат входных данных</li>
|
||||
<li>Формат выходных данных</li>
|
||||
<li>Спецификации</li>
|
||||
</ul>
|
||||
Далее находится форма для отправки решения (.zip архив) и список отправленных решений.<br>
|
||||
Решение содержит в себе следующие атрибуты:
|
||||
<ul>
|
||||
<li>id</li>
|
||||
<li>Дата и время отправки</li>
|
||||
<li>Результат тестирования</li>
|
||||
<li>Оценка, выставленная преподавателем или ассистентом</li>
|
||||
<li>Комментарий к решению</li>
|
||||
</ul>
|
||||
Sprint поддерживает следующие варианты результатов тестирования:
|
||||
<ul>
|
||||
<li>M/N - решение прошло M тестов из N</li>
|
||||
<li>Time limit - превышен предел времени, отведенного на тестирование</li>
|
||||
<li>Compilation error - не удалось скомпилировать решение</li>
|
||||
<li>TEST ERROR - ошибка тестирования (может быть связана с некорректыми тестами)</li>
|
||||
<li>TESTING - решение находится в процессе тестирования</li>
|
||||
<li>IN QUEUE - решение находится в очереди на тестирование</li>
|
||||
</ul>
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/task_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на результат тестирование можно получить информацию о том, какие именно тесты были пройдены, а какие - нет.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/test_datails.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на id решения пользователь попадает на страницу решения.<br>
|
||||
Страница с решением содержит ту же информацию, что и на странице таска.<br>
|
||||
Помимо этого, на данной странице можно выставить оценку за решение и оставить комментарий. Для сохранения результата необходимо нажать на кнопку "Зачесть".<br>
|
||||
Для выставления оценки 0 или максимальной оценки за таск, необходимо нажать на кнопку "Незачесть" или "Выставить максимальный балл" соответственно.<br>
|
||||
Комментарий будет сохранен при нажатии на любую из кнопок.<br>
|
||||
Внизу страницы отображаются файлы решения, содержащие только символы Unicode, иначе говоря, файлы с кодом.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/solution_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Режим администратора</h3>
|
||||
<i>Некоторые функции доступны только преподавателям</i><br>
|
||||
Главная страница режима администратора показывает доступные для администрирования курсы, блоки в них и ссылку на данную методичку.<br>
|
||||
Добавление нового блока в курс происходит по нажатии кнопки "Новый блок".
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/admin_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на кнопку "Участники курса" открывается страница с настройками участников курса.<br>
|
||||
Для подписки существующих студентов или преподавателей на курс, необходимо ввести в форму "Добавить участников" ФИО студента или email, на который зарегестрирован аккаунт.<br>
|
||||
Для подписки всех студентов определенной группы, нужно ввести название группы.<br>
|
||||
Для отписки студента от курса, нужно нажать на кнопку "отписка" рядом с нужным студентом.<br>
|
||||
Для того, чтобы сделать подписчика курса ассистентом или снятия с него таких прав, необходимо выбрать почту этого студента в форме "Назначить или разжаловать" и нажать на кнопку применить. Статус студента будет отображен в таблице ниже.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/users_settings_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
В Sprint отсутствует регистрация в обычном понимании данного термина, поэтому регистрация студентов происходит при первом добавлении его к некоторому курсу.<br>
|
||||
Прежде всего необходимо создать JSON файл по определенным правилам, описанным на странице участников курса.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/upload_rules.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на блок, отображается страница с настройками блока.<br>
|
||||
Страница настроек блока содержит следующие аттрибуты:
|
||||
<ul>
|
||||
<li>Набор тасков</li>
|
||||
<li>Интервал времени, в течение которого можно отправлять решения</li>
|
||||
<li>Решения, отправленные в данный блок</li>
|
||||
</ul>
|
||||
Ограничения по времени были внедрены для того, чтобы отложить запуск блока. Блок будет доступен студентам только если данный момент времени входит в интервал между временем начала и временем конца (границы включаются), и блок открыт для отправки решений.<br>
|
||||
После установки нужных значений необходимо нажать на кнопку "Сохранить".<br>
|
||||
Для создания нового таска после списка тасков есть кнопка "Новый таск".
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/admin_block_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на таск из списка, откроется страница с настройками таска.<br>
|
||||
Поля "Легенда", "Входные данные", "Выходные данные" и "Спецификации" заполняются с помощью html разметки, которая потом будет отображена в режиме студента.<br>
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/task_settings_page_1.png' %}" class="screenshot" />
|
||||
</div>
|
||||
В настройках таска содержатся еще несколько аттрибутов:
|
||||
<ul>
|
||||
<li>Ограничения по времени - количество миллисекунд, которое отводится на тестирование решения (целое число)</li>
|
||||
<li>Вес задачи - доля значимости таска в блоке (вещественное число, может быть больше единицы, а суммарно таски могут иметь вес отличный от единицы)</li>
|
||||
<li>Максимальная оценка - максмальная оценка, которую можно выставить за таск (целое число)</li>
|
||||
<li>Максимум решений - максимальное количество решений, которое можно отправить к таску (целое число, ограничение не действует на ассистентов и преподавателей)</li>
|
||||
</ul>
|
||||
В форму загрузки тестов загружается .cs файл с Unit тестами библиотеки Nunit. По нажатии на кнопку "Посмотреть" можно посмотреть тесты и отредактировать их. Для сохранения файла необходимо нажать на кнопку "Сохранить" внизу страницы.<br>
|
||||
В форму "Дополнительные файлы" можно загрузить файлы, которые будут скорированы в директорию с исполняемыми файлами при тестировании.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/task_settings_page_2.png' %}" class="screenshot" />
|
||||
</div>
|
||||
При нажатии на кнопку "Посмотреть решения" на странице настроек блока открывается страница решений, загруженных в текущий блок.<br>
|
||||
Функциональность таблицы решений внизу страницы схожа с подобной в режиме студента.<br>
|
||||
Фильтр на данной странице содержит следующие поля:
|
||||
<ul>
|
||||
<li>id - id решения</li>
|
||||
<li>Таск - имя таска</li>
|
||||
<li>Пользователь - почта студента</li>
|
||||
<li>Группа - решения от студентов из данной группы</li>
|
||||
<li>Последнее решение - последнее отправленное решение от каждого студента к каждому таску</li>
|
||||
<li>Лучший результат - решения с наибольшим количеством пройденных тестов от каждого студента к каждому таску</li>
|
||||
<li>Только студенты - исключить из фильтра решения от ассистентов и преподавателей</li>
|
||||
</ul>
|
||||
При нажатии на кнопку "Перетест" отфильтрованные решения будут перетестированы.
|
||||
<div class="border border-dark screenshot_div">
|
||||
<img src="{% static 'img/solutions_page.png' %}" class="screenshot" />
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Darwin</h3>
|
||||
Одним из ограниений Unit тестирования является невозможность тестирования приватных методов и приватных классов.<br>
|
||||
Для решения данной проблемы в Sprint содержится компонента Darwin. Загрузить библиотеку можно по данной <a href="{% static '/files/Darwin.dll' %}" download>ссылке</a>.<br>
|
||||
Darwin позволяет создавать объекты приватных классов, задавать значения приватным полям и свойствам, а также вызывать приватные методы.<br>
|
||||
Основой библиотеки является класс <b>DObject</b>.<br>
|
||||
В конструктор передается имя того класса, объект которого необходимо создать, с указанием пространства имен.
|
||||
<div class="border border-dark code">
|
||||
<pre>
|
||||
using Darwin;
|
||||
using Nunit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class Tests
|
||||
{
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
DObject d = new DObject("Calculator.Calc");
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
Для доступа к полям и свойствам класса нужно использовать индексатор.<br>
|
||||
<div class="border border-dark code">
|
||||
<pre>
|
||||
using Darwin;
|
||||
using Nunit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class Tests
|
||||
{
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
DObject d = new DObject("Calculator.Calc");
|
||||
d["a"] = 5;
|
||||
Assert.AreEqual(d["a"], 5);
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
Для вызова методов класса используется метод InvokeMethod.<br>
|
||||
В качетсве параметров передается имя метода и массив параметров.<br>
|
||||
Тип возвращаемого значения метода InvokeMethod - object, поэтому необходимо проводить каст к нужному типу после вызова.
|
||||
<div class="border border-dark code">
|
||||
<pre>
|
||||
using Darwin;
|
||||
using Nunit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class Tests
|
||||
{
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
DObject d = new DObject("Calculator.Calc");
|
||||
d["a"] = 5;
|
||||
Assert.AreEqual(d["a"], 5);
|
||||
int val = (int)d.InvokeMethod("GetNumber", new object[] { d["a"] });
|
||||
Assert.AreEqual(val, d["a"]);
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<hr>
|
||||
<h2><b>Use cases</b></h2>
|
||||
<h3>Как собрать таск</h3>
|
||||
<hr>
|
||||
<h3>Как написать Unit тест правильно</h3>
|
||||
<hr>
|
||||
<h3>Как написать тест на входные и выходные данные</h3>
|
||||
<hr>
|
||||
<h3>Что необходимо указать в спецификациях</h3>
|
||||
<hr>
|
||||
<h3>Полезные html тэги</h3>
|
||||
{% endblock %}
|
68
templates/enter.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
{% load static %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>
|
||||
Sprint
|
||||
</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href={% static "css/styles.css" %}>
|
||||
<script src="https://use.fontawesome.com/49b98aaeb5.js"></script>
|
||||
<script type="text/javascript" src={% static "js/scripts.js" %}></script>
|
||||
<script type="text/javascript">
|
||||
function restore() {
|
||||
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.center {
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -200px;
|
||||
margin-left: -200px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 500%;
|
||||
}
|
||||
.form {
|
||||
border: none;
|
||||
border-bottom: 2px solid;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="center">
|
||||
<center>
|
||||
<div>
|
||||
<h1>
|
||||
<i class="fa fa-random"></i> Sprint
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="text" class="form" name="email" placeholder="email"><br>
|
||||
<input type="password" class="form" name="password" placeholder="password"><br>
|
||||
<input type="submit" value="Вход" class="sub btn btn-dark form">
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="window.location.href='/restore'" class="sub btn btn-dark form">Восстановить пароль</button>
|
||||
</div>
|
||||
</center>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
30
templates/main.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}Главная{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Доступные курсы</h2>
|
||||
{% for key, value in blocks.items %}
|
||||
<h5>{{ key.name }}</h5>
|
||||
{% for block in value %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/block?id={{ block.id }}">{{ block.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% with mark=block|mark_for_block:user %}
|
||||
{% if mark|marked %}
|
||||
<div style="margin-left: 20px; border: 1px solid black; background: {{ mark|mark_color }}; width: 25px; text-align: center;">
|
||||
{{ mark }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
31
templates/messages.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block content %}
|
||||
<h3 style="margin-bottom: 30px;">Сообщения</h3>
|
||||
{% for message in Block.messages %}
|
||||
<div class="row">
|
||||
<div class="col-2"></div>
|
||||
<div class="col-8" style="margin-bottom: 50px;">
|
||||
{% with userinfo=message.sender|userinfo_by_user %}
|
||||
<div style="background-color: #DDDDDD; padding: 15px;">
|
||||
<b>
|
||||
{{ userinfo }} | {{ userinfo.group }} |
|
||||
<a href="/{% if current_page == 'admin' %}admin/{% endif %}task?id={{ message.task.id }}">{{ message.task.name }}</a>
|
||||
{% if is_admin %} | <a href="/admin/solutions?block_id={{ Block.id }}&task_name={{ message.task.name }}&user={{ userinfo }}">Решения участника</a>{% endif %}
|
||||
</b>
|
||||
</div>
|
||||
<div class="border border-dark bg-light" style="padding: 30px;">
|
||||
{{ message.text }}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger" style="right: 0;">Ответить всем</button>
|
||||
<button class="btn btn-success" style="right: 0;">Ответить</button>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
33
templates/queue.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ Block.name }} | очередь{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function doPoll() {
|
||||
jQuery.get('/queue_table?block_id={{ Block.id }}', function(data) {
|
||||
document.getElementById('body').innerHTML = data;
|
||||
setTimeout(function() {doPoll()}, 2000);
|
||||
})
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}doPoll(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3><a href="/admin/block?id={{ Block.id }}">{{ Block.name }}</a></h3>
|
||||
<h4>Очередь тестирования</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th scope="col">id</th>
|
||||
<th scope="col">Таск</th>
|
||||
<th scope="col">Дата и время отправки</th>
|
||||
<th scope="col">Студент</th>
|
||||
<th scope="col">Статус</th>
|
||||
</thead>
|
||||
<tbody id="body">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
19
templates/queue_table.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% for solution in solutions %}
|
||||
<tr style="background-color: {% if solution.result == 'TESTING' %}#FFE2B7{% else %}{% if solution.result == 'IN QUEUE' %}#EAEAFF{% endif %}{% endif %};">
|
||||
<td>
|
||||
<b><a href="/admin/solution?id={{ solution.id }}">{{ solution.id }}</a></b>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/task?id={{ solution.task.id }}">{{ solution.task.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.time_sent }}
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }}
|
||||
</td>
|
||||
<td>
|
||||
<b>{{ solution.result }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
49
templates/rating.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}Рейтинг{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Рейтинг (beta)</h3>
|
||||
<h4><a style="color: black;" href="/{% if current_page == 'admin' %}admin/{% endif %}block?id={{ Block.id }}">{{ Block.name }}</a></h4>
|
||||
<table border="1px solid black" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Студент</th>
|
||||
{% for task in Block.tasks %}
|
||||
<th>{% if admin_course %}<a href="/admin/solutions?block_id={{ Block.id }}&task_name={{ task.name }}">{{ task.name }}</a>{% else %}{{ task.name }}{% endif %}</th>
|
||||
{% endfor %}
|
||||
<th scope="col" style="background-color: lightblue;">Оценка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sub in Block.course.students %}
|
||||
<tr>
|
||||
<th scope="row">{% if admin_course %}<a href="/admin/solutions?block_id={{ Block.id }}&user={{ sub.user|userinfo_by_user }}">{{ sub.user|userinfo_by_user }}</a>{% else %}{{ sub.user|userinfo_by_user }}{% endif %}</th>
|
||||
{% for task in Block.tasks %}
|
||||
{% with status=sub.user|mark_status:task %}
|
||||
<th style="background-color: {% if status == '-' %}white{% else %}{% if sub.user|fully_marked:task %}lime{% else %}yellow{% endif %}{% endif %};">
|
||||
{% if current_page == 'admin' and status != '-' %}
|
||||
<a style="color: black;" href="/admin/solutions?block_id={{ Block.id }}&user={{ sub.user|userinfo_by_user }}&task_name={{ task.name }}">{{ status }}</a>
|
||||
{% else %}
|
||||
{{ status }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
<td style="background-color: lightblue;">
|
||||
{% with mark=Block|mark_for_block:sub.user %}
|
||||
{% if mark|marked %}
|
||||
<div style="margin-left: 20px; border: 1px solid black; background: {{ mark|mark_color }}; width: 25px; text-align: center;">
|
||||
{{ mark }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn btn-dark" style="margin-top: 30px;" onclick="window.location.href='/admin/download_rating?block_id={{ Block.id }}'">Скачать csv</button>
|
||||
{% endblock %}
|
29
templates/reset_password.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
{% load static %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>
|
||||
Sprint
|
||||
</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href={% static "css/styles.css" %}>
|
||||
<script type="text/javascript" src={% static "js/scripts.js" %}></script>
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<h1>
|
||||
<font color="black"><a href="/enter">Sprint</a></font>
|
||||
</h1>
|
||||
{{ error }}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<input type="submit" value="Установить пароль" class="btn btn-dark" style="margin-top: 20px;">
|
||||
</form>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
36
templates/restore.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
{% load static %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>
|
||||
Восстановление пароля
|
||||
</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href={% static "css/styles.css" %}>
|
||||
<script type="text/javascript" src={% static "js/scripts.js" %}></script>
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<h1>
|
||||
<font color="black"><a href="{% url 'enter' %}">Sprint</a></font>
|
||||
</h1>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<font color="black">Почта</font>
|
||||
</td>
|
||||
<td>
|
||||
<input type="email" name="email" class="input_simple">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" value="Восстановить" class="btn btn-dark" style="margin-top: 20px;">
|
||||
</form>
|
||||
Если данный электронный адрес есть в базе, то на него будет отправлена ссылка для восстановления пароля.
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
18
templates/settings.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Настройки{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>
|
||||
Сменить пароль
|
||||
</h2>
|
||||
<font color="red">{{ error }}</font>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<button type="submit" class="btn btn-dark" value="Сменить" style="margin-top: 20px;"><i class="fa fa-check"></i> Сменить</button>
|
||||
</form>
|
||||
<hr>
|
||||
{% endblock %}
|
271
templates/solution.html
Normal file
@ -0,0 +1,271 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ solution.task.name }}|решение{% endblock %}
|
||||
|
||||
{% block links %}
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.4.0/build/styles/default.min.css">
|
||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.4.0/build/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
{% endblock %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if can_edit %}
|
||||
function findGetParameter(parameterName) {
|
||||
var result = null,
|
||||
tmp = [];
|
||||
location.search
|
||||
.substr(1)
|
||||
.split("&")
|
||||
.forEach(function (item) {
|
||||
tmp = item.split("=");
|
||||
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
function next() {
|
||||
const solutions_request = findGetParameter('solutions');
|
||||
const solutions = solutions_request.split(' ');
|
||||
const current = findGetParameter('id');
|
||||
const current_index = solutions.findIndex((element) => element == current);
|
||||
if (current_index != solutions.length - 1) {
|
||||
var next_element = document.getElementById('next');
|
||||
next_element.setAttribute('href', '/admin/solution?id=' + solutions[current_index + 1] + '&solutions=' + solutions_request);
|
||||
next_element.innerHTML = '->';
|
||||
}
|
||||
}
|
||||
function previous() {
|
||||
const solutions_request = findGetParameter('solutions');
|
||||
const solutions = solutions_request.split(' ');
|
||||
const current = findGetParameter('id');
|
||||
const current_index = solutions.findIndex((element) => element == current);
|
||||
if (current_index != 0) {
|
||||
var next_element = document.getElementById('previous');
|
||||
next_element.setAttribute('href', '/admin/solution?id=' + solutions[current_index - 1] + '&solutions=' + solutions_request);
|
||||
next_element.innerHTML = '<-';
|
||||
}
|
||||
}
|
||||
function fillContent() {
|
||||
next();
|
||||
previous();
|
||||
}
|
||||
{% if can_edit %}
|
||||
function showHideTests() {
|
||||
var text = document.getElementById('tests_text');
|
||||
var button = document.getElementById('tests_button');
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать тесты';
|
||||
} else {
|
||||
button.textContent = 'Скрыть тесты';
|
||||
}
|
||||
}
|
||||
function showHideLog() {
|
||||
var text = document.getElementById('log_text');
|
||||
var button = document.getElementById('log_button');
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать лог';
|
||||
} else {
|
||||
button.textContent = 'Скрыть лог';
|
||||
}
|
||||
}
|
||||
function retest() {
|
||||
let del = confirm("Подтвердите перетест");
|
||||
if (del) {
|
||||
const sols = findGetParameter('solutions');
|
||||
const link = '/admin/retest?block_id={{ solution.task.block.id }}&solution_id={{ solution.id }}&next={% autoescape off %}{{ path }}?id={{ solution.id }}{% if current_page == 'admin' %}%26solutions={% endif %}{% endautoescape %}'{% if current_page == 'admin' %} + sols.replaceAll(' ', '%20'){% endif %};
|
||||
window.location.href = link;
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}{% if can_edit %}fillContent(){% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h5>
|
||||
<form method="POST">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>
|
||||
Блок
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% if current_page == 'admin' %}/admin{% endif %}/block?id={{ solution.task.block.id }}">{{ solution.task.block.name }}</a>
|
||||
{% if current_page == 'admin' %}
|
||||
| <a style="margin-top: 15px;" href="/admin/solutions?block_id={{ solution.task.block.id }}">К посылкам</a> | <a style="margin-top: 15px;" href="/admin/rating?block_id={{ solution.task.block.id }}">К рейтингу</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Таск
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% if current_page == 'admin' %}/admin{% endif %}/task?id={{ solution.task.id }}">{{ solution.task.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Студент
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
id решения
|
||||
</td>
|
||||
<td>
|
||||
<div style="text-align: center;"></div>
|
||||
{% if can_edit %}
|
||||
<a id="previous"></a>
|
||||
{% endif %}
|
||||
{{ solution.id }}
|
||||
{% if can_edit %}
|
||||
<a id="next"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Результат
|
||||
</td>
|
||||
<td>
|
||||
{% if can_edit or solution.task.show_details %}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-link" data-toggle="modal" data-target="#resultModalLong{{ solution.id }}">
|
||||
{{ solution.result }}
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="resultModalLong{{ solution.id }}" tabindex="-1" role="dialog" aria-labelledby="resultModalLongTitle{{ solution.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="resultModalLongTitle{{ solution.id }}">Подробная информация о тестировании {{ solution.id }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% autoescape off %}
|
||||
{{ solution.details }}
|
||||
{% endautoescape %}
|
||||
{% if can_edit %}
|
||||
<hr>
|
||||
<pre id="tests_text" hidden>
|
||||
<h4>Тесты</h4><br>
|
||||
{{ solution.task.tests_text }}
|
||||
</pre>
|
||||
<hr>
|
||||
<pre id="log_text" hidden>
|
||||
<h4>Лог</h4><br>
|
||||
{{ solution.log_text }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% if can_edit %}
|
||||
<button type="button" id="log_button" class="btn btn-warning" onclick="showHideLog()">Показать лог</button>
|
||||
<button type="button" id="tests_button" class="btn btn-primary" onclick="showHideTests()">Показать тесты</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if solution.task.show_result %}
|
||||
{{ solution.result }}
|
||||
{% else %}
|
||||
Accepted
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Оценка
|
||||
</td>
|
||||
<td>
|
||||
{% if can_edit %}
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
<select name="mark">
|
||||
{% autoescape off %}
|
||||
{{ solution.mark_select }}
|
||||
{% endautoescape %}
|
||||
</select>
|
||||
</label>
|
||||
{% else %}
|
||||
{% if solution.task.show_result %}
|
||||
{{ solution.mark_property }}
|
||||
{% else %}
|
||||
{% if solution.mark == null %}
|
||||
Checking
|
||||
{% else %}
|
||||
Checked
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Комментарий
|
||||
</td>
|
||||
<td>
|
||||
{% if can_edit %}
|
||||
<textarea rows="10" cols="50" style="resize: none;" name="comment">{{ solution.comment }}</textarea>
|
||||
{% else %}
|
||||
<pre>
|
||||
{{ solution.comment_property }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if can_edit %}
|
||||
<button type="submit" name="action" value="Зачесть" class="btn btn-dark"><i class="fa fa-check"></i> Зачесть</button>
|
||||
<button type="submit" name="action" value="Незачесть" class="btn btn-dark"><i class="fa fa-times"></i> Не зачесть</button>
|
||||
<button type="submit" name="action" value="Выставить макс. балл" class="btn btn-dark"><i class="fa fa-arrow-up"></i> Выставить макс. балл</button>
|
||||
<button type="button" class="btn btn-dark" onclick="retest()"><i class="fa fa-undo"></i> Перетест</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-dark"><i class="fa fa-question-circle"></i> Помощь</button>
|
||||
</form>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Files</h3>
|
||||
{% for filename, text in solution.files.items %}
|
||||
<h5>{{ filename }}</h5>
|
||||
{% if filename|is_code %}
|
||||
<pre>
|
||||
<code class="c# border border-dark">
|
||||
{{ text }}
|
||||
</code>
|
||||
</pre>
|
||||
{% else %}
|
||||
<div class="border border-dark bg-light" style="padding: 15px;">
|
||||
<pre>
|
||||
{{ text }}
|
||||
</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
315
templates/solutions.html
Normal file
@ -0,0 +1,315 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ Block.name }} | решения{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function filter() {
|
||||
const idi = document.getElementById('idi').value;
|
||||
const task = document.getElementById('task_name').value;
|
||||
const user = document.getElementById('user').value;
|
||||
const group = document.getElementById('group').value;
|
||||
const last_solution = document.getElementById('last_solution').checked;
|
||||
const best_result = document.getElementById('best_result').checked;
|
||||
const only_students = document.getElementById('only_students').checked;
|
||||
const not_seen = document.getElementById('not_seen').checked;
|
||||
var req = '';
|
||||
if (idi) {
|
||||
req += '&solution_id=' + idi;
|
||||
}
|
||||
if (not_seen) {
|
||||
req += '¬_seen=' + not_seen;
|
||||
}
|
||||
if (task) {
|
||||
req += '&task_name=' + task;
|
||||
}
|
||||
if (user) {
|
||||
req += '&user=' + user;
|
||||
}
|
||||
if (group) {
|
||||
req += '&group=' + group;
|
||||
}
|
||||
if (last_solution) {
|
||||
req += '&last_solution=' + last_solution;
|
||||
}
|
||||
if (best_result) {
|
||||
req += '&best_result=' + best_result;
|
||||
}
|
||||
if (only_students) {
|
||||
req += '&only_students=' + only_students;
|
||||
}
|
||||
window.location.href = '/admin/solutions?block_id={{ Block.id }}' + req;
|
||||
}
|
||||
function retest() {
|
||||
let del = confirm("Подтвердите перетест");
|
||||
if (del)
|
||||
window.location.href = '/admin/retest?block_id={{ Block.id }}{% autoescape off %}{{ req }}{% endautoescape %}';
|
||||
}
|
||||
function download() {
|
||||
let del = confirm("Подтвердите скачивание");
|
||||
if (del) {
|
||||
window.location.href = '/admin/download?block_id={{ Block.id }}{% autoescape off %}{{ req }}{% endautoescape %}';
|
||||
}
|
||||
}
|
||||
function showHideTests() {
|
||||
var text = document.getElementById('tests_text');
|
||||
var button = document.getElementById('tests_button');
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать тесты';
|
||||
} else {
|
||||
button.textContent = 'Скрыть тесты';
|
||||
}
|
||||
}
|
||||
function showHideLog() {
|
||||
var text = document.getElementById('log_text');
|
||||
var button = document.getElementById('log_button');
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать лог';
|
||||
} else {
|
||||
button.textContent = 'Скрыть лог';
|
||||
}
|
||||
}
|
||||
function fillModalResults(id) {
|
||||
jQuery.get('/get_result_data?id=' + id, function(data) {
|
||||
const response = JSON.parse(data);
|
||||
if (response['success'] == true) {
|
||||
document.getElementById('resultModalLongTitle').innerHTML = 'Подробная информация о тестировании ' + id;
|
||||
document.getElementById('results_text').innerHTML = response['results_text'];
|
||||
document.getElementById('tests_text').innerHTML = '<h4>Тесты</h4><br>' + response['tests_text'];
|
||||
document.getElementById('log_text').innerHTML = '<h4>Лог</h4><br>' + response['log_text'];
|
||||
}
|
||||
});
|
||||
}
|
||||
function fillModalComments(id) {
|
||||
jQuery.get('/get_comment_data?id=' + id, function(data) {
|
||||
const response = JSON.parse(data);
|
||||
if (response['success'] == true) {
|
||||
document.getElementById('commentModalLongTitle').innerHTML = 'Комментарий к решению ' + id;
|
||||
document.getElementById('comment_text').innerHTML = response['comment_text'];
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
.input_field {
|
||||
width: 500px;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="resultModalLong" tabindex="-1" role="dialog" aria-labelledby="resultModalLongTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="resultModalLongTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div id="results_text"></div>
|
||||
<hr>
|
||||
<pre id="tests_text" hidden></pre>
|
||||
<hr>
|
||||
<pre id="log_text" hidden></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="log_button" class="btn btn-warning" onclick="showHideLog()">Показать лог</button>
|
||||
<button type="button" id="tests_button" class="btn btn-primary" onclick="showHideTests()">Показать тесты</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="commentModalLong" tabindex="-1" role="dialog" aria-labelledby="commentModalLongTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="commentModalLongTitle"></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<pre id="comment_text"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2><a href="/admin/block?id={{ Block.id }}">{{ Block.name }}</a></h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h4>Фильтр</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td><input class="input_field" type="text" placeholder="Id" id="idi" name="idi" value="{{ options.solution_id }}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Таск</td>
|
||||
<td>
|
||||
<select class="input_field" id="task_name" name="task_name">
|
||||
<option value="">Все</option>
|
||||
{% for task in solutions_info.tasks %}
|
||||
<option value="{{ task.name }}" {% if options.task_name == task.name %}selected{% endif %}>{{ task.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Пользователь</td>
|
||||
<td>
|
||||
<select class="input_field" id="user" name="user">
|
||||
<option value="">Все</option>
|
||||
{% for u in solutions_info.users %}
|
||||
<option value="{{ u }}" {% if options.user == u %}selected{% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Группа</td>
|
||||
<td>
|
||||
<select class="input_field" id="group" name="group">
|
||||
<option value="">Все</option>
|
||||
{% for group in solutions_info.groups %}
|
||||
<option value="{{ group }}" {% if options.group == group %}selected{% endif %}>{{ group }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Последнее решение</td>
|
||||
<td><input type="checkbox" id="last_solution" name="last_solution" {% if options.last_solution %}checked{% endif %}>{{ option.last_solution }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Лучший результат</td>
|
||||
<td><input type="checkbox" id="best_result" name="best_result" {% if options.best_result %}checked{% endif %}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Только студенты</td>
|
||||
<td><input type="checkbox" id="only_students" name="only_students" {% if options.only_students %}checked{% endif %}></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Еще не проверено</td>
|
||||
<td><input type="checkbox" id="not_seen" name="not_seen" {% if options.not_seen %}checked{% endif %}></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button type="button" class="btn btn-dark" id="control" onclick="filter()" style="margin-top: 20px;"><i class="fa fa-check"></i> Применить</button><br>
|
||||
<button type="button" class="btn btn-dark" id="control" onclick="window.location.href='/admin/solutions?block_id={{ Block.id }}'" style="margin-top: 10px;"><i class="fa fa-times"></i> Сбросить</button><br>
|
||||
<button style="margin-top: 10px;" class="btn btn-dark" onclick="download()"><i class="fa fa-download"></i> Скачать решения</button><br>
|
||||
<button style="margin-top: 10px;" class="btn btn-dark" onclick="retest()"><i class="fa fa-undo"></i> Перетест</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h4>Статистика по таскам</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th scope="col">Таск</th>
|
||||
<th scope="col">Верно</th>
|
||||
<th scope="col">Частично</th>
|
||||
<th scope="col">С ошибкой</th>
|
||||
<th scope="col">Все</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in Block.tasks %}
|
||||
<tr>
|
||||
<th scope="row">{{ task.name }}</th>
|
||||
<td>{{ task.correct_count }}</td>
|
||||
<td>{{ task.partially_passed }}</td>
|
||||
<td>{{ task.solutions_with_error }}</td>
|
||||
<td>{{ task.solutions_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
<h5>Решения</h5>
|
||||
<form method="POST" id="delete_form">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">id</th>
|
||||
<th scope="col">Таск</th>
|
||||
<th scope="col">Пользователь</th>
|
||||
<th scope="col">Время отправки</th>
|
||||
<th scope="col">Группа</th>
|
||||
<th scope="col">Результат</th>
|
||||
<th scope="col">Оценка</th>
|
||||
<th scope="col">Комментарий</th>
|
||||
<th scope="col">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solution in solutions %}
|
||||
<tr style="background-color: {% if solution.result == 'TESTING' %}#FFE2B7{% else %}{% if solution.result == 'IN QUEUE' %}#EAEAFF{% endif %}{% endif %};">
|
||||
<th scope="row">
|
||||
<a href="/admin/solution?id={{ solution.id }}&solutions={{ filter }}">{{ solution.id }}</a>
|
||||
</th>
|
||||
<td>
|
||||
<a href="/admin/task?id={{ solution.task.id }}">{{ solution.task.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.userinfo.surname }} {{ solution.userinfo.name }} {{ solution.userinfo.middle_name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.time_sent }}
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.userinfo.group }}
|
||||
</td>
|
||||
<td>
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-link" data-toggle="modal" data-target="#resultModalLong" onclick="fillModalResults({{ solution.id }})">
|
||||
{{ solution.result }}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{{ solution.mark_property }}
|
||||
</td>
|
||||
<td>
|
||||
{% if solution.comment %}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-link" data-toggle="modal" data-target="#commentModalLong" onclick="fillModalComments({{ solution.id }})">
|
||||
Посмотреть
|
||||
</button>
|
||||
{% else %}
|
||||
<p>Нет комментария</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" onclick="window.location.href='/admin/download?block_id={{ Block.id }}&solution_id={{ solution.id }}';"><i class="fa fa-download"></i></button>
|
||||
<button name="DELETE_SOLUTION" form="delete_form" value="{{ solution.id }}" onclick="var del = confirm('Подтвердите удаление');if (del){this.form.submit();}" class="btn btn-danger" type="игеещт"><i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
45
templates/solutions_table.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% for solution in solutions %}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a href="/solution?id={{ solution.id }}">{{ solution.id }}</a>
|
||||
</th>
|
||||
<td>
|
||||
{{ solution.time_sent }}
|
||||
</td>
|
||||
<td>
|
||||
{% if can_edit or task.show_details %}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-link" data-toggle="modal" data-target="#resultModalLong" onclick="showData('{{ solution.id }}')">
|
||||
{{ solution.result }}
|
||||
</button>
|
||||
<div id="details_{{ solution.id }}" hidden>{% autoescape off %}{{ solution.details }}{% endautoescape %}</div>
|
||||
{% if can_edit %}
|
||||
<div id="tests_{{ solution.id }}" hidden>{{ solution.task.tests_text }}</div>
|
||||
<div id="log_{{ solution.id }}" hidden>{% autoescape off %}{{ solution.log_text }}{% endautoescape %}</div>
|
||||
{% endif %}
|
||||
{% else %}{% if task.show_result %}
|
||||
{{ solution.result }}
|
||||
{% else %}
|
||||
Accepted
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{% if task.show_result or can_edit %}
|
||||
{{ solution.mark_property }}
|
||||
{% else %}
|
||||
{% if solution.mark == null %}
|
||||
Checking
|
||||
{% else %}
|
||||
Checked
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<pre>
|
||||
{{ solution.comment_property }}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
85
templates/superuser.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Режим Бога{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Настройки системы</h1>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Параметр</th>
|
||||
<th scope="col">Значение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in params %}
|
||||
<tr>
|
||||
<td>{{ param.key }}</td>
|
||||
<td><input style="width: 100%;" name="{{ param.key }}" value="{{ param.value }}"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" value="Сохранить">
|
||||
</form>
|
||||
<hr>
|
||||
<h1>Курсы</h1>
|
||||
<table border="1px solid black">
|
||||
<tr>
|
||||
<td>
|
||||
<b>id</b>
|
||||
</td>
|
||||
<td>
|
||||
Название
|
||||
</td>
|
||||
<td>
|
||||
Преподаватели
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="text" name="course_name">
|
||||
</td>
|
||||
<td>
|
||||
<select name="teacher">
|
||||
{% for teacher in teachers %}
|
||||
<option value="{{ teacher.email }}">{{ teacher.email }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="btn btn-dark" type="submit" value="Создать">
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
{% for course in courses %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ course.id }}
|
||||
</td>
|
||||
<td>
|
||||
{{ course.name }}
|
||||
</td>
|
||||
<td>
|
||||
{% for teacher in course.teachers %}
|
||||
{{ teacher.surname }} {{ teacher.name }} {{ teacher.middle_name }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<hr>
|
||||
<button class="btn btn-dark" onclick="window.location.href='/admin/'">Django admin</button>
|
||||
<hr>
|
||||
{% endblock %}
|
290
templates/task.html
Normal file
@ -0,0 +1,290 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load filters %}
|
||||
|
||||
{% block title %}{{ task.name }}{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}doPoll(){% endblock %}
|
||||
|
||||
{% block links %}
|
||||
<script>
|
||||
var current_solution = null;
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
function partyhard() {
|
||||
var elem = document.getElementById('paint');
|
||||
elem.hidden = false;
|
||||
}
|
||||
function uploaded() {
|
||||
document.getElementById('is_uploaded').style.display = 'block';
|
||||
document.getElementById('is_uploaded').nodeValue = document.getElementById('file-upload').nodeValue;
|
||||
}
|
||||
function doPoll() {
|
||||
jQuery.get('/solutions_table?id={{ task.id }}', function(data) {
|
||||
if (data == 'done') {
|
||||
return
|
||||
}
|
||||
else {
|
||||
document.getElementById('solutions').innerHTML = data;
|
||||
if (current_solution != null) {
|
||||
{% if can_edit %}
|
||||
document.getElementById('log').innerHTML = document.getElementById('log_' + current_solution).innerHTML;
|
||||
{% endif %}
|
||||
document.getElementById('details').innerHTML = document.getElementById('details_' + current_solution).innerHTML;
|
||||
}
|
||||
setTimeout(function() {doPoll()}, 2000);
|
||||
}
|
||||
})
|
||||
jQuery.get('/solutions_table?id={{ task.id }}&render=true', function(data) {
|
||||
document.getElementById('solutions').innerHTML = data;
|
||||
if (current_solution != null) {
|
||||
{% if can_edit %}
|
||||
document.getElementById('log').innerHTML = document.getElementById('log_' + current_solution).innerHTML;
|
||||
{% endif %}
|
||||
document.getElementById('details').innerHTML = document.getElementById('details_' + current_solution).innerHTML;
|
||||
}
|
||||
})
|
||||
}
|
||||
{% if can_edit %}
|
||||
function showHideTests(id) {
|
||||
var text = document.getElementById('tests_text_' + id);
|
||||
var button = document.getElementById('tests_button_' + id);
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать тесты';
|
||||
} else {
|
||||
button.textContent = 'Скрыть тесты';
|
||||
}
|
||||
}
|
||||
function showHideLog(id) {
|
||||
var text = document.getElementById('log_text_' + id);
|
||||
var button = document.getElementById('log_button_' + id);
|
||||
text.hidden = !text.hidden;
|
||||
if (text.hidden) {
|
||||
button.textContent = 'Показать лог';
|
||||
} else {
|
||||
button.textContent = 'Скрыть лог';
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
{% if task.show_details or can_edit %}
|
||||
function showData(id) {
|
||||
current_solution = id;
|
||||
const dataTypes = ['details'{% if can_edit %}, 'tests', 'log'{% endif %}];
|
||||
for (const dt of dataTypes) {
|
||||
document.getElementById(dt).innerHTML = document.getElementById(dt + '_' + id).innerHTML;
|
||||
}
|
||||
document.getElementById('resultModalLongTitle').innerHTML = 'Подробная информация о тестировании ' + id;
|
||||
}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!--
|
||||
Помогите! Меня взяли в заложники и заставляют писать на Python!!!
|
||||
Я уже пятый день сижу в подвале и прикручиваю Docker, избавьте меня от мучений и спасите!
|
||||
-->
|
||||
<h5>
|
||||
<a href="/block?id={{ task.block.id }}">Обратно к блоку</a>
|
||||
</h5>
|
||||
<h2>
|
||||
{{ task.name }}
|
||||
{% if can_edit %}
|
||||
<a style="color: black;" href="/admin/task?id={{ task.id }}"><i class="fa fa-pencil"></i></a>
|
||||
{% endif %}
|
||||
<a style="color: black;" href="" data-toggle="modal" data-target="#messageModal"><!--i class="fa fa-comment"></i--></a>
|
||||
</h2>
|
||||
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog" aria-labelledby="messageModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="messageModalLabel">Написать сообщение преподавателям</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<textarea name="message" rows="10" cols="50" style="resize: none;"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Закрыть</button>
|
||||
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<h3>Легенда</h3>
|
||||
{% autoescape off %}
|
||||
{{ task.legend }}
|
||||
{% endautoescape %}
|
||||
<hr>
|
||||
<h3>Формат входных данных</h3>
|
||||
{% autoescape off %}
|
||||
|
||||
{{ task.input }}
|
||||
{% endautoescape %}
|
||||
|
||||
<hr>
|
||||
<h3>Формат выходных данных</h3>
|
||||
{% autoescape off %}
|
||||
|
||||
{{ task.output }}
|
||||
{% endautoescape %}
|
||||
|
||||
<hr>
|
||||
<h3>Спецификации</h3>
|
||||
{% autoescape off %}
|
||||
|
||||
{{ task.specifications }}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
<div class="col-2">
|
||||
<h5>Таски</h5>
|
||||
<table>
|
||||
{% for t in task.block.tasks %}
|
||||
<tr>
|
||||
{% with mark=t|mark_for_task:user %}
|
||||
{% if mark|marked %}
|
||||
<td>
|
||||
<div style="margin-left: 20px; border: 1px solid black; background: {{ mark|mark_color }}; width: 25px; text-align: center;">
|
||||
{{ mark }}
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<td>
|
||||
<a href="/task?id={{ t.id }}">{% if t.id == task.id %}<b>{{ t.name }}</b>{% else %}{{ t.name }}{% endif %}</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Самплы</h3>
|
||||
{% for sample in task.samples %}
|
||||
<h5>Пример {{ sample.input.num }}</h5><br>
|
||||
<b>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
Входные данные
|
||||
</td>
|
||||
<td>
|
||||
Выходные данные
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</b>
|
||||
<hr>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="width: 50%; vertical-align: top;">
|
||||
<pre>
|
||||
{{ sample.input.text }}
|
||||
</pre>
|
||||
</td>
|
||||
<td style="width: 50%; vertical-align: top;">
|
||||
<pre>
|
||||
{{ sample.output.text }}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% if can_send or can_edit %}
|
||||
<h2>Отправить решение</h2>
|
||||
<form action="" method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<label for="file-upload" class="btn btn-dark" style="margin-top: 20px;">
|
||||
<i class="fa fa-upload"></i> Загрузить файл
|
||||
</label>
|
||||
<span id="is_uploaded" style="display: none;">Решение загружено</span>
|
||||
<input type="file" class="btn form-control-file" id="file-upload" value="Выбрать файл" name="file" onchange="uploaded()">
|
||||
<br><button type="submit" value="Отправить" class="btn btn-outline-dark"><i class="fa fa-share"></i> Отправить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not can_edit and can_send %}
|
||||
Осталось попыток: {{ user|last_attempts:task }}
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit or task.show_details %}
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="resultModalLong" tabindex="-1" role="dialog" aria-labelledby="resultModalLongTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="resultModalLongTitle">Подробная информация о тестировании</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% autoescape off %}
|
||||
<div id="details"></div>
|
||||
{% endautoescape %}
|
||||
{% if can_edit %}
|
||||
<hr>
|
||||
<pre id="tests_text_{{ solution.id }}" hidden>
|
||||
<h4>Тесты</h4><br>
|
||||
<div id="tests"></div>
|
||||
</pre>
|
||||
<hr>
|
||||
<pre id="log_text_{{ solution.id }}" hidden>
|
||||
<h4>Лог</h4><br>
|
||||
<div id="log"></div>
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% if can_edit %}
|
||||
<button type="button" id="log_button_{{ solution.id }}" class="btn btn-warning" onclick="showHideLog('{{ solution.id }}')">Показать лог</button>
|
||||
<button type="button" id="tests_button_{{ solution.id }}" class="btn btn-primary" onclick="showHideTests('{{ solution.id }}')">Показать тесты</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2 style="margin-top: 20px;">Решения</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">id</th>
|
||||
<th scope="col">Дата и время отправки</th>
|
||||
<th scope="col">Результат</th>
|
||||
<th scope="col">Оценка</th>
|
||||
<th scope="col">Комментарий</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="solutions">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<iframe id="paint" src="https://jspaint.app/" style="width: 100%; height: 1000px;" hidden></iframe>
|
||||
|
||||
{% endblock %}
|