Skip to content
Permalink
Browse files

custom part types can require extensions

  • Loading branch information...
christianp committed Jul 26, 2018
1 parent d619ad7 commit e2322305d5408d59797c7d422a173489b9a2d2a9
@@ -294,7 +294,7 @@ class UpdateCustomPartTypeForm(forms.ModelForm):

class Meta:
model = CustomPartType
fields = ['name', 'short_name', 'description', 'help_url', 'input_widget', 'input_options', 'can_be_gap', 'can_be_step', 'settings', 'marking_script', 'marking_notes', 'ready_to_use']
fields = ['name', 'short_name', 'description', 'help_url', 'input_widget', 'input_options', 'can_be_gap', 'can_be_step', 'settings', 'marking_script', 'marking_notes', 'ready_to_use', 'extensions']
widgets = {
'name': forms.TextInput(attrs={'class':'form-control'}),
'short_name': forms.TextInput(attrs={'class':'form-control'}),
@@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2018-07-25 09:26

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('editor', '0034_set_contributors'),
]

operations = [
migrations.AddField(
model_name='customparttype',
name='extensions',
field=models.ManyToManyField(blank=True, to='editor.Extension'),
),
]
@@ -406,6 +406,7 @@ class CustomPartType(models.Model, ControlledObject):
public_availability = models.CharField(max_length=10, choices=CUSTOM_PART_TYPE_PUBLIC_CHOICES, verbose_name='Public availability', default='restricted')
ready_to_use = models.BooleanField(default=False, verbose_name='Ready to use?')
copy_of = models.ForeignKey('self', null=True, related_name='copies', on_delete=models.SET_NULL)
extensions = models.ManyToManyField(Extension, blank=True)

def copy(self, author, name):
new_type = CustomPartType.objects.get(pk=self.pk)
@@ -415,6 +416,7 @@ def copy(self, author, name):
new_type.name = name
new_type.set_short_name(slugify(name))
new_type.copy_of = self
new_type.extensions.set(self.extensions)
return new_type

def __str__(self):
@@ -484,6 +486,7 @@ def as_json(self):
'settings': self.settings,
'public_availability': self.public_availability,
'published': self.published,
'extensions': [e.location for e in self.extensions.all()],
}

class Resource(models.Model):
@@ -327,9 +327,11 @@ $(document).ready(function() {
this.short_name = ko.observable('');
this.description = ko.observable('');
this.help_url = ko.observable('');
this.extensions = ko.observableArray([]);

this.tabs = [
new Editor.Tab('description','Description','cog'),
new Editor.Tab('extensions','Required extensions','transfer'),
new Editor.Tab('settings','Part settings','wrench'),
new Editor.Tab('input','Answer input','pencil'),
new Editor.Tab('marking','Marking','check'),
@@ -347,6 +349,15 @@ $(document).ready(function() {

this.currentTab = ko.observable(this.tabs[0]);

for(var i=0;i<item_json.numbasExtensions.length;i++) {
var ext = item_json.numbasExtensions[i];
ext.used = ko.observable(false);
this.extensions.push(ext);
}
this.usedExtensions = ko.computed(function() {
return this.extensions().filter(function(e){return e.used()});
},this);

this.edit_name = function() {
pt.setTab('description')();
}
@@ -478,6 +489,13 @@ $(document).ready(function() {
var pt = this;
tryLoad(data,['name','short_name','description','help_url','published'],this);
tryLoadMatchingId(data,'input_widget','name',this.input_widgets,this);

if('extensions' in data) {
this.extensions().map(function(e) {
if(data.extensions.indexOf(e.location)>=0)
e.used(true);
});
}
if('input_options' in data) {
tryLoad(data.input_options,['correctAnswer'],this.input_options);
this.input_options.hint.load(data.input_options.hint);
@@ -565,7 +583,8 @@ $(document).ready(function() {
'settings': JSON.stringify(this.settings().map(function(s){ return s.toJSON() })),
'marking_script': this.marking_script(),
'marking_notes': JSON.stringify(this.marking_notes().map(function(n){ return n.toJSON() })),
'ready_to_use': this.ready_to_use()
'ready_to_use': this.ready_to_use(),
'extensions': this.usedExtensions().map(function(e){return e.pk})
}
}
}
@@ -164,6 +164,12 @@ $(document).ready(function() {
this.gapTypes = ko.computed(function(){ return q.partTypes().filter(function(t){ return t.can_be_gap!==false }); });
this.stepTypes = ko.computed(function(){ return q.partTypes().filter(function(t){ return t.can_be_step!==false }); });

this.usedPartTypes = ko.computed(function() {
return Editor.part_types.models.filter(function(pt) {
return q.allParts().some(function(p){ return p.type().name==pt.name });
})
}, this);

//for image attribute modal
this.imageModal = {
width: ko.observable(0),
@@ -215,13 +221,31 @@ $(document).ready(function() {
return out;
},this);

function Extension(q,data) {
var ext = this;
["location","name","edit_url","hasScript","url","scriptURL","author","pk"].forEach(function(k) {
ext[k] = data[k];
});
this.used = ko.observable(false);
this.required = ko.computed(function() {
return q.usedPartTypes().some(function(p){ return p.required_extensions && p.required_extensions.indexOf(ext.location) != -1 });
}, this);
this.used_or_required = ko.computed({
read: function() {
return this.used() || this.required();
},
write: function(v) {
return this.used(v);
}
}, ext);

}

for(var i=0;i<item_json.numbasExtensions.length;i++) {
var ext = item_json.numbasExtensions[i];
ext.used = ko.observable(false);
this.extensions.push(ext);
this.extensions.push(new Extension(this,item_json.numbasExtensions[i]));
}
this.usedExtensions = ko.computed(function() {
return this.extensions().filter(function(e){return e.used()});
return this.extensions().filter(function(e){return e.used_or_required()});
},this);

this.allsets = ko.computed(function() {
@@ -2840,6 +2864,7 @@ $(document).ready(function() {
this.has_marking_settings = data.has_marking_settings || false;
this.tabs = data.tabs || [];
this.model = data.model ? data.model(part) : {};
this.required_extensions = data.required_extensions || [];
this.is_custom_part_type = data.is_custom_part_type;
this.toJSONFn = data.toJSON || function() {};
this.loadFn = data.load || function() {};
@@ -897,6 +897,7 @@ function CustomPartType(data) {
this.settings_def = data.settings;
this.marking_script = data.marking_script;
this.source = data.source;
this.required_extensions = data.extensions || [];
Numbas.partConstructors[this.name] = Numbas.parts.CustomPart;
Numbas.custom_part_types[this.name] = data;

@@ -169,6 +169,25 @@ <h1 class="name-header">
{% endif %}
</section>

<!-- Required extensions -->
<section class="tab-pane" data-bind="css: {active: ko.unwrap($root.currentTab().id)=='extensions'}">
<div class="form-group">
<p class="help-block">
Select any extensions that are required for this part to work.
</p>
<ul class="extensions" data-bind="foreach: extensions">
<li class="extension">
<label>
<span class="checkbox"><input {% if not editable %}disabled{% endif %} type="checkbox" data-bind="checked: used"></span>
<span data-bind="text:name"></span>
<a data-bind="if: url, attr: {href:url, title: 'Documentation on the '+name+' extension'}" target="_blank"><small class="glyphicon glyphicon-book"></small></a>
{% if not user.is_anonymous %}<a data-bind="if: author=={{user.pk}}, attr: {href: edit_url, title: 'Edit the '+name+' extension'}" target="_blank"><small class="glyphicon glyphicon-pencil"></small></a>{% endif %}
</label>
</li>
</ul>
</div>
</section>

<!-- Part settings -->
<section class="tab-pane" data-bind="css: {active: ko.unwrap($root.currentTab().id)=='settings'}">
<p><a target="numbasquickhelp" href="{{HELP_URL}}custom-part-types/reference.html#part-settings"><span class="glyphicon glyphicon-question-sign"></span> Help with part settings</a></p>
@@ -971,7 +971,7 @@ <h4 class="panel-title">Extensions {% helplink 'question/reference.html#term-ext
<ul class="extensions" data-bind="foreach: extensions">
<li class="extension">
<label>
<span class="checkbox"><input {% if not editable %}disabled{% endif %} type="checkbox" data-bind="checked: used"></span>
<span class="checkbox"><input type="checkbox" data-bind="checked: used_or_required, disable: !item_json.editable || required(), attr: { title: required() ? 'This extension is required by one or more question parts' : '' }"></span>
<span data-bind="text:name"></span>
<a data-bind="if: url, attr: {href:url, title: 'Documentation on the '+name+' extension'}" target="_blank"><small class="glyphicon glyphicon-book"></small></a>
{% if not user.is_anonymous %}<a data-bind="if: author=={{user.pk}}, attr: {href: edit_url, title: 'Edit the '+name+' extension'}" target="_blank"><small class="glyphicon glyphicon-pencil"></small></a>{% endif %}
@@ -1,11 +1,12 @@
from django import http
from django.contrib import messages
from django.db import transaction
from django.db.models.functions import Lower
from django.views import generic
from django.urls import reverse
from django.shortcuts import redirect

from editor.models import CustomPartType, CUSTOM_PART_TYPE_PUBLIC_CHOICES, CUSTOM_PART_TYPE_INPUT_WIDGETS
from editor.models import CustomPartType, CUSTOM_PART_TYPE_PUBLIC_CHOICES, CUSTOM_PART_TYPE_INPUT_WIDGETS, Extension
from editor.forms import NewCustomPartTypeForm, UpdateCustomPartTypeForm, CopyCustomPartTypeForm
from editor.views.generic import AuthorRequiredMixin

@@ -50,6 +51,7 @@ def get_form_kwargs(self):
def form_valid(self, form):
with transaction.atomic(), reversion.create_revision():
self.object = form.save(commit=False)
self.object.extensions.set(form.cleaned_data['extensions'])
self.object.save()
reversion.set_user(self.request.user)

@@ -74,8 +76,18 @@ def get_success_url(self):
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)

extensions = Extension.objects.filter(public=True) | self.object.extensions.all()
if not self.request.user.is_anonymous:
extensions |= Extension.objects.filter(author=self.request.user)
extensions = extensions.distinct().order_by(Lower('name'))
context['extensions'] = [e.as_json() for e in extensions]

context['editable'] = self.object.can_be_edited_by(self.request.user)
context['item_json'] = {'data': self.object.as_json(), 'save_url': self.object.get_absolute_url()}
context['item_json'] = {
'data': self.object.as_json(),
'save_url': self.object.get_absolute_url(),
'numbasExtensions': context['extensions']
}

return context

@@ -4,6 +4,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from django.db import transaction
from django.db.models.functions import Lower
from django.http import Http404
from django import http
from django.shortcuts import redirect
@@ -173,7 +174,7 @@ def get_context_data(self, **kwargs):
extensions = Extension.objects.filter(public=True) | self.object.extensions.all()
if not self.request.user.is_anonymous:
extensions |= Extension.objects.filter(author=self.request.user)
extensions = extensions.distinct()
extensions = extensions.distinct().order_by(Lower('name'))
self.item_json['numbasExtensions'] = context['extensions'] = [e.as_json() for e in extensions]

# get publicly available part types first

0 comments on commit e232230

Please sign in to comment.
You can’t perform that action at this time.