From 644c9296a3ce593b92fe86d13163a16799c9dc9d Mon Sep 17 00:00:00 2001
From: janmax <mail-github@jmx.io>
Date: Wed, 5 Apr 2017 11:36:23 +0200
Subject: [PATCH] Added convert script database is now ready for exam

---
 .gitignore                                  |   1 +
 README.rst                                  |   8 +-
 convert.py                                  |   2 +-
 core/forms.py                               |   3 +-
 core/migrations/0008_auto_20170403_2313.py  |  20 ++++
 core/models.py                              |   2 +
 core/static/lib/css/custom.css              |   2 +-
 core/templates/core/feedback_card.html      |   2 +-
 core/templates/core/feedback_form.html      |  32 ++++--
 core/templates/core/feedback_list.html      |  47 ++++++++
 core/templates/core/reviewer_startpage.html |  54 ++--------
 core/views/feedback.py                      |   2 +-
 core/views/user_startpages.py               |   6 +-
 populatedb.py                               | 112 +++++++++++---------
 14 files changed, 176 insertions(+), 117 deletions(-)
 create mode 100644 core/migrations/0008_auto_20170403_2313.py
 create mode 100644 core/templates/core/feedback_list.html

diff --git a/.gitignore b/.gitignore
index f618068c..b644b140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ env-grady/
 reinit.sh
 data/
 raw/
+testing_facility/
diff --git a/README.rst b/README.rst
index f8f7acc7..35f9d2c9 100644
--- a/README.rst
+++ b/README.rst
@@ -11,16 +11,16 @@ TODO
 - improve ace editor usability
 - provide one time passwords for student accounts
 - use postgresql
-- fix seen
 - csv export
-- ilias import
 - versioning for feedback
-- implement better capabilities for the reviewer (optional)
+- implement feedback status (default: final, needs review, init)
+- better usernames for students
+- ajaxify feedback finalize/status marker
 
 Overview
 ========
 
-Grady has three basic functions for the tree types of users
+Grady has three basic functions for the three types of users
 
 - Reviewers can
     + edit feedback that has been provided by tutors
diff --git a/convert.py b/convert.py
index 6c465336..e93ba416 100755
--- a/convert.py
+++ b/convert.py
@@ -112,7 +112,7 @@ json_dict = {
         'name' : user.name,
         'matrikel_no' : name2mat[user.name],
         'submissions' : {
-            f"[{task.id}] {task.title}" : code
+            f"{task.id}" : code
             for task, code in zip(task_list[::2], task_list[1::2])
         }
     } for (user, *task_list) in sorted(root, key=lambda u: u[0].name)
diff --git a/core/forms.py b/core/forms.py
index f7d46831..c0f6346f 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -1,5 +1,4 @@
-from django.forms import (CharField, HiddenInput, ModelForm, Textarea,
-                          ValidationError)
+from django.forms import CharField, ModelForm, Textarea, ValidationError
 
 from core.models import Feedback
 
diff --git a/core/migrations/0008_auto_20170403_2313.py b/core/migrations/0008_auto_20170403_2313.py
new file mode 100644
index 00000000..2a864a3e
--- /dev/null
+++ b/core/migrations/0008_auto_20170403_2313.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.6 on 2017-04-03 23:13
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0007_auto_20170331_1249'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='feedback',
+            name='origin',
+            field=models.CharField(choices=[('E', 'was empty'), ('UT', 'passed unittests'), ('CF', 'did not compile'), ('LF', 'could not link'), ('M', 'created by a human. yak!')], default='M', max_length=2),
+        ),
+    ]
diff --git a/core/models.py b/core/models.py
index a8cc9edb..c2e2e55c 100644
--- a/core/models.py
+++ b/core/models.py
@@ -149,11 +149,13 @@ class Feedback(models.Model):
     WAS_EMPTY = 'E'
     PASSED_UNIT_TESTS = 'UT'
     DID_NOT_COMPILE = 'CF'
+    COULD_NOT_LINK = 'LF'
     MANUAL = 'M'
     ORIGIN = (
         (WAS_EMPTY, 'was empty'),
         (PASSED_UNIT_TESTS, 'passed unittests'),
         (DID_NOT_COMPILE, 'did not compile'),
+        (COULD_NOT_LINK, 'could not link'),
         (MANUAL, 'created by a human. yak!'),
     )
     origin = models.CharField(
diff --git a/core/static/lib/css/custom.css b/core/static/lib/css/custom.css
index 002f5425..ddf9f8f2 100644
--- a/core/static/lib/css/custom.css
+++ b/core/static/lib/css/custom.css
@@ -26,7 +26,7 @@ pre {
 }
 
 .editor-pre {
-    height: 250px;
+    height: 400px;
 }
 
 .nopadding-right {
diff --git a/core/templates/core/feedback_card.html b/core/templates/core/feedback_card.html
index 882846fd..e5a8917d 100644
--- a/core/templates/core/feedback_card.html
+++ b/core/templates/core/feedback_card.html
@@ -4,7 +4,7 @@
   </a>
   <div id="collapse{{unique}}" class="collapse {{expanded}}" role="tabpanel">
     <div class="card-block m-2">
-      {{ content }}
+      {{ content|safe }}
     </div>
   </div>
 </div>
diff --git a/core/templates/core/feedback_form.html b/core/templates/core/feedback_form.html
index 628fe5e8..265e6b6a 100644
--- a/core/templates/core/feedback_form.html
+++ b/core/templates/core/feedback_form.html
@@ -17,21 +17,32 @@
       </div>
     </div>
 
-    {# Custom feedback seems too complicated #}
+    {# Custom feedback from the compiler #}
     <div class="card my-1">
       <a data-toggle="collapse" href="#collapse4">
         <h5 class="card-header">Custom Feedback</h5>
       </a>
-      <div id="collapse4" class="collapse show" role="tabpanel">
+      <div id="collapse4" class="collapse {% if feedback.of_submission.pre_corrections %}show{% else %}hide{% endif %}" role="tabpanel">
         <div class="card-block m-2">
           <div id="pre_corrections" class="editor editor-pre">{{feedback.of_submission.pre_corrections}}</div>
         </div>
       </div>
     </div>
 
+    {# A sample solution #}
+    <div class="card my-1">
+      <a data-toggle="collapse" href="#collapse5">
+        <h5 class="card-header">Sample Solution</h5>
+      </a>
+      <div id="collapse5" class="collapse hide" role="tabpanel">
+        <div class="card-block m-2">
+          <div id="solution" class="editor editor-pre">{{feedback.of_submission.type.possible_solution}}</div>
+        </div>
+      </div>
+    </div>
+
     {% include "core/feedback_card.html" with unique="1" header="Description" content=feedback.of_submission.type.task_description expanded="hide" %}
-    {% include "core/feedback_card.html" with unique="2" header="Solution" content=feedback.of_submission.type.possible_solution expanded="hide" %}
-    {% include "core/feedback_card.html" with unique="3" header="Correction Guideline" content=feedback.of_submission.type.correction_guideline expanded="hide" %}
+    {# {% include "core/feedback_card.html" with unique="3" header="Correction Guideline" content=feedback.of_submission.type.correction_guideline expanded="hide" %} #}
 
     <div class="my-2">
       <button type="button" id="collapseAllOpen"  class="btn btn-secondary">Open All</button>
@@ -72,7 +83,7 @@
         </div>
 
         {% else %}
-        <div> {{ field }} </div>
+        <div hidden> {{ field }} </div>
         {% endif %}
         {% endfor %}
       </div>
@@ -104,15 +115,24 @@
     })
   });
 
-  // we need this one for the student readonly
+  // we need this one for the compiler erros readonly
   var editor_pre = ace.edit("pre_corrections");
   editor_pre.setOptions({
     readOnly: true,
     showGutter: false,
   })
 
+  // we need this one for the sample solution readonly
+  var editor_solution = ace.edit("solution");
+  editor_solution.getSession().setMode("ace/mode/c_cpp");
+  editor_solution.setOptions({
+    readOnly: true,
+    showGutter: false,
+  })
+
   // we need this one for the student readonly
   var editor_student = ace.edit("student_text");
+  editor_student.getSession().setMode("ace/mode/c_cpp");
   editor_student.setOptions({
     readOnly: true,
   })
diff --git a/core/templates/core/feedback_list.html b/core/templates/core/feedback_list.html
new file mode 100644
index 00000000..8c893ea9
--- /dev/null
+++ b/core/templates/core/feedback_list.html
@@ -0,0 +1,47 @@
+<div class="card mb-2">
+  <a data-toggle="collapse" href="#collapse{{unique}}">
+    <h5 class="card-header">{{header}}</h5>
+  </a>
+  <div id="collapse{{unique}}" class="collapse hide" role="tabpanel">
+    <div class="card-block">
+      <table class="table nomargin">
+        <thead>
+          <tr>
+            <th></th>
+            <th>Submission Type</th>
+            <th>Student</th>
+            <th>Score</th>
+            <th>Auto feedback</th>
+            <th></th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for feedback in feedback_list %}
+          <tr>
+            <td class="align-middle">
+              {% if feedback.final %}
+              <span class="badge badge-success">Final</span>
+              {% endif %}
+            </td>
+            <td class="align-middle"> {{ feedback.of_submission.type }} </td>
+            <td class="align-middle"> {{ feedback.of_submission.student }} </td>
+            <td class="align-middle"> <code> {{ feedback.score }} / {{ feedback.of_submission.type.full_score }} </code> </td>
+            <td class="align-middle"> {{ feedback.get_origin_display }} </td>
+            <td class="align-middle">
+              {% if not feedback.origin == feedback.WAS_EMPTY %}
+              <a href="{% url 'FeedbackEdit' feedback.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View</a>
+              <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-outline-danger mb-1" name="delete" value="Delete">Delete</a>
+              {% if not feedback.final %}
+              <a class="btn btn-outline-success mb-1" href="{% url 'FeedbackMarkFinal' feedback.slug %}"> Mark final </a>
+              {% else %}
+              <a class="btn btn-outline-secondary mb-1" href="{% url 'FeedbackMarkNotFinal' feedback.slug %}"> Undo </a>
+              {% endif %}
+              {% endif %}
+            </td>
+          </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
diff --git a/core/templates/core/reviewer_startpage.html b/core/templates/core/reviewer_startpage.html
index 805fb7d3..7c13fdaf 100644
--- a/core/templates/core/reviewer_startpage.html
+++ b/core/templates/core/reviewer_startpage.html
@@ -52,12 +52,12 @@
 
   <div class="col-8">
     <div class="card mb-2">
-      {% if feedback_list|length == 0 %}
+      {% if feedback_list_manual|length == 0 %}
       <h5 class="card-header">There is no feedback, yet. Maybe you have to kick some ass?</h5>
-      {% elif feedback_list|length == 1 %}
+      {% elif feedback_list_manual|length == 1 %}
       <h5 class="card-header">Well, one is better than nothing</h5>
       {% else %}
-      <h5 class="card-header">So far {{feedback_list|length}} contributions were provided</h5>
+      <h5 class="card-header">So far {{feedback_list_manual|length}} contributions were provided</h5>
       {% endif %}
       <div class="card-block">
         <table class="table nomargin">
@@ -72,7 +72,7 @@
             </tr>
           </thead>
           <tbody>
-            {% for feedback in feedback_list %}
+            {% for feedback in feedback_list_manual %}
             <tr>
               <td class="align-middle"> {% if feedback.final %}
                 <span class="badge badge-success">Final</span>
@@ -99,49 +99,9 @@
     </div>
 
     {# This is card for empty feedback for the sake of completeness #}
-    <div class="card mb-2">
-      <a data-toggle="collapse" href="#collapseEmpty">
-        <h5 class="card-header">Empty feedback</h5>
-      </a>
-      <div id="collapseEmpty" class="collapse hide" role="tabpanel">
-        <div class="card-block">
-          <table class="table nomargin">
-            <thead>
-              <tr>
-                <th></th>
-                <th>Submission Type</th>
-                <th>Student</th>
-                <th>Score</th>
-                <th>Auto feedback</th>
-                <th></th>
-              </tr>
-            </thead>
-            <tbody>
-              {% for feedback in feedback_list_auto %}
-              <tr>
-                <td class="align-middle">
-                  {% if feedback.final %}
-                  <span class="badge badge-success">Final</span>
-                  {% endif %}
-                </td>
-                <td class="align-middle"> {{ feedback.of_submission.type }} </td>
-                <td class="align-middle"> {{ feedback.of_submission.student }} </td>
-                <td class="align-middle"> <code> {{ feedback.score }} / {{ feedback.of_submission.type.full_score }} </code> </td>
-                <td class="align-middle"> {{ feedback.get_origin_display }} </td>
-                <td class="align-middle">
-                  {% if not feedback.origin == feedback.WAS_EMPTY %}
-                  <a href="{% url 'FeedbackEdit' feedback.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View</a>
-                  <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-outline-danger mb-1" name="delete" value="Delete">Delete</a>
-                  <a class="btn btn-outline-secondary mb-1" href="{% url 'FeedbackMarkNotFinal' feedback.slug %}"> Undo </a>
-                  {% endif %}
-                </td>
-              </tr>
-              {% endfor %}
-            </tbody>
-          </table>
-        </div>
-      </div>
-    </div>
+    {% include "core/feedback_list.html" with header="Did not compile feedback" unique="2" feedback_list=feedback_list_did_not_compile %}
+    {% include "core/feedback_list.html" with header="Could not link feedback" unique="3" feedback_list=feedback_list_could_not_link %}
+    {% include "core/feedback_list.html" with header="Empty feedback" unique="1" feedback_list=feedback_list_empty %}
   </div>
 </div>
 
diff --git a/core/views/feedback.py b/core/views/feedback.py
index f5822d74..4aa72307 100644
--- a/core/views/feedback.py
+++ b/core/views/feedback.py
@@ -79,7 +79,7 @@ class FeedbackEdit(UpdateView):
         if form.is_valid():
             form.instance.empty = False
             form.save()
-        if 'Next' in self.request.POST['update']:
+        if 'Next' in self.request.POST['update'] and not in_groups(self.request.user, ('Reviewers', )):
             return HttpResponseRedirect(reverse('CreateFeedbackForType', args=(form.instance.of_submission.type.slug,)))
         return HttpResponseRedirect(self.get_success_url())
 
diff --git a/core/views/user_startpages.py b/core/views/user_startpages.py
index 2171b1e2..fb5f19f0 100644
--- a/core/views/user_startpages.py
+++ b/core/views/user_startpages.py
@@ -65,7 +65,9 @@ def reviewer_view(request):
         'submission_type_list': get_annotated_feedback_count(),
         'tutor_list': User.objects.annotate(Count('corrected_submissions')).filter(groups__name='Tutors'),
         'submission_list': Submission.objects.all(),
-        'feedback_list': Feedback.objects.filter(origin=Feedback.MANUAL),
-        'feedback_list_auto': Feedback.objects.exclude(origin=Feedback.MANUAL),
+        'feedback_list_manual': Feedback.objects.filter(origin=Feedback.MANUAL),
+        'feedback_list_empty': Feedback.objects.filter(origin=Feedback.WAS_EMPTY),
+        'feedback_list_did_not_compile': Feedback.objects.filter(origin=Feedback.DID_NOT_COMPILE),
+        'feedback_list_could_not_link': Feedback.objects.filter(origin=Feedback.COULD_NOT_LINK),
     }
     return render(request, 'core/reviewer_startpage.html', context)
diff --git a/populatedb.py b/populatedb.py
index bae53c3f..decde083 100644
--- a/populatedb.py
+++ b/populatedb.py
@@ -6,8 +6,12 @@ import getpass
 import json
 import argparse
 import subprocess
+from collections import namedtuple
 django.setup()
 
+HTML_DIR = 'data/html'
+SOLUTION_DIR = 'data/code/code-lsg'
+
 
 from django.contrib.auth.models import Group, User
 
@@ -21,10 +25,9 @@ def parseme():
         help='Superuser will be created users be created',
         action='store_true')
     parser.add_argument(
-        'DATA',
+        'DATADIR',
         help='a folder containing a predefined set of files with information',
-        default='data',
-        metavar='DATA')
+        default='data')
     parser.add_argument(
         '-s', '--submissions',
         help='A file with submission code and student user names',
@@ -43,15 +46,15 @@ def parseme():
     parser.add_argument(
         '-st', '--submission_types',
         help='some kind of descriptions for all the submission types',
-        default='submission_types.json',
+        default='submission_types.csv',
         metavar="FILE")
 
     args = parser.parse_args()
 
-    args.tutors             = os.path.join(args.DATA, args.tutors)
-    args.reviewers          = os.path.join(args.DATA, args.reviewers)
-    args.submissions        = os.path.join(args.DATA, args.submissions)
-    args.submission_types   = os.path.join(args.DATA, args.submission_types)
+    args.tutors             = os.path.join(args.DATADIR, args.tutors)
+    args.reviewers          = os.path.join(args.DATADIR, args.reviewers)
+    args.submissions        = os.path.join(args.DATADIR, args.submissions)
+    args.submission_types   = os.path.join(args.DATADIR, args.submission_types)
 
     return args
 
@@ -62,7 +65,7 @@ def add_submission_type(name,
                         possible_solution="__possible_solution: a sample solution",
                         correction_guideline="__possible_solution: a way to correct the task",):
     task, created = SubmissionType.objects.get_or_create(name=name)
-    task.full_score = score
+    task.full_score = int(score)
     task.task_description = task_description
     task.possible_solution = possible_solution
     task.correction_guideline = correction_guideline
@@ -73,63 +76,56 @@ def add_submission_type(name,
         print(f"- Got Task {task.name}")
     return task
 
+
 def student_has_all_submissions(student):
     return Submission.objects.filter(student=student).count() \
         == SubmissionType.objects.all().count()
 
-def add_submission(type, text, student, pre="Vorgabe"):
-    def get_compiler_output():
-        command = subprocess.run(
-            ["gcc-6", "-Wall", "-pedantic", "-xc", "-o", "/dev/null", "-"],
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.PIPE,
-            input=text,
-            encoding='utf-8',
-        )
-        return command.stderr # and returncode
 
+def add_submission(type, text, student, compiler_output):
     if student_has_all_submissions(student):
         return None
 
     sub = Submission()
     sub.type = type
     sub.text = text
-    sub.pre_corrections = get_compiler_output()
     sub.student = student
+    sub.pre_corrections = compiler_output
     sub.save()
+    add_auto_feedback(sub, compiler_output)
     print(f"- Created Submission of Type {sub.type}")
     return sub
 
 
-def add_empty_feedback(submission):
+def add_auto_feedback(submission, compiler_output):
+    if not compiler_output:
+        return # let the tutor do his job
+
+    def deduct_feedback_type():
+        if not submission.text:
+            return Feedback.WAS_EMPTY
+        elif compiler_output.endswith('DID NOT COMPILE'):
+            return Feedback.DID_NOT_COMPILE
+        elif compiler_output.endswith('COULD NOT LINK'):
+            return Feedback.COULD_NOT_LINK
+
     auto_correct, _ = User.objects.get_or_create(username='auto_correct')
     feedback = Feedback()
-    feedback.text = "--- You have not submitted any code for this task ---"
-    feedback.final = True
+    feedback.text = "--- Was generated automatically ---"
     feedback.empty = False
-    feedback.origin = Feedback.WAS_EMPTY
+    feedback.origin = deduct_feedback_type()
     feedback.of_submission = submission
     feedback.of_tutor = auto_correct
+    if feedback.origin == Feedback.WAS_EMPTY:
+        feedback.final = True
+        feedback.save()
+        submission.final_feedback = feedback
+        submission.save()
     feedback.save()
-    print(f"- Created empty Feedback for Submission {submission}")
+    print(f"- Created {feedback.origin} Feedback for Submission {submission}")
     return feedback
 
 
-def add_empty_submission(type, student):
-
-    if student_has_all_submissions(student):
-        return None
-
-    sub = Submission()
-    sub.type = type
-    sub.student = student
-    sub.save()
-    sub.feedback = add_empty_feedback(sub)
-    sub.save()
-    print(f"- Created empty Submission of Type {sub.type}")
-    return sub
-
-
 def add_student(username, name, matrikel_no):
     student_group, _ = Group.objects.get_or_create(name='Students')
     student, created = Student.objects.get_or_create(
@@ -178,6 +174,7 @@ class PopulateDatabase:
 
     __slots__ = (
         'args',
+        'type_dict',
         'student_group',
         'tutor_group',
         'reviewer_group',
@@ -187,6 +184,7 @@ class PopulateDatabase:
         self.args = args
         self.create_groups()
         self.create_user_accounts()
+        self.create_submission_types()
         self.populate_submissions()
 
     def create_groups(self):
@@ -203,26 +201,36 @@ class PopulateDatabase:
             for reviewer in reviewers:
                 add_user(reviewer.strip(), self.reviewer_group)
 
+    def create_submission_types(self):
+        submission_type = namedtuple('submission_type', 'id name score')
+        with open(args.submission_types) as data:
+            types = list(submission_type(*line.strip().split(', '))
+                     for line in data if line)
+
+        self.type_dict = {}
+        for t in types:
+            with \
+                    open(os.path.join(SOLUTION_DIR, t.id + '-lsg.c' )) as lsg, \
+                    open(os.path.join(HTML_DIR, t.id + '.html' )) as desc:
+                self.type_dict[t.id] = add_submission_type(
+                    f"[{t.id}] {t.name}",
+                    t.score,
+                    desc.read(),
+                    lsg.read(),
+                )
+
     def populate_submissions(self):
         with open(self.args.submissions) as data:
             stud_data = json.JSONDecoder().decode(data.read())
 
-        # dirty
-        type_dict = {}
-        for submission in stud_data.values():
-            for submission_type in submission['submissions']:
-                type_dict[submission_type] = add_submission_type(
-                    submission_type, 15)
-            break
-
         for user, userdata in stud_data.items():
             student = add_student(
                 user, userdata['name'], userdata['matrikel_no'])
             for s, code in userdata['submissions'].items():
-                if code:
-                    add_submission(type_dict[s], code, student)
-                else:
-                    add_empty_submission(type_dict[s], student)
+                add_submission(
+                    self.type_dict[s], code, student,
+                    userdata['compiler_output'][s]
+                )
 
 
 # Start execution here!
-- 
GitLab