def badge(request, **kwargs): # NOTE: a downloaded image is part the current page, meaning that the coding keys # should NOT be rotated; this is achieved by passing "NOT" as test code. context = generic_context(request, 'NOT') try: # check whether user can have student role if not (has_role(context, 'Student') or has_role(context, 'Instructor')): raise Exception('No access') # if a sample badge is asked for, render it in the requested color bc = kwargs.get('bc', '') if bc: # render a participant badge (image only, not certified) return render_sample_badge(int(bc)) # otherwise, a hex-encoded badge ID is needed h = kwargs.get('hex', '') # verify that hex code is valid # NOTE: since keys have not been rotated, use the ENcoder here! bid = decode(h, context['user_session'].encoder) b = PrestoBadge.objects.get(pk=bid) # for speed, the reward gallery requests tiny images (80x80 px) if kwargs.get('tiny', ''): return render_tiny_badge_image(b) # if otherwise render the certified badge image return render_certified_badge(b) except Exception, e: log_message('ERROR while rendering badge: %s' % str(e), context['user']) with open(os.path.join(settings.IMAGE_DIR, 'not-found.png'), "rb") as f: return HttpResponse(f.read(), content_type="image/png")
def picture_queue(request, **kwargs): h = kwargs.get('hex', '') act = kwargs.get('action', '') # check whether user can view this course try: if act in ['delete', 'get']: # NOTE: when getting a picture, the coding keys should NOT be rotated context = generic_context(request, 'NOT') # and the day code should be used to decode the hexed queue picture ID qpid = decode(h, day_code(PQ_DAY_CODE)) qp = QueuePicture.objects.get(pk=qpid) c = qp.course else: # the hex code should be a course ID, and key rotation should proceed as usual context = generic_context(request, h) cid = decode(h, context['user_session'].decoder) c = Course.objects.get(pk=cid) # always ensure that the user is instructor in the course if not (c.manager == context['user'] or c.instructors.filter(id=context['user'].id)): log_message('ACCESS DENIED: Invalid course parameter', context['user']) return render(request, 'presto/forbidden.html', context) except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
def developer(request, **kwargs): context = generic_context(request) # check whether user can have developer role if not change_role(context, 'Developer'): return render(request, 'presto/forbidden.html', context) # check whether a template must be deleted if kwargs.get('action', '') == 'delete-template': try: h = kwargs.get('hex', '') context = generic_context(request, h) etid = decode(h, context['user_session'].decoder) et = EstafetteTemplate.objects.get(pk=etid) log_message('Deleting template %s' % et.name, context['user']) et.delete() except Exception, e: report_error(request, context, e) return render(request, 'presto/error.html', context)
def estafette_view(request, **kwargs): h = kwargs.get('hex', '') context = generic_context(request, h) # check whether user can have instructor role if not change_role(context, 'Instructor'): return render(request, 'presto/forbidden.html', context) # check whether estafette case must be deleted if kwargs.get('action', '') == 'delete-case': try: ecid = decode(h, context['user_session'].decoder) ec = EstafetteCase.objects.get(pk=ecid) # remember the estafette that is being edited e = ec.estafette ec.delete() except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
def course(request, **kwargs): h = kwargs.get('hex', '') act = kwargs.get('action', '') context = generic_context(request, h) # check whether user can view this course try: cid = decode(h, context['user_session'].decoder) if act == 'delete-relay': # in this case, the course relay ID is passed as hex ce = CourseEstafette.objects.get(pk=cid) c = ce.course else: # otherwise the course ID c = Course.objects.get(pk=cid) # ensure that user is instructor in the course if not (c.manager == context['user'] or c.instructors.filter(id=context['user'].id)): log_message('ACCESS DENIED: Invalid course parameter', context['user']) return render(request, 'presto/forbidden.html', context) except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
def history_view(request, **kwargs): h = kwargs.get('hex', '') context = generic_context(request, h) # check whether user can have student role if not change_role(context, 'Student'): return render(request, 'presto/forbidden.html', context) # check whether user is enrolled in any courses cl = Course.objects.filter(coursestudent__user=context['user'] ).annotate(count=Count('coursestudent')) # add this list in readable form to the context (showing multiple enrollments) context['course_list'] = ', '.join( [c.title() + (' <span style="font-weight: 800; color: red">%d×</span>' % c.count if c.count > 1 else '') for c in cl ]) # student (but also instructor in that role) may be enrolled in several courses # NOTE: "dummy" students are included, but not the "instructor" students csl = CourseStudent.objects.filter(user=context['user'], dummy_index__gt=-1) # get the estafettes for all the student's courses (even if they are not active) cel = CourseEstafette.objects.filter( is_deleted=False, is_hidden=False, course__in=[cs.course.id for cs in csl]) # add this list in readable form to the context context['estafette_list'] = ', '.join([ce.title() for ce in cel]) # get the set of all the course student's current participations pl = Participant.objects.filter(estafette__is_deleted=False, estafette__is_hidden=False, student__in=[cs.id for cs in csl] ).order_by('-estafette__start_time') # if user is a "focused" dummy user, retain only this course user's participations if context.has_key('alias'): pl = pl.filter(student=context['csid']) # if h is not set, show the list of participations as a menu if h == '': # start with an empty list (0 participations) context['participations'] = [] # for each participation, create a context entry with properties to be displayed for p in pl: lang = p.estafette.course.language # estafettes "speak" the language of their course steps = p.estafette.estafette.template.nr_of_legs() part = {'object': p, 'lang': lang, 'start': lang.ftime(p.estafette.start_time), 'end': lang.ftime(p.estafette.end_time), 'next_deadline': p.estafette.next_deadline(), 'steps': steps, 'hex': encode(p.id, context['user_session'].encoder), 'progress': '%d/%d' % (p.submitted_steps(), steps), } context['participations'].append(part) # and show the list as a menu context['page_title'] = 'Presto History' return render(request, 'presto/history_view.html', context) # if we get here, h is set, which means that a specific estafette has been selected try: # first validate the hex code pid = decode(h, context['user_session'].decoder) p = Participant.objects.get(pk=pid) context['object'] = p # encode again, because used to get progress chart context['hex'] = encode(p.id, context['user_session'].encoder) # add progress bar data context['things_to_do'] = p.things_to_do() # do not add participant name popups context['identify'] = False # add context fields to be displayed when rendering the template set_history_properties(context, p) # show the full estafette history using the standard page template context['page_title'] = 'Presto History' return render(request, 'presto/estafette_history.html', context) except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
def progress(request, **kwargs): # NOTE: a downloaded image is part the current page, meaning that the coding keys # should NOT be rotated; this is achieved by passing "NOT" as test code. context = generic_context(request, 'NOT') try: # check whether user can have student role if not (has_role(context, 'Student') or has_role(context, 'Instructor')): raise Exception('No access') h = kwargs.get('hex', '') # verify that hex code is valid # NOTE: since keys have not been rotated, use the ENcoder here! oid = decode(h, context['user_session'].encoder) # check whether oid indeed refers to an existing participant or course estafette p_or_ce = kwargs.get('p_or_ce', '') if p_or_ce == 'p': p = Participant.objects.get(pk=oid) ce = p.estafette else: p = None ce = CourseEstafette.objects.get(pk=oid) # get the basic bar chart img = update_progress_chart(ce) # if image requested by a participant, add orange markers for his/her uploads if p: draw = ImageDraw.Draw(img) # get a font (merely to draw nicely anti-aliased circular outlines) fnt = ImageFont.truetype( os.path.join(settings.FONT_DIR, 'segoeui.ttf'), 25) # calculate how many seconds of estafette time is represented by one bar time_step = int( (ce.end_time - ce.start_time).total_seconds() / BAR_CNT) + 1 # get the number of registered participants (basis for 100%) p_count = Participant.objects.filter(estafette=ce).count() # get leg number and upload time all uploaded assignments for this participant a_list = Assignment.objects.filter(participant=p).filter( time_uploaded__gt=DEFAULT_DATE).filter( clone_of__isnull=True).values('leg__number', 'time_uploaded') for a in a_list: # get the number of assignments submitted earlier cnt = Assignment.objects.filter( participant__estafette=ce ).filter(leg__number=a['leg__number']).filter( time_uploaded__gt=DEFAULT_DATE).filter( clone_of__isnull=True).exclude( time_uploaded__gt=a['time_uploaded']).count() bar = int( (a['time_uploaded'] - ce.start_time).total_seconds() / time_step) perc = round(250 * cnt / p_count) x = V_AXIS_X + bar * BAR_WIDTH y = H_AXIS_Y - perc - 5 # mark uploads with orange & white outline (10 pixels diameter) draw.ellipse([x, y, x + 10, y + 10], fill=(236, 127, 44, 255), outline=None) # draw white letter o to produce neat circular outline draw.text((x - 1.5, y - 14.5), 'o', font=fnt, fill=(255, 255, 255, 255)) # get nr and submission time for this participant's final reviews nr_of_steps = ce.estafette.template.nr_of_legs() r_set = PeerReview.objects.filter(reviewer=p).filter( assignment__leg__number=nr_of_steps).filter( time_submitted__gt=DEFAULT_DATE).values( 'reviewer__id', 'time_submitted').order_by('reviewer__id', 'time_submitted') r_index = 0 for r in r_set: r_index += 1 # get the number of final reviews submitted earlier cnt = PeerReview.objects.filter(reviewer__estafette=ce).filter( assignment__leg__number=nr_of_steps).filter( time_submitted__gt=DEFAULT_DATE).exclude( time_submitted__gt=r['time_submitted']).values( 'reviewer__id', 'time_submitted').order_by( 'reviewer__id', 'time_submitted').annotate( rev_cnt=Count('reviewer_id')).filter( rev_cnt=r_index).count() bar = int( (r['time_submitted'] - ce.start_time).total_seconds() / time_step) perc = round(250 * cnt / p_count) x = V_AXIS_X + bar * BAR_WIDTH y = H_AXIS_Y - perc - 5 # mark final reviews with orange draw.ellipse([x, y, x + 10, y + 10], fill=(236, 127, 44, 255), outline=None) # draw black letter o to produce neat circular outline draw.text((x - 1.5, y - 14.5), 'o', font=fnt, fill=(0, 0, 0, 255)) # output image to browser (do NOT save it as a file) response = HttpResponse(content_type='image/png') img.save(response, 'PNG') return response except Exception, e: log_message('ERROR while generating progress chart: %s' % str(e), context['user']) with open(os.path.join(settings.IMAGE_DIR, 'not-found.png'), "rb") as f: return HttpResponse(f.read(), content_type="image/png")
def ack_letter(request, **kwargs): # NOTE: downloading a file opens a NEW browser tab/window, meaning that # the coding keys should NOT be rotated; this is achieved by passing "NOT" as test code. context = generic_context(request, 'NOT') # check whether user can have student role if not has_role(context, 'Student'): return render(request, 'presto/forbidden.html', context) try: h = kwargs.get('hex', '') # verify that letter exists # NOTE: since keys have not been rotated, use the ENcoder here! lid = decode(h, context['user_session'].encoder) # get letter properties loa = LetterOfAcknowledgement.objects.get(id=lid) # update fields, but do not save yet because errors may still prevent effective rendering loa.time_last_rendered = timezone.now() loa.rendering_count += 1 # get the dict with relevant LoA properties in user-readable form rd = loa.as_dict() # create letter as PDF pdf = MyFPDF() pdf.add_font('DejaVu', '', DEJAVU_FONT, uni=True) pdf.add_font('DejaVu', 'I', DEJAVU_OBLIQUE_FONT, uni=True) pdf.add_font('DejaVu', 'B', DEJAVU_BOLD_FONT, uni=True) pdf.add_font('DejaVu', 'BI', DEJAVU_BOLD_OBLIQUE_FONT, uni=True) pdf.add_page() # see whether course has a description; if so, make a reference to page 2 and # and prepare the text for this page 2 if rd['CD']: see_page_2 = ' described on page 2' else: see_page_2 = '' # NOTE: if the RID entry (= the referee ID) equals zero, the letter is a participant LoA! if rd['RID'] == 0: pdf.letter_head(rd['AC'], rd['DI'], 'Acknowledgement of Project Relay completion') # add the participant acknowledgement text to the letter text = ''.join([ 'To whom it may concern,\n\n', 'With this letter, DelftX, an on-line learning initiative of Delft University of', ' Technology through edX, congratulates ', rd['FN'], '* for having completed', ' the project relay ', rd['PR'], ' offered as part of the online course ', rd['CN'], see_page_2, '.\n\n', 'A project relay comprises a series of steps: assignments that follow on from', ' each other. In each step, participants must first peer review, appraise, and', ' then build on the preceding step submitted by another participant.\n\n', 'The project relay ', rd['PR'], ' comprised ', rd['SL'], ', where each step posed an intellectual challenge that will have required', ' several hours of work. DelftX appreciates in particular the contribution that', ' participants make to the learning of other participants by giving feedback', ' on their work.\n\n\n', rd['SN'], '\n', rd['SP'] ]) else: pdf.letter_head(rd['AC'], rd['DI'], 'Project Relay Referee Letter of Acknowledgement') # adapt some text fragments to attribute values cases = plural_s(rd['ACC'], 'appeal case') hours = plural_s(rd['XH'], 'hour') # average appreciation is scaled between -1 and 1 if rd['AA'] > 0: appr = ' The participants involved in the appeal were appreciative of the arbitration.' elif rd['AA'] < -0.5: appr = ' Regrettably, the participants involved in the appeal were generally not appreciative of the arbitration.' else: appr = '' if rd['DFC'] == rd['DLC']: period = 'On ' + rd['DLC'] else: period = 'In the period between %s and %s' % (rd['DFC'], rd['DLC']) # add the referee acknowledgement text to the letter text = ''.join([ 'To whom it may concern,\n\n', 'With this letter, DelftX, an on-line learning initiative of Delft University of Technology', ' through edX, wishes to express its gratitude for the additional efforts made by ', rd['FN'], '* while participating in the project relay ', rd['PR'], ' offered as part of the online course ', rd['CN'], see_page_2, '.\n\n', 'A project relay comprises a series of steps: assignments that follow on from each other. ', 'In each step, participants must first peer review, appraise, and then build on the ', 'preceding step submitted by another participant. Participant ', rd['FN'], ' has not only completed the course, but also passed the referee test for ', rd['SL'], ' of the ', rd['PR'], ' project relay. This implies having a better command of the subject', ' taught than regular participants.\n\n', 'Referees arbitrate appeal cases, i.e., situations where the reviewed participant ', 'disagrees with the reviewer\'s critique and/or appraisal. ', period, ', participant ', rd['FN'], ' has arbitrated on ', cases, '. This corresponds to approximately ', hours, ' of work.', appr, '\n\nThe role of referee is indispensable to run project ', 'relays on a large scale. DelftX therefore greatly values participants volunteering to ', 'act as such, since it requires significant effort on top of the regular assignments.\n\n\n', rd['SN'], '\n', rd['SP'] ]) pdf.main_text(text) # add footnote with disclaimer pdf.footnote(rd['EM']) if see_page_2: pdf.page_2(rd) # set document properties if rd['RID'] == 0: task = 'completing a project relay' else: task = 'work as project relay referee' pdf.set_properties(rd['AC'], task, rd['FN'], rd['RC'], rd['TLR']) # output to temporary file temp_file = mkstemp()[1] pdf.output(temp_file, 'F') log_message('Rendering acknowledgement letter for %s' % rd['PR'], context['user']) # push the PDF as attachment to the browser w = FileWrapper(file(temp_file, 'rb')) response = HttpResponse(w, content_type='application/pdf') response[ 'Content-Disposition'] = 'attachment; filename="presto-LoA.pdf"' # now we can assume that the PDF will appear, so the updated letter data can be saved loa.save() return response except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
loa = LetterOfAcknowledgement.objects.filter(authentication_code=c) # if so, return the validation plus the LoA properties if loa: jd['r'] = 'VALID' loa = loa.first() jd.update(loa.as_dict()) loa.verification_count += 1 loa.time_last_verified = timezone.now() loa.save() # log all attempts if jd['r'] == 'VALID': log_message('LoA authenticated for %s' % jd['FN']) else: log_message('LoA authentication failed: no match for code "%s"' % c) elif a == 'enroll': cid = decode(request.POST.get('h', ''), user_session.encoder) c = Course.objects.get(pk=cid) cs = CourseStudent(course=c, user=presto_user, time_enrolled=timezone.now(), last_action=timezone.now()) cs.save() log_message('AJAX: Enrolled in course ' + unicode(c), presto_user) elif a == 'save assignment item': aid = decode(request.POST.get('h', ''), user_session.encoder) ua = Assignment.objects.get(pk=aid) i = ua.leg.upload_items.get(number=request.POST.get('i', '')) ia, created = ItemAssignment.objects.get_or_create(assignment=ua, item=i) ia.rating = request.POST.get('r', '') ia.comment = request.POST.get('c', '') ia.save() log_message('Assignment item saved: ' + unicode(ia), presto_user) # calculate minutes since the assignment was assigned
Participant.objects.filter(estafette=ce, student__dummy_index=-1).delete() # and then delete the course relay itself ce.delete() else: # otherwise, only set the "is deleted" property so that it will no longer show ce.is_deleted = True ce.save() except: warn_user(context, 'Failed to delete course relay', 'Please report this error to the Presto administrator.') # otherwise process form input (if any) elif len(request.POST) > 0: try: eid = decode(request.POST.get('relay', ''), context['user_session'].decoder) qid = decode(request.POST.get('questionnaire', ''), context['user_session'].decoder) ce = CourseEstafette( course=c, estafette=Estafette.objects.get(pk=eid), suffix=request.POST.get('suffix', ''), start_time=datetime.strptime(request.POST.get('starts', ''), '%Y-%m-%d %H:%M'), deadline=datetime.strptime(request.POST.get('deadline', ''), '%Y-%m-%d %H:%M'), review_deadline=datetime.strptime( request.POST.get('revsdue', ''), '%Y-%m-%d %H:%M'), end_time=datetime.strptime(request.POST.get('ends', ''), '%Y-%m-%d %H:%M'), questionnaire_template=QuestionnaireTemplate.objects.get(
def download(request, **kwargs): # NOTE: downloading a file opens a NEW browser tab/window, meaning that # the coding keys should NOT be rotated; this is achieved by passing "NOT" as test code. context = generic_context(request, 'NOT') # check whether user can have student or instructor role is_instructor = has_role(context, 'Instructor') if not (has_role(context, 'Student') or is_instructor): return render(request, 'presto/forbidden.html', context) try: h = kwargs.get('hex', '') # verify hex key # NOTE: since keys have not been rotated, use the ENcoder here! aid = decode(h, context['user_session'].encoder) file_name = kwargs.get('file_name', '') # file_name = 'case' indicates a download request for a case attachment if file_name == 'case': ec = EstafetteCase.objects.get(pk=aid) if ec.upload == None: raise ValueError('No attachment file for this case') f = ec.upload.upload_file ext = os.path.splitext(f.name)[1] w = FileWrapper(file(f.path, 'rb')) response = HttpResponse(w, 'application/octet-stream') response['Content-Disposition'] = ( 'attachment; filename="attachment-case-%s%s"' % (ec.letter, ext)) return response # no case attachment? then the download request must concern an assignment work = kwargs.get('work', '') dwnldr = kwargs.get('dwnldr', '') # verify that download is for an existing assignment log_message('Looking for assignment #%d' % aid, context['user']) a = Assignment.objects.get(pk=aid) # get the list of participant uploads for this assignment (or its clone original) # and also the full path to the upload directory if a.clone_of: original = a.clone_of # in case a clone was cloned, keep looking until the "true" original has been found while original.clone_of: original = original.clone_of pul = ParticipantUpload.objects.filter(assignment=original) upl_dir = os.path.join(settings.MEDIA_ROOT, original.participant.upload_dir) else: pul = ParticipantUpload.objects.filter(assignment=a) upl_dir = os.path.join(settings.MEDIA_ROOT, a.participant.upload_dir) log_message('Upload dir = ' + upl_dir, context['user']) # create an empty temporary dir to hold copies of uploaded files temp_dir = os.path.join(upl_dir, 'temp') try: rmtree(temp_dir) except: pass os.mkdir(temp_dir) log_message('TEMP dir: ' + temp_dir, context['user']) if file_name == 'all-zipped': pr_work = 'pr-step%d%s' % (a.leg.number, a.case.letter) zip_dir = os.path.join(temp_dir, pr_work) os.mkdir(zip_dir) # copy the upladed files to the temporary dir ... for pu in pul: real_name = os.path.join(upl_dir, os.path.basename(pu.upload_file.name)) # ... under their formal name, not their actual ext = os.path.splitext(pu.upload_file.name)[1].lower() formal_name = os_path( os.path.join(zip_dir, pu.file_name) + ext) if is_instructor: log_message( 'Copying %s "as is" to ZIP as %s' % (real_name, formal_name), context['user']) # NOTE: for instructors, do NOT anonymize the document copy2(real_name, formal_name) else: log_message( 'Copy-cleaning %s to ZIP as %s' % (real_name, formal_name), context['user']) # strip author data from file and write it to the "work" dir clear_metadata(real_name, formal_name) # compress the files into a single zip file zip_file = make_archive(zip_dir, 'zip', temp_dir, pr_work) response = HttpResponse(FileWrapper(file(zip_file, 'rb')), content_type='application/zip') response['Content-Disposition'] = ( 'attachment; filename="%s.zip"' % pr_work) # always record download in database UserDownload.objects.create(user=context['user_session'].user, assignment=a) # only change time_first_download if it concerns a predecessor's work! if work == 'pre' and a.time_first_download == DEFAULT_DATE: a.time_first_download = timezone.now() a.time_last_download = timezone.now() a.save() return response else: # check whether file name is on "required files" list fl = a.leg.file_list() rf = False for f in fl: if f['name'] == file_name: rf = f if not rf: raise ValueError('Unknown file name: %s' % file_name) # find the corresponding upload pul = pul.filter(file_name=rf['name']) if not pul: raise ValueError('File "%s" not found' % rf['name']) pu = pul.first() # the real file name should not be known to the user real_name = os.path.join(upl_dir, os.path.basename(pu.upload_file.name)) ext = os.path.splitext(pu.upload_file.name)[1] # the formal name is the requested file field plus the document's extension formal_name = os_path(os.path.join(temp_dir, pu.file_name) + ext) if is_instructor: log_message( 'Copying %s "as is" to ZIP as %s' % (real_name, formal_name), context['user']) # NOTE: for instructors, do NOT anonymize the document copy2(real_name, formal_name) else: # strip author data from the file log_message( 'Copy-cleaning %s to %s' % (real_name, formal_name), context['user']) clear_metadata(real_name, formal_name) mime = { '.pdf': 'application/pdf', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation' } w = FileWrapper(file(settings.LEADING_SLASH + formal_name, 'rb')) response = HttpResponse(w, content_type=mime[ext]) response['Content-Disposition'] = ( 'attachment; filename="%s-%d%s%s"' % (file_name, a.leg.number, a.case.letter, ext)) # always record download in database UserDownload.objects.create(user=context['user_session'].user, assignment=a) # only change time_first_download if it concerns a predecessor's work! if work == 'pre' and a.time_first_download == DEFAULT_DATE: a.time_first_download = timezone.now() a.time_last_download = timezone.now() a.save() # if work is downloaded for the first time by a referee, this should be registered if dwnldr == 'ref': ap = Appeal.objects.filter(review__assignment=a).first() if not ap: raise ValueError('Appeal not found') if ap.time_first_viewed == DEFAULT_DATE: ap.time_first_viewed = timezone.now() ap.save() log_message('First view by referee: ' + unicode(ap), context['user']) return response except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context)
# check whether estafette case must be deleted if kwargs.get('action', '') == 'delete-case': try: ecid = decode(h, context['user_session'].decoder) ec = EstafetteCase.objects.get(pk=ecid) # remember the estafette that is being edited e = ec.estafette ec.delete() except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context) else: # if no deletion, the selected estafette is passed as hex code try: eid = decode(h, context['user_session'].decoder) e = Estafette.objects.get(pk=eid) except Exception, e: report_error(context, e) return render(request, 'presto/error.html', context) # check whether a new case is to be added if kwargs.get('action', '') == 'add-case': ec_list = EstafetteCase.objects.filter(estafette=e) # find first unused letter in the alfabet for l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': if not ec_list.filter(letter=l): break ec = EstafetteCase.objects.create(estafette=e, name='New case (%s)' % l, letter=l, creator=context['user'],