From: Guillaume Pellerin Date: Sun, 11 May 2014 21:45:18 +0000 (+0200) Subject: exam: add models X-Git-Tag: 1.1~489 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=7509eacdf19bfc323adb3d1b8906586bf4c315c5;p=teleforma.git exam: add models --- diff --git a/example/settings.py b/example/settings.py index 6ef81992..8d50485b 100644 --- a/example/settings.py +++ b/example/settings.py @@ -19,10 +19,10 @@ MANAGERS = ADMINS DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'teleforma', # Or path to database file if using sqlite3. - 'USER': 'teleforma', # Not used with sqlite3. - 'PASSWORD': 'HMYsrZLEtYeBrvER', # Not used with sqlite3. + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'teleforma_exam.sql', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } @@ -134,6 +134,7 @@ INSTALLED_APPS = ( 'jsonrpc', 'south', 'teleforma', + 'teleforma.exam', 'sorl.thumbnail', 'django_extensions', 'pagination', @@ -146,7 +147,7 @@ INSTALLED_APPS = ( 'jqchat', # 'follow', 'googletools', - 'telecaster', + # 'telecaster', ) TEMPLATE_CONTEXT_PROCESSORS = ( diff --git a/example/urls.py b/example/urls.py index 8bacff02..cf2c02d5 100644 --- a/example/urls.py +++ b/example/urls.py @@ -20,7 +20,7 @@ urlpatterns = patterns('', # TeleForma (r'^', include('teleforma.urls')), - (r'^telecaster/', include('telecaster.urls')), + # (r'^telecaster/', include('telecaster.urls')), # Languages (r'^i18n/', include('django.conf.urls.i18n')), diff --git a/teleforma/exam/__init__.py b/teleforma/exam/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/teleforma/exam/admin.py b/teleforma/exam/admin.py new file mode 100644 index 00000000..489a537b --- /dev/null +++ b/teleforma/exam/admin.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from teleforma.admin import * +from teleforma.exam.models import * +from django.contrib import admin +from django.contrib.auth.models import User +from django.contrib.auth.admin import UserAdmin + + +class ScriptPageInline(admin.StackedInline): + model = ScriptPage + ordering = ['number'] + extra = 10 + +class QuotaInline(admin.StackedInline): + model = Quota + +class CorrectorAdmin(admin.ModelAdmin): + model = Corrector + inlines = [QuotaInline] + +class ScriptAdmin(admin.ModelAdmin): + model = Script + # exclude = ['options'] + inlines = [ScriptPageInline] + + + +admin.site.register(Corrector, CorrectorAdmin) +admin.site.register(Script, ScriptAdmin) +admin.site.register(ScriptPage) +admin.site.register(Quota) +admin.site.register(Exam) diff --git a/teleforma/exam/models.py b/teleforma/exam/models.py new file mode 100644 index 00000000..4cd505b9 --- /dev/null +++ b/teleforma/exam/models.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" + TeleForma + + Copyright (c) 2014 Guillaume Pellerin + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + +# Author: Guillaume Pellerin +""" + +import os, uuid, time, hashlib, mimetypes, tempfile + +from django.db import models +from django.contrib.auth.models import User +from django.db.models import Q, Max, Min +from django.db.models.signals import post_save +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from teleforma.models import Course + +app = 'teleforma' + +class MetaCore: + + app_label = 'exam' + + +SCRIPT_STATUS = ((0, _('rejected')), (1, _('draft')), (2, _('pending')), (3, _('corrected')),) +REJECT_REASON = ((0, _('unreadable')), (1, _('bad orientation')), (2, _('bad framing')), (3, _('incomplete')),) + +cache_path = settings.MEDIA_ROOT + 'cache/' +script_path = settings.MEDIA_ROOT + 'scripts/' + + +def sha1sum_file(filename): + ''' + Return the secure hash digest with sha1 algorithm for a given file + + >>> wav_file = 'tests/samples/guitar.wav' # doctest: +SKIP + >>> print sha1sum_file(wav_file) + 08301c3f9a8d60926f31e253825cc74263e52ad1 + ''' + import hashlib + import io + + sha1 = hashlib.sha1() + chunk_size = sha1.block_size * io.DEFAULT_BUFFER_SIZE + + with open(filename, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b''): + sha1.update(chunk) + return sha1.hexdigest() + +def mimetype_file(path): + return mimetypes.guess_type(path)[0] + + +def set_file_properties(sender, **kwargs): + instance = kwargs['instance'] + if instance.file: + if not instance.mime_type: + instance.mime_type = mimetype_file(instance.file.path) + if not instance.sha1: + instance.sha1 = sha1sum_file(instance.file.path) + try: + if not instance.image: + path = cache_path + os.sep + instance.uuid + '.jpg' + command = 'convert ' + instance.file.path + ' ' + path + os.system(command) + instance.image = path + except: + pass + +def check_unique_mimetype(l): + i = 0 + for d in l: + mime_type = d['obj'].mime_type + if not i: + unique = True + else: + unique = unique and (last_type == mime_type) + last_type = mime_type + i += 1 + return unique + + +class Corrector(models.Model): + + user = models.ForeignKey(User, related_name="correctors", verbose_name=_('user'), blank=True, null=True) + + class Meta(MetaCore): + verbose_name = _('Corrector') + verbose_name_plural = _('Correctors') + + def __unicode__(self): + return ' '.join([self.user.first_name, self.user.last_name, str(self.id)]) + + +class Quota(models.Model): + + course = models.ForeignKey(Course, related_name="quotas", verbose_name=_('course'), blank=True, null=True) + corrector = models.ForeignKey('Corrector', related_name="quotas", verbose_name=_('corrector'), blank=True, null=True) + value = models.IntegerField(_('value')) + + class Meta(MetaCore): + verbose_name = _('Quota') + verbose_name_plural = _('Quotas') + + def __unicode__(self): + return ' - '.join([self.course.title, str(self.value)]) + + @property + def level(self): + if self.value: + if self.value != 0: + return 100*self.corrector.scripts.filter(Q(status=2) | Q(status=3)).count()/self.value + else: + return 0 + else: + return 0 + + +class BaseResource(models.Model): + + date_added = models.DateTimeField(_('date added'), auto_now_add=True) + date_modified = models.DateTimeField(_('date modified'), auto_now=True, null=True) + uuid = models.CharField(_('UUID'), unique=True, blank=True, max_length=512) + mime_type = models.CharField(_('MIME type'), max_length=128, blank=True) + sha1 = models.CharField(_('sha1'), blank=True, max_length=512) + + class Meta(MetaCore): + abstract = True + + def save(self, **kwargs): + super(BaseResource, self).save(**kwargs) + if not self.uuid: + self.uuid = unicode(uuid.uuid4()) + + def __unicode__(self): + return self.uuid + + +class Exam(BaseResource): + """Examination""" + + course = models.ForeignKey(Course, related_name="exams", verbose_name=_('courses'), blank=True, null=True, on_delete=models.SET_NULL) + session = models.IntegerField(_('Session'), blank=True, null=True) + author = models.ForeignKey(User, related_name="exams", verbose_name=_('author'), blank=True, null=True, on_delete=models.SET_NULL) + title = models.CharField(_('title'), max_length=255, blank=True) + description = models.TextField(_('description'), blank=True) + credits = models.TextField(_('credits'), blank=True) + file = models.FileField(_('File'), upload_to='exams/%Y/%m/%d', blank=True) + note = models.IntegerField(_('Maximum note'), blank=True, null=True) + + class Meta(MetaCore): + verbose_name = _('Exam') + verbose_name_plural = _('Exams') + +class ScriptPage(BaseResource): + + script = models.ForeignKey('Script', related_name='pages', verbose_name=_('script'), blank=True, null=True) + file = models.FileField(_('Page file'), upload_to='script_pages/%Y/%m/%d', blank=True) + image = models.ImageField(_('Image file'), upload_to='script_pages/%Y/%m/%d', blank=True) + number = models.IntegerField(_('number')) + + class Meta(MetaCore): + verbose_name = _('Page') + verbose_name_plural = _('Pages') + + +class Script(BaseResource): + + author = models.ForeignKey(User, related_name="scripts", verbose_name=_('author'), blank=True, null=True, on_delete=models.SET_NULL) + exam = models.ForeignKey('Exam', related_name="scripts", verbose_name=_('exam'), blank=True, null=True, on_delete=models.SET_NULL) + file = models.FileField(_('PDF file'), upload_to='exams/%Y/%m/%d', blank=True) + box_uuid = models.CharField(_('Box UUID'), max_length='256', blank=True) + box_session_key = models.CharField(_('Box session key'), max_length='1024', blank=True) + corrector = models.ForeignKey('Corrector', related_name="scripts", verbose_name=_('corrector'), blank=True, null=True, on_delete=models.SET_NULL) + note = models.FloatField(_('note'), blank=True) + comments = models.TextField(_('comments'), blank=True) + status = models.IntegerField(_('status'), choices=SCRIPT_STATUS, default=2, blank=True) + reject_reason = models.IntegerField(_('reject_reason'), choices=REJECT_REASON, blank=True) + date_corrected = models.DateTimeField(_('date corrected'), null=True, blank=True) + + class Meta(MetaCore): + verbose_name = _('Script') + verbose_name_plural = _('Scripts') + + + def save(self, **kwargs): + super(Script, self).save(**kwargs) + if self.status == 3: + self.date_corrected = self.date_modified + if not self.corrector: + self.auto_set_corrector() + + def auto_set_corrector(self): + quota_list = [] + quotas = self.exam.course.quotas.all() + for quota in quotas: + if quota.value: + quota_list.append({'obj':quota, 'level': quota.level}) + lower_quota = sorted(quota_list, key=lambda k: k['level'])[0] + self.corrector = lower_quota['obj'].corrector + self.save() + + def make_from_pages(self): + command = 'convert ' + all_pages = self.pages.all() + num_pages = all_pages.count() + pages = [] + paths = '' + + for page in all_pages: + pages.append({'obj': page, 'number': page.number}) + + pages = sorted(pages, key=lambda k: k['number']) + + for dict in pages: + page = pages[dict] + path = cache_path + os.sep + page.uuid + '.pdf' + command = 'convert ' + page.file.path + ' -page A4 ' + path + os.system(command) + paths += ' ' + path + + output = script_path + os.sep + self.uuid + '.pdf' + command = 'stapler ' + paths + ' ' + output + os.system(command) + self.file = output + self.save() + + def box_upload(self): + import crocodoc + crocodoc.api_token = settings.BOX_API_TOKEN + file_handle = open(self.file.path, 'r') + self.box_uuid = crocodoc.document.upload(file=file_handle) + file_handle.close() + user = {'id': self.corrector.id, 'name': self.corrector} + self.box_admin_session_key = crocodoc.session.create(self.box_uuid, editable=True, user=user, + filter='all', admin=True, downloadable=True, + copyprotected=False, demo=False, sidebar='visible') + self.status = 2 + self.save() + + def get_box_admin_url(self): + return 'https://crocodoc.com/view/' + self.box_session_key + + def get_box_user_url(self, user): + user = {'id': user.id, 'name': user} + session_key = crocodoc.session.create(self.box_uuid, editable=False, user=user, + filter='all', admin=False, downloadable=True, + copyprotected=False, demo=False, sidebar='visible') + return 'https://crocodoc.com/view/' + session_key + + +post_save.connect(set_file_properties, sender=Exam) +post_save.connect(set_file_properties, sender=Script) +post_save.connect(set_file_properties, sender=ScriptPage) diff --git a/teleforma/exam/tests.py b/teleforma/exam/tests.py new file mode 100644 index 00000000..501deb77 --- /dev/null +++ b/teleforma/exam/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/teleforma/exam/views.py b/teleforma/exam/views.py new file mode 100644 index 00000000..60f00ef0 --- /dev/null +++ b/teleforma/exam/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/teleforma/models/core.py b/teleforma/models/core.py index e2ee2289..dc48bb06 100644 --- a/teleforma/models/core.py +++ b/teleforma/models/core.py @@ -100,6 +100,7 @@ class Organization(Model): db_table = app_label + '_' + 'organization' verbose_name = _('organization') + class Department(Model): name = CharField(_('name'), max_length=255) @@ -146,6 +147,7 @@ class CourseType(Model): db_table = app_label + '_' + 'course_type' verbose_name = _('course type') + class Course(Model): department = ForeignKey('Department', related_name='course', @@ -565,7 +567,6 @@ class DocumentSimple(MediaBase): ordering = ['-date_added'] - class Media(MediaBase): "Describe a media resource linked to a conference and a telemeta item" diff --git a/teleforma/views/core.py b/teleforma/views/core.py index c07d2cbc..4966db11 100644 --- a/teleforma/views/core.py +++ b/teleforma/views/core.py @@ -176,6 +176,7 @@ def get_periods(user): return periods def get_default_period(periods): + period = None for period in periods: defaults = period.department.all() if defaults: @@ -188,8 +189,11 @@ class HomeRedirectView(View): def get(self, request): if request.user.is_authenticated(): periods = get_periods(request.user) - period = get_default_period(periods) - return HttpResponseRedirect(reverse('teleforma-desk-period-list', kwargs={'period_id': period.id})) + if periods: + period = get_default_period(periods) + return HttpResponseRedirect(reverse('teleforma-desk-period-list', kwargs={'period_id': period.id})) + else: + HttpResponseRedirect(reverse('telemeta-admin')) else: return HttpResponseRedirect(reverse('teleforma-login'))