diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 2473177b3a..8435cfca9e 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -29,6 +29,10 @@ class CourseDescriptor(SequenceDescriptor): @property def instructors(self): return self.get_about_section("instructors").split("\n") + + @property + def wiki_namespace(self): + return self.location.course def get_about_section(self, section_key): """ diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 52be6a7cbd..858cdb1c87 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -14,6 +14,7 @@ _FIELDS = ['number', # 6.002x 'path', # /some/absolute/filepath/6.002x --> course.xml is in here. 'instructors', # ['Anant Agarwal'] 'institution', # "MIT" + 'wiki_namespace', 'grader', # a courseware.graders.CourseGrader object #'start', # These should be datetime fields diff --git a/lms/djangoapps/simplewiki/admin.py b/lms/djangoapps/simplewiki/admin.py index b53ace1a7a..8d1094bbc0 100644 --- a/lms/djangoapps/simplewiki/admin.py +++ b/lms/djangoapps/simplewiki/admin.py @@ -34,7 +34,7 @@ class ArticleAdminForm(forms.ModelForm): model = Article class ArticleAdmin(admin.ModelAdmin): - list_display = ('created_by', 'slug', 'modified_on', 'parent') + list_display = ('created_by', 'slug', 'modified_on', 'namespace') search_fields = ('slug',) prepopulated_fields = {'slug': ('title',) } inlines = [RevisionInline] diff --git a/lms/djangoapps/simplewiki/mdx_circuit.py b/lms/djangoapps/simplewiki/mdx_circuit.py index 1f47dd0568..bc58ad91ed 100755 --- a/lms/djangoapps/simplewiki/mdx_circuit.py +++ b/lms/djangoapps/simplewiki/mdx_circuit.py @@ -17,8 +17,6 @@ circuit-schematic:[["r",[128,48,0],{"r":"1","_json_":0},["2","1"]],["view",0,0,2 import markdown import re -import simplewiki.settings as settings - from django.utils.html import escape try: @@ -68,5 +66,7 @@ class CircuitLink(markdown.inlinepatterns.Pattern): return etree.fromstring("
") -def makeExtension(configs=None) : - return CircuitExtension(configs=configs) +def makeExtension(configs=None): + to_return = CircuitExtension(configs=configs) + print "circuit returning " , to_return + return to_return diff --git a/lms/djangoapps/simplewiki/mdx_wikipath.py b/lms/djangoapps/simplewiki/mdx_wikipath.py index 5072526247..d9381e4bee 100755 --- a/lms/djangoapps/simplewiki/mdx_wikipath.py +++ b/lms/djangoapps/simplewiki/mdx_wikipath.py @@ -33,7 +33,7 @@ class WikiPathExtension(markdown.Extension): def __init__(self, configs): # set extension defaults self.config = { - 'base_url' : ['/', 'String to append to beginning or URL.'], + 'default_namespace' : ['edX', 'Default namespace for when one isn\'t specified.'], 'html_class' : ['wikipath', 'CSS hook. Leave blank for none.'] } @@ -62,7 +62,10 @@ class WikiPath(markdown.inlinepatterns.Pattern): if article_title.startswith("/"): article_title = article_title[1:] - url = self.config['base_url'][0] + article_title + if not "/" in article_title: + article_title = self.config['default_namespace'][0] + "/" + article_title + + url = "../" + article_title label = m.group('linkTitle') a = etree.Element('a') a.set('href', url) diff --git a/lms/djangoapps/simplewiki/migrations/0001_initial.py b/lms/djangoapps/simplewiki/migrations/0001_initial.py new file mode 100644 index 0000000000..542c39248c --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0001_initial.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Article' + db.create_table('simplewiki_article', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=512)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=100, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)), + ('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True)), + ('locked', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('permissions', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Permission'], null=True, blank=True)), + ('current_revision', self.gf('django.db.models.fields.related.OneToOneField')(blank=True, related_name='current_rev', unique=True, null=True, to=orm['simplewiki.Revision'])), + )) + db.send_create_signal('simplewiki', ['Article']) + + # Adding unique constraint on 'Article', fields ['slug', 'parent'] + db.create_unique('simplewiki_article', ['slug', 'parent_id']) + + # Adding M2M table for field related on 'Article' + db.create_table('simplewiki_article_related', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_article', models.ForeignKey(orm['simplewiki.article'], null=False)), + ('to_article', models.ForeignKey(orm['simplewiki.article'], null=False)) + )) + db.create_unique('simplewiki_article_related', ['from_article_id', 'to_article_id']) + + # Adding model 'ArticleAttachment' + db.create_table('simplewiki_articleattachment', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])), + ('file', self.gf('django.db.models.fields.files.FileField')(max_length=255)), + ('uploaded_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('uploaded_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('simplewiki', ['ArticleAttachment']) + + # Adding model 'Revision' + db.create_table('simplewiki_revision', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])), + ('revision_text', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), + ('revision_user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='wiki_revision_user', null=True, to=orm['auth.User'])), + ('revision_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('contents', self.gf('django.db.models.fields.TextField')()), + ('contents_parsed', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('counter', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('previous_revision', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Revision'], null=True, blank=True)), + ('deleted', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('simplewiki', ['Revision']) + + # Adding model 'Permission' + db.create_table('simplewiki_permission', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('permission_name', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('simplewiki', ['Permission']) + + # Adding M2M table for field can_write on 'Permission' + db.create_table('simplewiki_permission_can_write', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('simplewiki_permission_can_write', ['permission_id', 'user_id']) + + # Adding M2M table for field can_read on 'Permission' + db.create_table('simplewiki_permission_can_read', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('simplewiki_permission_can_read', ['permission_id', 'user_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Article', fields ['slug', 'parent'] + db.delete_unique('simplewiki_article', ['slug', 'parent_id']) + + # Deleting model 'Article' + db.delete_table('simplewiki_article') + + # Removing M2M table for field related on 'Article' + db.delete_table('simplewiki_article_related') + + # Deleting model 'ArticleAttachment' + db.delete_table('simplewiki_articleattachment') + + # Deleting model 'Revision' + db.delete_table('simplewiki_revision') + + # Deleting model 'Permission' + db.delete_table('simplewiki_permission') + + # Removing M2M table for field can_write on 'Permission' + db.delete_table('simplewiki_permission_can_write') + + # Removing M2M table for field can_read on 'Permission' + db.delete_table('simplewiki_permission_can_read') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] \ No newline at end of file diff --git a/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py b/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py new file mode 100644 index 0000000000..f81d8d3431 --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + # We collect every article slug in a set. Any time we see a duplicate, we change the duplicate's name + unique_slugs = set() + for article in orm.Article.objects.all(): + if article.slug in unique_slugs: + i = 2 + new_name = article.slug + str(i) + while new_name in unique_slugs: + i += 1 + new_name = article.slug + str(i) + print "Changing" , article.slug , "to" , new_name + article.slug = new_name + article.save() + + unique_slugs.add( article.slug ) + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] + symmetrical = True diff --git a/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py b/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py new file mode 100644 index 0000000000..0ad2bb6cae --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Article', fields ['slug', 'parent'] + db.delete_unique('simplewiki_article', ['slug', 'parent_id']) + + # Adding model 'Namespace' + db.create_table('simplewiki_namespace', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=30)), + )) + db.send_create_signal('simplewiki', ['Namespace']) + + # Deleting field 'Article.parent' + db.delete_column('simplewiki_article', 'parent_id') + + # Adding field 'Article.namespace' + db.add_column('simplewiki_article', 'namespace', + self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['simplewiki.Namespace']), + keep_default=False) + + # Adding unique constraint on 'Article', fields ['namespace', 'slug'] + db.create_unique('simplewiki_article', ['namespace_id', 'slug']) + + + def backwards(self, orm): + # Removing unique constraint on 'Article', fields ['namespace', 'slug'] + db.delete_unique('simplewiki_article', ['namespace_id', 'slug']) + + # Deleting model 'Namespace' + db.delete_table('simplewiki_namespace') + + # Adding field 'Article.parent' + db.add_column('simplewiki_article', 'parent', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True), + keep_default=False) + + # Deleting field 'Article.namespace' + db.delete_column('simplewiki_article', 'namespace_id') + + # Adding unique constraint on 'Article', fields ['slug', 'parent'] + db.create_unique('simplewiki_article', ['slug', 'parent_id']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.namespace': { + 'Meta': {'object_name': 'Namespace'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] \ No newline at end of file diff --git a/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py b/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py new file mode 100644 index 0000000000..5f61eb9d1d --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + namespace6002x, created = orm.Namespace.objects.get_or_create(name="6.002xS12") + if created: + namespace6002x.save() + + for article in orm.Article.objects.all(): + article.namespace = namespace6002x + article.save() + + def backwards(self, orm): + raise RuntimeError("Cannot reverse this migration.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.namespace': { + 'Meta': {'object_name': 'Namespace'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] + symmetrical = True diff --git a/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py b/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py new file mode 100644 index 0000000000..1ed6b11b67 --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding unique constraint on 'Namespace', fields ['name'] + db.create_unique('simplewiki_namespace', ['name']) + + + def backwards(self, orm): + # Removing unique constraint on 'Namespace', fields ['name'] + db.delete_unique('simplewiki_namespace', ['name']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.namespace': { + 'Meta': {'object_name': 'Namespace'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] \ No newline at end of file diff --git a/lms/djangoapps/simplewiki/migrations/0006_auto.py b/lms/djangoapps/simplewiki/migrations/0006_auto.py new file mode 100644 index 0000000000..2f7c698f04 --- /dev/null +++ b/lms/djangoapps/simplewiki/migrations/0006_auto.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding index on 'Namespace', fields ['name'] + db.create_index('simplewiki_namespace', ['name']) + + + def backwards(self, orm): + # Removing index on 'Namespace', fields ['name'] + db.delete_index('simplewiki_namespace', ['name']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'simplewiki.article': { + 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), + 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), + 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) + }, + 'simplewiki.articleattachment': { + 'Meta': {'object_name': 'ArticleAttachment'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'simplewiki.namespace': { + 'Meta': {'object_name': 'Namespace'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30', 'db_index': 'True'}) + }, + 'simplewiki.permission': { + 'Meta': {'object_name': 'Permission'}, + 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'simplewiki.revision': { + 'Meta': {'object_name': 'Revision'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), + 'contents': ('django.db.models.fields.TextField', [], {}), + 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), + 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['simplewiki'] \ No newline at end of file diff --git a/lms/djangoapps/simplewiki/migrations/__init__.py b/lms/djangoapps/simplewiki/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/simplewiki/models copy.py b/lms/djangoapps/simplewiki/models copy.py new file mode 100644 index 0000000000..ade95ed491 --- /dev/null +++ b/lms/djangoapps/simplewiki/models copy.py @@ -0,0 +1,367 @@ +import difflib +import os + +from django import forms +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.db import models +from django.db.models import signals +from django.utils.translation import ugettext_lazy as _ +from markdown import markdown + +from wiki_settings import * +from util.cache import cache + + +class ShouldHaveExactlyOneRootSlug(Exception): + pass + +class Article(models.Model): + """Wiki article referring to Revision model for actual content. + 'slug' and 'parent' field should be maintained centrally, since users + aren't allowed to change them, anyways. + """ + + title = models.CharField(max_length=512, verbose_name=_('Article title'), + blank=False) + slug = models.SlugField(max_length=100, verbose_name=_('slug'), + help_text=_('Letters, numbers, underscore and hyphen.'), + blank=True) + created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True) + created_on = models.DateTimeField(auto_now_add = 1) + modified_on = models.DateTimeField(auto_now_add = 1) + parent = models.ForeignKey('self', verbose_name=_('Parent article slug'), + help_text=_('Affects URL structure and possibly inherits permissions'), + null=True, blank=True) + locked = models.BooleanField(default=False, verbose_name=_('Locked for editing')) + permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'), + blank=True, null=True, + help_text=_('Permission group')) + current_revision = models.OneToOneField('Revision', related_name='current_rev', + blank=True, null=True, editable=True) + related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True, + help_text=_('Sets a symmetrical relation other articles'), + blank=True, null=True) + + def attachments(self): + return ArticleAttachment.objects.filter(article__exact = self) + + @classmethod + def get_root(cls): + """Return the root article, which should ALWAYS exist.. + except the very first time the wiki is loaded, in which + case the user is prompted to create this article.""" + try: + return Article.objects.filter(slug__exact = "")[0] + except: + raise ShouldHaveExactlyOneRootSlug() + + def get_url(self): + """Return the Wiki URL for an article""" + url = self.slug + "/" + if self.parent_id: + parent_url = cache.get("wiki_url-" + str(self.parent_id)) + if parent_url is None: + parent_url = self.parent.get_url() + + url = parent_url + url + + cache.set("wiki_url-" + str(self.id), url, 60*60) + + return url + + def get_abs_url(self): + """Return the absolute path for an article. This is necessary in cases + where the template system isn't used for generating URLs...""" + # TODO: Remove and create a reverse() lookup. + return WIKI_BASE + self.get_url() + + @models.permalink + def get_absolute_url(self): + return ('wiki_view', [self.get_url()]) + + @classmethod + def get_url_reverse(cls, path, article, return_list=[]): + """Lookup a URL and return the corresponding set of articles + in the path.""" + if path == []: + return return_list + [article] + # Lookup next child in path + try: + a = Article.objects.get(parent__exact = article, slug__exact=str(path[0])) + return cls.get_url_reverse(path[1:], a, return_list+[article]) + except Exception, e: + return None + + def can_read(self, user): + """ Check read permissions and return True/False.""" + if user.is_superuser: + return True + if self.permissions: + perms = self.permissions.can_read.all() + return perms.count() == 0 or (user in perms) + else: + return self.parent.can_read(user) if self.parent else True + + def can_write(self, user): + """ Check write permissions and return True/False.""" + if user.is_superuser: + return True + if self.permissions: + perms = self.permissions.can_write.all() + return perms.count() == 0 or (user in perms) + else: + return self.parent.can_write(user) if self.parent else True + + def can_write_l(self, user): + """Check write permissions and locked status""" + if user.is_superuser: + return True + return not self.locked and self.can_write(user) + + def can_attach(self, user): + return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous()) + + def __unicode__(self): + if self.slug == '' and not self.parent: + return unicode(_('Root article')) + else: + return self.get_url() + + class Meta: + unique_together = (('slug', 'parent'),) + verbose_name = _('Article') + verbose_name_plural = _('Articles') + +def get_attachment_filepath(instance, filename): + """Store file, appending new extension for added security""" + dir_ = WIKI_ATTACHMENTS + instance.article.get_url() + dir_ = '/'.join(filter(lambda x: x!='', dir_.split('/'))) + if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_): + os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_) + return dir_ + '/' + filename + '.upload' + +class ArticleAttachment(models.Model): + article = models.ForeignKey(Article, verbose_name=_('Article')) + file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment')) + uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True) + uploaded_on = models.DateTimeField(auto_now_add = True, verbose_name=_('Upload date')) + + def download_url(self): + return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename())) + + def filename(self): + return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) + + def get_size(self): + try: + size = self.file.size + except OSError: + size = 0 + return size + + def filename(self): + return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) + + def is_image(self): + fname = self.filename().split('.') + if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS: + return True + return False + + def get_thumb(self): + return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE) + + def get_thumb_small(self): + return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL) + + def mk_thumbs(self): + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True}) + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True}) + + def mk_thumb(self, width, height, force=False): + """Requires Python Imaging Library (PIL)""" + if not self.get_size(): + return False + + if not self.is_image(): + return False + + base_path = os.path.dirname(self.file.path) + orig_name = self.filename().split('.') + thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) + thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename) + + if force or not os.path.exists(thumb_filepath): + try: + import Image + img = Image.open(self.file.path) + img.thumbnail((width,height), Image.ANTIALIAS) + img.save(thumb_filepath) + except IOError: + return False + + return True + + def get_thumb_impl(self, width, height): + """Requires Python Imaging Library (PIL)""" + + if not self.get_size(): + return False + + if not self.is_image(): + return False + + self.mk_thumb(width, height) + + orig_name = self.filename().split('.') + thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) + thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename + + return thumb_url + + def __unicode__(self): + return self.filename() + +class Revision(models.Model): + + article = models.ForeignKey(Article, verbose_name=_('Article')) + revision_text = models.CharField(max_length=255, blank=True, null=True, + verbose_name=_('Description of change')) + revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), + blank=True, null=True, related_name='wiki_revision_user') + revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date')) + contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)')) + contents_parsed = models.TextField(editable=False, blank=True, null=True) + counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False) + previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False) + + # Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should + # be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed + # from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't + # show up in the history. + deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0) + + def get_user(self): + return self.revision_user if self.revision_user else _('Anonymous') + + # Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in + # save that update the revision or reject the save if contents haven't changed + def adminSetDeleted(self, deleted): + self.deleted = deleted + super(Revision, self).save() + + def save(self, **kwargs): + # Check if contents have changed... if not, silently ignore save + if self.article and self.article.current_revision: + if self.deleted == 0 and self.article.current_revision.contents == self.contents: + return + else: + import datetime + self.article.modified_on = datetime.datetime.now() + self.article.save() + + # Increment counter according to previous revision + previous_revision = Revision.objects.filter(article=self.article).order_by('-counter') + if previous_revision.count() > 0: + if previous_revision.count() > previous_revision[0].counter: + self.counter = previous_revision.count() + 1 + else: + self.counter = previous_revision[0].counter + 1 + else: + self.counter = 1 + if (self.article.current_revision and self.article.current_revision.deleted == 0): + self.previous_revision = self.article.current_revision + + # Create pre-parsed contents - no need to parse on-the-fly + ext = WIKI_MARKDOWN_EXTENSIONS + ext += ["wikipath(base_url=%s)" % reverse('wiki_view', args=('/',))] + self.contents_parsed = markdown(self.contents, + extensions=ext, + safe_mode='escape',) + super(Revision, self).save(**kwargs) + + def delete(self, **kwargs): + """If a current revision is deleted, then regress to the previous + revision or insert a stub, if no other revisions are available""" + article = self.article + if article.current_revision == self: + prev_revision = Revision.objects.filter(article__exact = article, + pk__not = self.pk).order_by('-counter') + if prev_revision: + article.current_revision = prev_revision[0] + article.save() + else: + r = Revision(article=article, + revision_user = article.created_by) + r.contents = unicode(_('Auto-generated stub')) + r.revision_text= unicode(_('Auto-generated stub')) + r.save() + article.current_revision = r + article.save() + super(Revision, self).delete(**kwargs) + + def get_diff(self): + if (self.deleted == 1): + yield "Article Deletion" + return + + if self.previous_revision: + previous = self.previous_revision.contents.splitlines(1) + else: + previous = [] + + # Todo: difflib.HtmlDiff would look pretty for our history pages! + diff = difflib.unified_diff(previous, self.contents.splitlines(1)) + # let's skip the preamble + diff.next(); diff.next(); diff.next() + + for d in diff: + yield d + + def __unicode__(self): + return "r%d" % self.counter + + class Meta: + verbose_name = _('article revision') + verbose_name_plural = _('article revisions') + +class Permission(models.Model): + permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name')) + can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write', + help_text=_('Select none to grant anonymous access.')) + can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read', + help_text=_('Select none to grant anonymous access.')) + def __unicode__(self): + return self.permission_name + class Meta: + verbose_name = _('Article permission') + verbose_name_plural = _('Article permissions') + +class RevisionForm(forms.ModelForm): + contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50})) + class Meta: + model = Revision + fields = ['contents', 'revision_text'] +class RevisionFormWithTitle(forms.ModelForm): + title = forms.CharField(label=_('Title')) + class Meta: + model = Revision + fields = ['title', 'contents', 'revision_text'] +class CreateArticleForm(RevisionForm): + title = forms.CharField(label=_('Title')) + class Meta: + model = Revision + fields = ['title', 'contents',] + +def set_revision(sender, *args, **kwargs): + """Signal handler to ensure that a new revision is always chosen as the + current revision - automatically. It simplifies stuff greatly. Also + stores previous revision for diff-purposes""" + instance = kwargs['instance'] + created = kwargs['created'] + if created and instance.article: + instance.article.current_revision = instance + instance.article.save() + +signals.post_save.connect(set_revision, Revision) diff --git a/lms/djangoapps/simplewiki/models.py b/lms/djangoapps/simplewiki/models.py index ade95ed491..200f161eba 100644 --- a/lms/djangoapps/simplewiki/models.py +++ b/lms/djangoapps/simplewiki/models.py @@ -16,9 +16,23 @@ from util.cache import cache class ShouldHaveExactlyOneRootSlug(Exception): pass +class Namespace(models.Model): + name = models.CharField(max_length=30, db_index=True, unique=True, verbose_name=_('namespace')) + # TODO: We may want to add permissions, etc later + + @classmethod + def ensure_namespace(cls, name): + try: + namespace = Namespace.objects.get(name__exact = name) + except Namespace.DoesNotExist: + new_namespace = Namespace(name=name) + new_namespace.save() + + + class Article(models.Model): """Wiki article referring to Revision model for actual content. - 'slug' and 'parent' field should be maintained centrally, since users + 'slug' and 'title' field should be maintained centrally, since users aren't allowed to change them, anyways. """ @@ -27,12 +41,10 @@ class Article(models.Model): slug = models.SlugField(max_length=100, verbose_name=_('slug'), help_text=_('Letters, numbers, underscore and hyphen.'), blank=True) + namespace = models.ForeignKey(Namespace, verbose_name=_('Namespace')) created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True) created_on = models.DateTimeField(auto_now_add = 1) modified_on = models.DateTimeField(auto_now_add = 1) - parent = models.ForeignKey('self', verbose_name=_('Parent article slug'), - help_text=_('Affects URL structure and possibly inherits permissions'), - null=True, blank=True) locked = models.BooleanField(default=False, verbose_name=_('Locked for editing')) permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'), blank=True, null=True, @@ -45,53 +57,44 @@ class Article(models.Model): def attachments(self): return ArticleAttachment.objects.filter(article__exact = self) + + def get_path(self): + return self.namespace.name + "/" + self.slug + + @classmethod + def get_article(cls, article_path): + """ + Given an article_path like namespace/slug, this returns the article. It may raise + a Article.DoesNotExist if no matching article is found or ValueError if the + article_path is not constructed properly. + """ + #TODO: Verify the path, throw a meaningful error? + namespace, slug = article_path.split("/") + return Article.objects.get( slug__exact = slug, namespace__name__exact = namespace) + @classmethod - def get_root(cls): + def get_root(cls, namespace): """Return the root article, which should ALWAYS exist.. except the very first time the wiki is loaded, in which case the user is prompted to create this article.""" try: - return Article.objects.filter(slug__exact = "")[0] + return Article.objects.filter(slug__exact = "", namespace__name__exact = namespace)[0] except: raise ShouldHaveExactlyOneRootSlug() - def get_url(self): - """Return the Wiki URL for an article""" - url = self.slug + "/" - if self.parent_id: - parent_url = cache.get("wiki_url-" + str(self.parent_id)) - if parent_url is None: - parent_url = self.parent.get_url() - - url = parent_url + url - - cache.set("wiki_url-" + str(self.id), url, 60*60) - - return url - - def get_abs_url(self): - """Return the absolute path for an article. This is necessary in cases - where the template system isn't used for generating URLs...""" - # TODO: Remove and create a reverse() lookup. - return WIKI_BASE + self.get_url() - - @models.permalink - def get_absolute_url(self): - return ('wiki_view', [self.get_url()]) - - @classmethod - def get_url_reverse(cls, path, article, return_list=[]): - """Lookup a URL and return the corresponding set of articles - in the path.""" - if path == []: - return return_list + [article] - # Lookup next child in path - try: - a = Article.objects.get(parent__exact = article, slug__exact=str(path[0])) - return cls.get_url_reverse(path[1:], a, return_list+[article]) - except Exception, e: - return None + # @classmethod + # def get_url_reverse(cls, path, article, return_list=[]): + # """Lookup a URL and return the corresponding set of articles + # in the path.""" + # if path == []: + # return return_list + [article] + # # Lookup next child in path + # try: + # a = Article.objects.get(parent__exact = article, slug__exact=str(path[0])) + # return cls.get_url_reverse(path[1:], a, return_list+[article]) + # except Exception, e: + # return None def can_read(self, user): """ Check read permissions and return True/False.""" @@ -101,7 +104,8 @@ class Article(models.Model): perms = self.permissions.can_read.all() return perms.count() == 0 or (user in perms) else: - return self.parent.can_read(user) if self.parent else True + # TODO: We can inherit namespace permissions here + return True def can_write(self, user): """ Check write permissions and return True/False.""" @@ -111,7 +115,8 @@ class Article(models.Model): perms = self.permissions.can_write.all() return perms.count() == 0 or (user in perms) else: - return self.parent.can_write(user) if self.parent else True + # TODO: We can inherit namespace permissions here + return True def can_write_l(self, user): """Check write permissions and locked status""" @@ -123,13 +128,13 @@ class Article(models.Model): return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous()) def __unicode__(self): - if self.slug == '' and not self.parent: + if self.slug == '': return unicode(_('Root article')) else: - return self.get_url() + return self.slug class Meta: - unique_together = (('slug', 'parent'),) + unique_together = (('slug', 'namespace'),) verbose_name = _('Article') verbose_name_plural = _('Articles') @@ -275,7 +280,7 @@ class Revision(models.Model): # Create pre-parsed contents - no need to parse on-the-fly ext = WIKI_MARKDOWN_EXTENSIONS - ext += ["wikipath(base_url=%s)" % reverse('wiki_view', args=('/',))] + ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name ] self.contents_parsed = markdown(self.contents, extensions=ext, safe_mode='escape',) diff --git a/lms/djangoapps/simplewiki/urls.py b/lms/djangoapps/simplewiki/urls.py index a41ce3617b..2d01c06bf9 100644 --- a/lms/djangoapps/simplewiki/urls.py +++ b/lms/djangoapps/simplewiki/urls.py @@ -1,20 +1,19 @@ -from django.conf.urls.defaults import * +from django.conf.urls.defaults import patterns, url + +namespace_regex = r"[a-zA-Z\d._-]+" +article_slug = r'/(?P' + namespace_regex + r'/[a-zA-Z\d_-]*)' +namespace = r'/(?P' + namespace_regex + r')' urlpatterns = patterns('', - url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), - url(r'^view(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.view', name='wiki_view'), - url(r'^view_revision/([0-9]*)(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.view_revision', name='wiki_view_revision'), - url(r'^edit(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.edit', name='wiki_edit'), - url(r'^create(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.create', name='wiki_create'), - url(r'^history(/[a-zA-Z\d/_-]*)/([0-9]*)/?$', 'simplewiki.views.history', name='wiki_history'), - url(r'^search_related(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.search_add_related', name='search_related'), - url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), - url(r'^revision_feed/([0-9]*)/?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), - url(r'^search/?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), - url(r'^list/?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), #Just an alias for the search, but you usually don't submit a search term -# url(r'^/?([a-zA-Z\d/_-]*)/_related/add/$', 'simplewiki.views.add_related', name='add_related'), -# url(r'^/?([a-zA-Z\d/_-]*)/_related/remove/(\d+)$', 'simplewiki.views.remove_related', name='wiki_remove_relation'), -# url(r'^/?([a-zA-Z\d/_-]*)/_add_attachment/$', 'simplewiki.views_attachments.add_attachment', name='add_attachment'), -# url(r'^/?([a-zA-Z\d/_-]*)/_view_attachment/(.+)?$', 'simplewiki.views_attachments.view_attachment', name='wiki_view_attachment'), -# url(r'^(.*)$', 'simplewiki.views.encode_err', name='wiki_encode_err') + url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), + url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'), + url(r'^view_revision/(?P[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'), + url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'), + url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'), + url(r'^history' + article_slug + r'(?:/(?P[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'), + url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'), + url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), + url(r'^revision_feed' + namespace + r'/(?P[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), + url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), + url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), #Just an alias for the search, but you usually don't submit a search term ) diff --git a/lms/djangoapps/simplewiki/views copy.py b/lms/djangoapps/simplewiki/views copy.py new file mode 100644 index 0000000000..553e6e8d19 --- /dev/null +++ b/lms/djangoapps/simplewiki/views copy.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- +from django.conf import settings as settings +from django.contrib.auth.decorators import login_required +from django.core.context_processors import csrf +from django.core.urlresolvers import reverse +from django.db.models import Q +from django.http import HttpResponse, HttpResponseRedirect +from django.utils import simplejson +from django.utils.translation import ugettext_lazy as _ +from mitxmako.shortcuts import render_to_response + +from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm +import wiki_settings + +def view(request, wiki_url): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True, check_deleted=True) + if perm_err: + return perm_err + d = {'wiki_article': article, + 'wiki_article_revision':article.current_revision, + 'wiki_write': article.can_write_l(request.user), + 'wiki_attachments_write': article.can_attach(request.user), + 'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0), + 'wiki_title' : article.title + " - MITX 6.002x Wiki" + } + d.update(csrf(request)) + return render_to_response('simplewiki_view.html', d) + +def view_revision(request, revision_number, wiki_url, revision=None): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + try: + revision = Revision.objects.get(counter=int(revision_number), article=article) + except: + d = {'wiki_article': article, + 'wiki_err_norevision': revision_number,} + d.update(csrf(request)) + return render_to_response('simplewiki_error.html', d) + + + perm_err = check_permissions(request, article, check_read=True, check_deleted=True, revision=revision) + if perm_err: + return perm_err + + d = {'wiki_article': article, + 'wiki_article_revision':revision, + 'wiki_write': article.can_write_l(request.user), + 'wiki_attachments_write': article.can_attach(request.user), + 'wiki_current_revision_deleted' : not (revision.deleted == 0), + } + d.update(csrf(request)) + return render_to_response('simplewiki_view.html', d) + + +def root_redirect(request): + try: + root = Article.get_root() + except: + err = not_found(request, '/') + return err + + return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url()))) + +def create(request, wiki_url): + + url_path = get_url_path(wiki_url) + + if url_path != [] and url_path[0].startswith('_'): + d = {'wiki_err_keyword': True, + 'wiki_url': '/'.join(url_path) } + d.update(csrf(request)) + return render_to_response('simplewiki_error.html', d) + + # Lookup path + try: + # Ensure that the path exists... + root = Article.get_root() + # Remove root slug if present in path + if url_path and root.slug == url_path[0]: + url_path = url_path[1:] + + path = Article.get_url_reverse(url_path[:-1], root) + if not path: + d = {'wiki_err_noparent': True, + 'wiki_url_parent': '/'.join(url_path[:-1]) } + d.update(csrf(request)) + return render_to_response('simplewiki_error.html', d) + + perm_err = check_permissions(request, path[-1], check_locked=False, check_write=True, check_deleted=True) + if perm_err: + return perm_err + # Ensure doesn't already exist + article = Article.get_url_reverse(url_path, root) + if article: + return HttpResponseRedirect(reverse('wiki_view', args=(article[-1].get_url(),))) + + # TODO: Somehow this doesnt work... + #except ShouldHaveExactlyOneRootSlug, (e): + except: + if Article.objects.filter(parent=None).count() > 0: + return HttpResponseRedirect(reverse('wiki_view', args=('/',))) + # Root not found... + path = [] + url_path = [""] + + if request.method == 'POST': + f = CreateArticleForm(request.POST) + if f.is_valid(): + article = Article() + article.slug = url_path[-1] + if not request.user.is_anonymous(): + article.created_by = request.user + article.title = f.cleaned_data.get('title') + if path != []: + article.parent = path[-1] + a = article.save() + new_revision = f.save(commit=False) + if not request.user.is_anonymous(): + new_revision.revision_user = request.user + new_revision.article = article + new_revision.save() + import django.db as db + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + else: + f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', url_path[-1]), + 'contents':_('Headline\n===\n\n')}) + + d = {'wiki_form': f, + 'wiki_write': True, + 'create_article' : True, + } + d.update(csrf(request)) + + return render_to_response('simplewiki_edit.html', d) + +def edit(request, wiki_url): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + # Check write permissions + perm_err = check_permissions(request, article, check_write=True, check_locked=True, check_deleted=False) + if perm_err: + return perm_err + + if wiki_settings.WIKI_ALLOW_TITLE_EDIT: + EditForm = RevisionFormWithTitle + else: + EditForm = RevisionForm + + if request.method == 'POST': + f = EditForm(request.POST) + if f.is_valid(): + new_revision = f.save(commit=False) + new_revision.article = article + + if request.POST.__contains__('delete'): + if (article.current_revision.deleted == 1): #This article has already been deleted. Redirect + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + new_revision.contents = "" + new_revision.deleted = 1 + elif not new_revision.get_diff(): + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + + if not request.user.is_anonymous(): + new_revision.revision_user = request.user + new_revision.save() + if wiki_settings.WIKI_ALLOW_TITLE_EDIT: + new_revision.article.title = f.cleaned_data['title'] + new_revision.article.save() + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + else: + startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n' + + f = EditForm({'contents': startContents, 'title': article.title}) + d = {'wiki_form': f, + 'wiki_write': True, + 'wiki_article': article, + 'wiki_title' : article.title, + 'wiki_attachments_write': article.can_attach(request.user), + 'create_article' : False, + } + d.update(csrf(request)) + + return render_to_response('simplewiki_edit.html', d) + +def history(request, wiki_url, page=1): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True, check_deleted=False) + if perm_err: + print "returned error " , perm_err + return perm_err + + page_size = 10 + + try: + p = int(page) + except ValueError: + p = 1 + + history = Revision.objects.filter(article__exact = article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article') + + if request.method == 'POST': + if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT and not request.user.is_authenticated(): + return HttpResponseRedirect('/') + + if request.POST.__contains__('revision'): #They selected a version, but they can be either deleting or changing the version + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + redirectURL = reverse('wiki_view', args=(article.get_url(),)) + try: + r = int(request.POST['revision']) + revision = Revision.objects.get(id=r) + if request.POST.__contains__('change'): + article.current_revision = revision + article.save() + elif request.POST.__contains__('view'): + redirectURL = reverse('wiki_view_revision', args=(revision.counter, article.get_url(),)) + + #The rese of these are admin functions + elif request.POST.__contains__('delete') and request.user.is_superuser: + if (revision.deleted == 0): + revision.adminSetDeleted(2) + elif request.POST.__contains__('restore') and request.user.is_superuser: + if (revision.deleted == 2): + revision.adminSetDeleted(0) + elif request.POST.__contains__('delete_all') and request.user.is_superuser: + Revision.objects.filter(article__exact = article, deleted = 0).update(deleted = 2) + elif request.POST.__contains__('lock_article'): + print "changing locked article " , article.locked + article.locked = not article.locked + print "changed locked article " , article.locked + article.save() + except: + pass + finally: + return HttpResponseRedirect(redirectURL) + # + # + # + # + # + # %else: + # + # + + page_count = (history.count()+(page_size-1)) / page_size + if p > page_count: + p = 1 + beginItem = (p-1) * page_size + + next_page = p + 1 if page_count > p else None + prev_page = p - 1 if p > 1 else None + + d = {'wiki_page': p, + 'wiki_next_page': next_page, + 'wiki_prev_page': prev_page, + 'wiki_write': article.can_write_l(request.user), + 'wiki_attachments_write': article.can_attach(request.user), + 'wiki_article': article, + 'wiki_title': article.title, + 'wiki_history': history[beginItem:beginItem+page_size], + 'show_delete_revision' : request.user.is_superuser,} + d.update(csrf(request)) + + return render_to_response('simplewiki_history.html', d) + + +def revision_feed(request, page=1): + page_size = 10 + + try: + p = int(page) + except ValueError: + p = 1 + + history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision') + + page_count = (history.count()+(page_size-1)) / page_size + if p > page_count: + p = 1 + beginItem = (p-1) * page_size + + next_page = p + 1 if page_count > p else None + prev_page = p - 1 if p > 1 else None + + d = {'wiki_page': p, + 'wiki_next_page': next_page, + 'wiki_prev_page': prev_page, + 'wiki_history': history[beginItem:beginItem+page_size], + 'show_delete_revision' : request.user.is_superuser,} + d.update(csrf(request)) + + return render_to_response('simplewiki_revision_feed.html', d) + +def search_articles(request): + # blampe: We should check for the presence of other popular django search + # apps and use those if possible. Only fall back on this as a last resort. + # Adding some context to results (eg where matches were) would also be nice. + + # todo: maybe do some perm checking here + + if request.method == 'POST': + querystring = request.POST['value'].strip() + else: + querystring = "" + + + results = Article.objects.all() + + if request.user.is_superuser: + results = results.order_by('current_revision__deleted') + else: + results = results.filter(current_revision__deleted = 0) + + + if querystring: + for queryword in querystring.split(): + # Basic negation is as fancy as we get right now + if queryword[0] == '-' and len(queryword) > 1: + results._search = lambda x: results.exclude(x) + queryword = queryword[1:] + else: + results._search = lambda x: results.filter(x) + + results = results._search(Q(current_revision__contents__icontains = queryword) | \ + Q(title__icontains = queryword)) + + results = results.select_related('current_revision__deleted') + + results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_url().lower()) ) + + if len(results) == 1 and querystring: + return HttpResponseRedirect(reverse('wiki_view', args=(results[0].get_url(),))) + else: + d = {'wiki_search_results': results, + 'wiki_search_query': querystring,} + d.update(csrf(request)) + return render_to_response('simplewiki_searchresults.html', d) + + +def search_add_related(request, wiki_url): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True) + if perm_err: + return perm_err + + search_string = request.GET.get('query', None) + self_pk = request.GET.get('self', None) + if search_string: + results = [] + related = Article.objects.filter(title__istartswith = search_string) + others = article.related.all() + if self_pk: + related = related.exclude(pk=self_pk) + if others: + related = related.exclude(related__in = others) + related = related.order_by('title')[:10] + for item in related: + results.append({'id': str(item.id), + 'value': item.title, + 'info': item.get_url()}) + else: + results = [] + + json = simplejson.dumps({'results': results}) + return HttpResponse(json, mimetype='application/json') + +def add_related(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + try: + related_id = request.POST['id'] + rel = Article.objects.get(id=related_id) + has_already = article.related.filter(id=related_id).count() + if has_already == 0 and not rel == article: + article.related.add(rel) + article.save() + except: + pass + finally: + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def remove_related(request, wiki_url, related_id): + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + try: + rel_id = int(related_id) + rel = Article.objects.get(id=rel_id) + article.related.remove(rel) + article.save() + except: + pass + finally: + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def random_article(request): + from random import randint + num_arts = Article.objects.count() + article = Article.objects.all()[randint(0, num_arts-1)] + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def encode_err(request, url): + d = {'wiki_err_encode': True} + d.update(csrf(request)) + return render_to_response('simplewiki_error.html', d) + +def not_found(request, wiki_url): + """Generate a NOT FOUND message for some URL""" + d = {'wiki_err_notfound': True, + 'wiki_url': wiki_url} + d.update(csrf(request)) + return render_to_response('simplewiki_error.html', d) + +def get_url_path(url): + """Return a list of all actual elements of a url, safely ignoring + double-slashes (//) """ + return filter(lambda x: x!='', url.split('/')) + +def fetch_from_url(request, url): + """Analyze URL, returning the article and the articles in its path + If something goes wrong, return an error HTTP response""" + + err = None + article = None + path = None + + url_path = get_url_path(url) + + try: + root = Article.get_root() + except: + err = not_found(request, '/') + return (article, path, err) + + if url_path and root.slug == url_path[0]: + url_path = url_path[1:] + + path = Article.get_url_reverse(url_path, root) + if not path: + err = not_found(request, '/' + '/'.join(url_path)) + else: + article = path[-1] + return (article, path, err) + + +def check_permissions(request, article, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision = None): + read_err = check_read and not article.can_read(request.user) + + write_err = check_write and not article.can_write(request.user) + + locked_err = check_locked and article.locked + + if revision == None: + revision = article.current_revision + deleted_err = check_deleted and not (revision.deleted == 0) + if (request.user.is_superuser): + deleted_err = False + locked_err = False + + if read_err or write_err or locked_err or deleted_err: + d = {'wiki_article': article, + 'wiki_err_noread': read_err, + 'wiki_err_nowrite': write_err, + 'wiki_err_locked': locked_err, + 'wiki_err_deleted': deleted_err,} + d.update(csrf(request)) + # TODO: Make this a little less jarring by just displaying an error + # on the current page? (no such redirect happens for an anon upload yet) + # benjaoming: I think this is the nicest way of displaying an error, but + # these errors shouldn't occur, but rather be prevented on the other pages. + return render_to_response('simplewiki_error.html', d) + else: + return None + +#################### +# LOGIN PROTECTION # +#################### + +if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW: + view = login_required(view) + history = login_required(history) + search_articles = login_required(search_articles) + root_redirect = login_required(root_redirect) + revision_feed = login_required(revision_feed) + random_article = login_required(random_article) + search_add_related = login_required(search_add_related) + not_found = login_required(not_found) + view_revision = login_required(view_revision) + +if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT: + create = login_required(create) + edit = login_required(edit) + add_related = login_required(add_related) + remove_related = login_required(remove_related) + +if wiki_settings.WIKI_CONTEXT_PREPROCESSORS: + settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py index 167ce27d95..81ce8abf76 100644 --- a/lms/djangoapps/simplewiki/views.py +++ b/lms/djangoapps/simplewiki/views.py @@ -4,155 +4,178 @@ from django.contrib.auth.decorators import login_required from django.core.context_processors import csrf from django.core.urlresolvers import reverse from django.db.models import Q -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.utils import simplejson from django.utils.translation import ugettext_lazy as _ from mitxmako.shortcuts import render_to_response -from multicourse import multicourse_settings +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.django import modulestore -from models import Revision, Article, CreateArticleForm, RevisionFormWithTitle, RevisionForm +from models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm import wiki_settings -def view(request, wiki_url): - (article, path, err) = fetch_from_url(request, wiki_url) +def get_course(course_id): + if course_id == None: + return None + + course_loc = CourseDescriptor.id_to_location(course_id) + course = modulestore().get_item(course_loc) + # raise Http404("Course not found") + return course + +def wiki_reverse(wiki_page, article = None, course = None, namespace=None, args=[], kwargs={}): + kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'} + if not 'course_id' in kwargs and course: + kwargs['course_id'] = course.id + if not 'article_path' in kwargs and article: + kwargs['article_path'] = article.get_path() + if not 'namespace' in kwargs and namespace: + kwargs['namespace'] = namespace + return reverse(wiki_page, kwargs=kwargs, args=args) + +def update_template_dictionary(dictionary, request = None, course = None, article = None, revision = None): + if article: + dictionary['wiki_article'] = article + dictionary['wiki_title'] = article.title #TODO: What is the title when viewing the article in a course? + if not course and 'namespace' not in dictionary: + dictionary['namespace'] = article.namespace.name + + if course: + dictionary['course'] = course + if 'namespace' not in dictionary: + dictionary['namespace'] = course.wiki_namespace + else: + dictionary['course'] = None + + if revision: + dictionary['wiki_article_revision'] = revision + dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0) + + if request: + dictionary.update(csrf(request)) + + +def view(request, article_path, course_id=None): + course = get_course(course_id) + + (article, err) = get_article(request, article_path, course ) if err: return err - if 'coursename' in request.session: coursename = request.session['coursename'] - else: coursename = None - - course_number = multicourse_settings.get_course_number(coursename) - - perm_err = check_permissions(request, article, check_read=True, check_deleted=True) + perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True) if perm_err: return perm_err - d = {'wiki_article': article, - 'wiki_article_revision':article.current_revision, - 'wiki_write': article.can_write_l(request.user), - 'wiki_attachments_write': article.can_attach(request.user), - 'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0), - 'wiki_title' : article.title + " - MITX %s Wiki" % course_number - } - d.update(csrf(request)) - return render_to_response('simplewiki_view.html', d) -def view_revision(request, revision_number, wiki_url, revision=None): - (article, path, err) = fetch_from_url(request, wiki_url) + d = {} + update_template_dictionary(d, request, course, article, article.current_revision) + return render_to_response('simplewiki/simplewiki_view.html', d) + +def view_revision(request, revision_number, article_path, course_id=None): + course = get_course(course_id) + (article, err) = get_article(request, article_path, course ) if err: return err try: revision = Revision.objects.get(counter=int(revision_number), article=article) except: - d = {'wiki_article': article, - 'wiki_err_norevision': revision_number,} - d.update(csrf(request)) - return render_to_response('simplewiki_error.html', d) - + d = {'wiki_err_norevision': revision_number} + update_template_dictionary(d, request, course, article) + return render_to_response('simplewiki/simplewiki_error.html', d) - perm_err = check_permissions(request, article, check_read=True, check_deleted=True, revision=revision) + perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision) if perm_err: return perm_err - - d = {'wiki_article': article, - 'wiki_article_revision':revision, - 'wiki_write': article.can_write_l(request.user), - 'wiki_attachments_write': article.can_attach(request.user), - 'wiki_current_revision_deleted' : not (revision.deleted == 0), - } - d.update(csrf(request)) - return render_to_response('simplewiki_view.html', d) + + d = {} + update_template_dictionary(d, request, course, article, revision) + + return render_to_response('simplewiki/simplewiki_view.html', d) -def root_redirect(request): +def root_redirect(request, course_id=None): + course = get_course(course_id) try: - root = Article.get_root() + root = Article.get_root(course.wiki_namespace) except: - err = not_found(request, '/') + # If the root is not found, we probably are loading this class for the first time + # We should make sure the namespace exists so the root article can be created. + Namespace.ensure_namespace(course.wiki_namespace) + + err = not_found(request, course.wiki_namespace + '/', course) return err - - return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url()))) - -def create(request, wiki_url): - url_path = get_url_path(wiki_url) + return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id' : course_id, 'article_path' : root.get_path()} )) - if url_path != [] and url_path[0].startswith('_'): - d = {'wiki_err_keyword': True, - 'wiki_url': '/'.join(url_path) } - d.update(csrf(request)) - return render_to_response('simplewiki_error.html', d) +def create(request, article_path, course_id=None): + course = get_course(course_id) + + article_path_components = article_path.split('/') - # Lookup path + # Ensure the namespace exists + if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0: + d = {'wiki_err_no_namespace': True} + update_template_dictionary(d, request, course) + return render_to_response('simplewiki/simplewiki_error.html', d) + + namespace = None try: - # Ensure that the path exists... - root = Article.get_root() - # Remove root slug if present in path - if url_path and root.slug == url_path[0]: - url_path = url_path[1:] + namespace = Namespace.objects.get(name__exact = article_path_components[0]) + except Namespace.DoesNotExist, ValueError: + d = {'wiki_err_bad_namespace': True} + update_template_dictionary(d, request, course) + return render_to_response('simplewiki/simplewiki_error.html', d) - path = Article.get_url_reverse(url_path[:-1], root) - if not path: - d = {'wiki_err_noparent': True, - 'wiki_url_parent': '/'.join(url_path[:-1]) } - d.update(csrf(request)) - return render_to_response('simplewiki_error.html', d) - - perm_err = check_permissions(request, path[-1], check_locked=False, check_write=True, check_deleted=True) - if perm_err: - return perm_err - # Ensure doesn't already exist - article = Article.get_url_reverse(url_path, root) - if article: - return HttpResponseRedirect(reverse('wiki_view', args=(article[-1].get_url(),))) + # See if the article already exists + article_slug = article_path_components[1] if len(article_path_components) >= 2 else '' + #TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex) - # TODO: Somehow this doesnt work... - #except ShouldHaveExactlyOneRootSlug, (e): - except: - if Article.objects.filter(parent=None).count() > 0: - return HttpResponseRedirect(reverse('wiki_view', args=('/',))) - # Root not found... - path = [] - url_path = [""] + try: + existing_article = Article.objects.get(namespace = namespace, slug__exact = article_slug) + #It already exists, so we just redirect to view the article + return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course)) + except Article.DoesNotExist: + #This is good. The article doesn't exist + pass + + #TODO: Once we have permissions for namespaces, we should check for create permissions + #check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True) if request.method == 'POST': f = CreateArticleForm(request.POST) if f.is_valid(): article = Article() - article.slug = url_path[-1] + article.slug = article_slug if not request.user.is_anonymous(): article.created_by = request.user article.title = f.cleaned_data.get('title') - if path != []: - article.parent = path[-1] + article.namespace = namespace a = article.save() new_revision = f.save(commit=False) if not request.user.is_anonymous(): new_revision.revision_user = request.user new_revision.article = article new_revision.save() - import django.db as db - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + + return HttpResponseRedirect(wiki_reverse("wiki_view", article, course)) else: - f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', url_path[-1]), + f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', article_slug), 'contents':_('Headline\n===\n\n')}) - d = {'wiki_form': f, - 'wiki_write': True, - 'create_article' : True, - } - d.update(csrf(request)) + d = {'wiki_form': f, 'create_article' : True, 'namespace' : namespace.name} + update_template_dictionary(d, request, course) - return render_to_response('simplewiki_edit.html', d) + return render_to_response('simplewiki/simplewiki_edit.html', d) -def edit(request, wiki_url): - (article, path, err) = fetch_from_url(request, wiki_url) +def edit(request, article_path, course_id=None): + course = get_course(course_id) + (article, err) = get_article(request, article_path, course ) if err: return err # Check write permissions - perm_err = check_permissions(request, article, check_write=True, check_locked=True, check_deleted=False) + perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False) if perm_err: return perm_err @@ -169,11 +192,11 @@ def edit(request, wiki_url): if request.POST.__contains__('delete'): if (article.current_revision.deleted == 1): #This article has already been deleted. Redirect - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) new_revision.contents = "" new_revision.deleted = 1 elif not new_revision.get_diff(): - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) if not request.user.is_anonymous(): new_revision.revision_user = request.user @@ -181,34 +204,30 @@ def edit(request, wiki_url): if wiki_settings.WIKI_ALLOW_TITLE_EDIT: new_revision.article.title = f.cleaned_data['title'] new_revision.article.save() - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) else: startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n' f = EditForm({'contents': startContents, 'title': article.title}) - d = {'wiki_form': f, - 'wiki_write': True, - 'wiki_article': article, - 'wiki_title' : article.title, - 'wiki_attachments_write': article.can_attach(request.user), - 'create_article' : False, - } - d.update(csrf(request)) + + d = {'wiki_form': f} + update_template_dictionary(d, request, course, article) + return render_to_response('simplewiki/simplewiki_edit.html', d) - return render_to_response('simplewiki_edit.html', d) - -def history(request, wiki_url, page=1): - (article, path, err) = fetch_from_url(request, wiki_url) +def history(request, article_path, page=1, course_id=None): + course = get_course(course_id) + (article, err) = get_article(request, article_path, course ) if err: return err - perm_err = check_permissions(request, article, check_read=True, check_deleted=False) + perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False) if perm_err: - print "returned error " , perm_err return perm_err page_size = 10 + if page == None: + page = 1 try: p = int(page) except ValueError: @@ -218,11 +237,11 @@ def history(request, wiki_url, page=1): if request.method == 'POST': if request.POST.__contains__('revision'): #They selected a version, but they can be either deleting or changing the version - perm_err = check_permissions(request, article, check_write=True, check_locked=True) + perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) if perm_err: return perm_err - redirectURL = reverse('wiki_view', args=(article.get_url(),)) + redirectURL = wiki_reverse('wiki_view', article, course) try: r = int(request.POST['revision']) revision = Revision.objects.get(id=r) @@ -230,8 +249,8 @@ def history(request, wiki_url, page=1): article.current_revision = revision article.save() elif request.POST.__contains__('view'): - redirectURL = reverse('wiki_view_revision', args=(revision.counter, article.get_url(),)) - + redirectURL = wiki_reverse('wiki_view_revision', course=course, + kwargs={'revision_number' : revision.counter, 'article_path' : article.get_path()}) #The rese of these are admin functions elif request.POST.__contains__('delete') and request.user.is_superuser: if (revision.deleted == 0): @@ -242,11 +261,10 @@ def history(request, wiki_url, page=1): elif request.POST.__contains__('delete_all') and request.user.is_superuser: Revision.objects.filter(article__exact = article, deleted = 0).update(deleted = 2) elif request.POST.__contains__('lock_article'): - print "changing locked article " , article.locked article.locked = not article.locked - print "changed locked article " , article.locked article.save() - except: + except Exception as e: + print str(e) pass finally: return HttpResponseRedirect(redirectURL) @@ -267,23 +285,24 @@ def history(request, wiki_url, page=1): next_page = p + 1 if page_count > p else None prev_page = p - 1 if p > 1 else None + d = {'wiki_page': p, 'wiki_next_page': next_page, 'wiki_prev_page': prev_page, - 'wiki_write': article.can_write_l(request.user), - 'wiki_attachments_write': article.can_attach(request.user), - 'wiki_article': article, - 'wiki_title': article.title, 'wiki_history': history[beginItem:beginItem+page_size], - 'show_delete_revision' : request.user.is_superuser,} - d.update(csrf(request)) - - return render_to_response('simplewiki_history.html', d) + 'show_delete_revision' : request.user.is_superuser} + update_template_dictionary(d, request, course, article) + + return render_to_response('simplewiki/simplewiki_history.html', d) -def revision_feed(request, page=1): +def revision_feed(request, page=1, namespace=None, course_id=None): + course = get_course(course_id) + page_size = 10 + if page == None: + page = 1 try: p = int(page) except ValueError: @@ -303,26 +322,30 @@ def revision_feed(request, page=1): 'wiki_next_page': next_page, 'wiki_prev_page': prev_page, 'wiki_history': history[beginItem:beginItem+page_size], - 'show_delete_revision' : request.user.is_superuser,} - d.update(csrf(request)) + 'show_delete_revision' : request.user.is_superuser, + 'namespace' : namespace} + update_template_dictionary(d, request, course) + + return render_to_response('simplewiki/simplewiki_revision_feed.html', d) - return render_to_response('simplewiki_revision_feed.html', d) - -def search_articles(request): +def search_articles(request, namespace=None, course_id = None): # blampe: We should check for the presence of other popular django search # apps and use those if possible. Only fall back on this as a last resort. # Adding some context to results (eg where matches were) would also be nice. # todo: maybe do some perm checking here - if request.method == 'POST': - querystring = request.POST['value'].strip() + if request.method == 'GET': + querystring = request.GET.get('value', '').strip() else: querystring = "" - - results = Article.objects.all() + course = get_course(course_id) + results = Article.objects.all() + if namespace: + results = results.filter(namespace__name__exact = namespace) + if request.user.is_superuser: results = results.order_by('current_revision__deleted') else: @@ -341,25 +364,26 @@ def search_articles(request): results = results._search(Q(current_revision__contents__icontains = queryword) | \ Q(title__icontains = queryword)) - results = results.select_related('current_revision__deleted') + results = results.select_related('current_revision__deleted','namespace') - results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_url().lower()) ) + results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower()) ) if len(results) == 1 and querystring: - return HttpResponseRedirect(reverse('wiki_view', args=(results[0].get_url(),))) + return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course )) else: d = {'wiki_search_results': results, - 'wiki_search_query': querystring,} - d.update(csrf(request)) - return render_to_response('simplewiki_searchresults.html', d) + 'wiki_search_query': querystring, + 'namespace' : namespace} + update_template_dictionary(d, request, course) + return render_to_response('simplewiki/simplewiki_searchresults.html', d) -def search_add_related(request, wiki_url): - (article, path, err) = fetch_from_url(request, wiki_url) +def search_add_related(request, course_id, slug, namespace): + (article, err) = get_article(request, slug, namespace if namespace else course_id ) if err: return err - perm_err = check_permissions(request, article, check_read=True) + perm_err = check_permissions(request, article, course, check_read=True) if perm_err: return perm_err @@ -384,13 +408,12 @@ def search_add_related(request, wiki_url): json = simplejson.dumps({'results': results}) return HttpResponse(json, mimetype='application/json') -def add_related(request, wiki_url): - - (article, path, err) = fetch_from_url(request, wiki_url) +def add_related(request, course_id, slug, namespace): + (article, err) = get_article(request, slug, namespace if namespace else course_id ) if err: return err - perm_err = check_permissions(request, article, check_write=True, check_locked=True) + perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) if perm_err: return perm_err @@ -406,12 +429,13 @@ def add_related(request, wiki_url): finally: return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) -def remove_related(request, wiki_url, related_id): - (article, path, err) = fetch_from_url(request, wiki_url) +def remove_related(request, course_id, namespace, slug, related_id): + (article, err) = get_article(request, slug, namespace if namespace else course_id ) + if err: return err - perm_err = check_permissions(request, article, check_write=True, check_locked=True) + perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) if perm_err: return perm_err @@ -425,57 +449,33 @@ def remove_related(request, wiki_url, related_id): finally: return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) -def random_article(request): +def random_article(request, course_id): + course = get_course(course_id) from random import randint num_arts = Article.objects.count() article = Article.objects.all()[randint(0, num_arts-1)] - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) - -def encode_err(request, url): - d = {'wiki_err_encode': True} - d.update(csrf(request)) - return render_to_response('simplewiki_error.html', d) + return HttpResponseRedirect( wiki_reverse('wiki_view', article, course)) -def not_found(request, wiki_url): +def not_found(request, article_path, course): """Generate a NOT FOUND message for some URL""" d = {'wiki_err_notfound': True, - 'wiki_url': wiki_url} - d.update(csrf(request)) - return render_to_response('simplewiki_error.html', d) - -def get_url_path(url): - """Return a list of all actual elements of a url, safely ignoring - double-slashes (//) """ - return filter(lambda x: x!='', url.split('/')) - -def fetch_from_url(request, url): - """Analyze URL, returning the article and the articles in its path - If something goes wrong, return an error HTTP response""" - + 'article_path': article_path, + 'namespace' : course.wiki_namespace} + update_template_dictionary(d, request, course) + return render_to_response('simplewiki/simplewiki_error.html', d) + +def get_article(request, article_path, course): err = None article = None - path = None - url_path = get_url_path(url) - try: - root = Article.get_root() - except: - err = not_found(request, '/') - return (article, path, err) + article = Article.get_article(article_path) + except Article.DoesNotExist, ValueError: + err = not_found(request, article_path, course) + + return (article, err) - if url_path and root.slug == url_path[0]: - url_path = url_path[1:] - - path = Article.get_url_reverse(url_path, root) - if not path: - err = not_found(request, '/' + '/'.join(url_path)) - else: - article = path[-1] - return (article, path, err) - - -def check_permissions(request, article, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision = None): +def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision = None): read_err = check_read and not article.can_read(request.user) write_err = check_write and not article.can_write(request.user) @@ -495,12 +495,12 @@ def check_permissions(request, article, check_read=False, check_write=False, che 'wiki_err_nowrite': write_err, 'wiki_err_locked': locked_err, 'wiki_err_deleted': deleted_err,} - d.update(csrf(request)) + update_template_dictionary(d, request, course) # TODO: Make this a little less jarring by just displaying an error # on the current page? (no such redirect happens for an anon upload yet) # benjaoming: I think this is the nicest way of displaying an error, but # these errors shouldn't occur, but rather be prevented on the other pages. - return render_to_response('simplewiki_error.html', d) + return render_to_response('simplewiki/simplewiki_error.html', d) else: return None @@ -508,6 +508,7 @@ def check_permissions(request, article, check_read=False, check_write=False, che # LOGIN PROTECTION # #################### + if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW: view = login_required(view) history = login_required(history) diff --git a/lms/static/js/simplewiki-AutoSuggest_c_2.0.js b/lms/static/js/simplewiki-AutoSuggest_c_2.0.js new file mode 100644 index 0000000000..b202f3abb2 --- /dev/null +++ b/lms/static/js/simplewiki-AutoSuggest_c_2.0.js @@ -0,0 +1,961 @@ +/** + * author: Timothy Groves - http://www.brandspankingnew.net + * version: 1.2 - 2006-11-17 + * 1.3 - 2006-12-04 + * 2.0 - 2007-02-07 + * + */ + +var useBSNns; + +if (useBSNns) +{ + if (typeof(bsn) == "undefined") + bsn = {} + _bsn = bsn; +} +else +{ + _bsn = this; +} + + + +if (typeof(_bsn.Autosuggest) == "undefined") + _bsn.Autosuggest = {} + + + + + + + + + + + + + +_bsn.AutoSuggest = function (fldID, param) +{ + // no DOM - give up! + // + if (!document.getElementById) + return false; + + + + + // get field via DOM + // + this.fld = _bsn.DOM.getElement(fldID); + + if (!this.fld) + return false; + + + + + // init variables + // + this.sInput = ""; + this.nInputChars = 0; + this.aSuggestions = []; + this.iHighlighted = 0; + + + + + // parameters object + // + this.oP = (param) ? param : {}; + + // defaults + // + if (!this.oP.minchars) this.oP.minchars = 1; + if (!this.oP.method) this.oP.meth = "get"; + if (!this.oP.varname) this.oP.varname = "input"; + if (!this.oP.className) this.oP.className = "autosuggest"; + if (!this.oP.timeout) this.oP.timeout = 2500; + if (!this.oP.delay) this.oP.delay = 500; + if (!this.oP.offsety) this.oP.offsety = -5; + if (!this.oP.shownoresults) this.oP.shownoresults = true; + if (!this.oP.noresults) this.oP.noresults = "No results!"; + if (!this.oP.maxheight && this.oP.maxheight !== 0) this.oP.maxheight = 250; + if (!this.oP.cache && this.oP.cache != false) this.oP.cache = true; + + + + + + // set keyup handler for field + // and prevent autocomplete from client + // + var pointer = this; + + // NOTE: not using addEventListener because UpArrow fired twice in Safari + //_bsn.DOM.addEvent( this.fld, 'keyup', function(ev){ return pointer.onKeyPress(ev); } ); + + this.fld.onkeypress = function(ev){ return pointer.onKeyPress(ev); } + this.fld.onkeyup = function(ev){ return pointer.onKeyUp(ev); } + + this.fld.setAttribute("autocomplete","off"); +} + + + + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.onKeyPress = function(ev) +{ + + var key = (window.event) ? window.event.keyCode : ev.keyCode; + + + + // set responses to keydown events in the field + // this allows the user to use the arrow keys to scroll through the results + // ESCAPE clears the list + // TAB sets the current highlighted value + // + var RETURN = 13; + var TAB = 9; + var ESC = 27; + + var bubble = true; + + switch(key) + { + + case RETURN: + this.setHighlightedValue(); + bubble = false; + break; + + + case ESC: + this.clearSuggestions(); + break; + } + + return bubble; +} + + + +_bsn.AutoSuggest.prototype.onKeyUp = function(ev) +{ + var key = (window.event) ? window.event.keyCode : ev.keyCode; + + + + // set responses to keydown events in the field + // this allows the user to use the arrow keys to scroll through the results + // ESCAPE clears the list + // TAB sets the current highlighted value + // + + var ARRUP = 38; + var ARRDN = 40; + + var bubble = true; + + switch(key) + { + + + case ARRUP: + this.changeHighlight(key); + bubble = false; + break; + + + case ARRDN: + this.changeHighlight(key); + bubble = false; + break; + + + default: + this.getSuggestions(this.fld.value); + } + + return bubble; + + +} + + + + + + + + +_bsn.AutoSuggest.prototype.getSuggestions = function (val) +{ + + // if input stays the same, do nothing + // + if (val == this.sInput) + return false; + + + // input length is less than the min required to trigger a request + // reset input string + // do nothing + // + if (val.length < this.oP.minchars) + { + this.sInput = ""; + return false; + } + + + // if caching enabled, and user is typing (ie. length of input is increasing) + // filter results out of aSuggestions from last request + // + if (val.length>this.nInputChars && this.aSuggestions.length && this.oP.cache) + { + var arr = []; + for (var i=0;i" + val.substring(st, st+this.sInput.length) + "" + val.substring(st+this.sInput.length); + + + var span = _bsn.DOM.createElement("span", {}, output, true); + if (arr[i].info != "") + { + var br = _bsn.DOM.createElement("br", {}); + span.appendChild(br); + var small = _bsn.DOM.createElement("small", {}, arr[i].info); + span.appendChild(small); + } + + var a = _bsn.DOM.createElement("a", { href:"#" }); + + var tl = _bsn.DOM.createElement("span", {className:"tl"}, " "); + var tr = _bsn.DOM.createElement("span", {className:"tr"}, " "); + a.appendChild(tl); + a.appendChild(tr); + + a.appendChild(span); + + a.name = i+1; + a.onclick = function () { pointer.setHighlightedValue(); return false; } + a.onmouseover = function () { pointer.setHighlight(this.name); } + + var li = _bsn.DOM.createElement( "li", {}, a ); + + ul.appendChild( li ); + } + + + // no results + // + if (arr.length == 0) + { + var li = _bsn.DOM.createElement( "li", {className:"as_warning"}, this.oP.noresults ); + + ul.appendChild( li ); + } + + + div.appendChild( ul ); + + + var fcorner = _bsn.DOM.createElement("div", {className:"as_corner"}); + var fbar = _bsn.DOM.createElement("div", {className:"as_bar"}); + var footer = _bsn.DOM.createElement("div", {className:"as_footer"}); + footer.appendChild(fcorner); + footer.appendChild(fbar); + div.appendChild(footer); + + + + // get position of target textfield + // position holding div below it + // set width of holding div to width of field + // + var pos = _bsn.DOM.getPos(this.fld); + + div.style.left = pos.x + "px"; + div.style.top = ( pos.y + this.fld.offsetHeight + this.oP.offsety ) + "px"; + div.style.width = this.fld.offsetWidth + "px"; + + + + // set mouseover functions for div + // when mouse pointer leaves div, set a timeout to remove the list after an interval + // when mouse enters div, kill the timeout so the list won't be removed + // + div.onmouseover = function(){ pointer.killTimeout() } + div.onmouseout = function(){ pointer.resetTimeout() } + + + // add DIV to document + // + document.getElementsByTagName("body")[0].appendChild(div); + + + + // currently no item is highlighted + // + this.iHighlighted = 0; + + + + + + + // remove list after an interval + // + var pointer = this; + this.toID = setTimeout(function () { pointer.clearSuggestions() }, this.oP.timeout); +} + + + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.changeHighlight = function(key) +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + var n; + + if (key == 40) + n = this.iHighlighted + 1; + else if (key == 38) + n = this.iHighlighted - 1; + + + if (n > list.childNodes.length) + n = list.childNodes.length; + if (n < 1) + n = 1; + + + this.setHighlight(n); +} + + + +_bsn.AutoSuggest.prototype.setHighlight = function(n) +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + if (this.iHighlighted > 0) + this.clearHighlight(); + + this.iHighlighted = Number(n); + + list.childNodes[this.iHighlighted-1].className = "as_highlight"; + + + this.killTimeout(); +} + + +_bsn.AutoSuggest.prototype.clearHighlight = function() +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + if (this.iHighlighted > 0) + { + list.childNodes[this.iHighlighted-1].className = ""; + this.iHighlighted = 0; + } +} + + +_bsn.AutoSuggest.prototype.setHighlightedValue = function () +{ + if (this.iHighlighted) + { + this.sInput = this.fld.value = this.aSuggestions[ this.iHighlighted-1 ].value; + + // move cursor to end of input (safari) + // + this.fld.focus(); + if (this.fld.selectionStart) + this.fld.setSelectionRange(this.sInput.length, this.sInput.length); + + + this.clearSuggestions(); + + // pass selected object to callback function, if exists + // + if (typeof(this.oP.callback) == "function") + this.oP.callback( this.aSuggestions[this.iHighlighted-1] ); + } +} + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.killTimeout = function() +{ + clearTimeout(this.toID); +} + +_bsn.AutoSuggest.prototype.resetTimeout = function() +{ + clearTimeout(this.toID); + var pointer = this; + this.toID = setTimeout(function () { pointer.clearSuggestions() }, 1000); +} + + + + + + + +_bsn.AutoSuggest.prototype.clearSuggestions = function () +{ + + this.killTimeout(); + + var ele = _bsn.DOM.getElement(this.idAs); + var pointer = this; + if (ele) + { + var fade = new _bsn.Fader(ele,1,0,250,function () { _bsn.DOM.removeElement(pointer.idAs) }); + } +} + + + + + + + + + + +// AJAX PROTOTYPE _____________________________________________ + + +if (typeof(_bsn.Ajax) == "undefined") + _bsn.Ajax = {} + + + +_bsn.Ajax = function () +{ + this.req = {}; + this.isIE = false; +} + + + +_bsn.Ajax.prototype.makeRequest = function (url, meth, onComp, onErr) +{ + + if (meth != "POST") + meth = "GET"; + + this.onComplete = onComp; + this.onError = onErr; + + var pointer = this; + + // branch for native XMLHttpRequest object + if (window.XMLHttpRequest) + { + this.req = new XMLHttpRequest(); + this.req.onreadystatechange = function () { pointer.processReqChange() }; + this.req.open("GET", url, true); // + this.req.send(null); + // branch for IE/Windows ActiveX version + } + else if (window.ActiveXObject) + { + this.req = new ActiveXObject("Microsoft.XMLHTTP"); + if (this.req) + { + this.req.onreadystatechange = function () { pointer.processReqChange() }; + this.req.open(meth, url, true); + this.req.send(); + } + } +} + + +_bsn.Ajax.prototype.processReqChange = function() +{ + + // only if req shows "loaded" + if (this.req.readyState == 4) { + // only if "OK" + if (this.req.status == 200) + { + this.onComplete( this.req ); + } else { + this.onError( this.req.status ); + } + } +} + + + + + + + + + + +// DOM PROTOTYPE _____________________________________________ + + +if (typeof(_bsn.DOM) == "undefined") + _bsn.DOM = {} + + + + +_bsn.DOM.createElement = function ( type, attr, cont, html ) +{ + var ne = document.createElement( type ); + if (!ne) + return false; + + for (var a in attr) + ne[a] = attr[a]; + + if (typeof(cont) == "string" && !html) + ne.appendChild( document.createTextNode(cont) ); + else if (typeof(cont) == "string" && html) + ne.innerHTML = cont; + else if (typeof(cont) == "object") + ne.appendChild( cont ); + + return ne; +} + + + + + +_bsn.DOM.clearElement = function ( id ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + while (ele.childNodes.length) + ele.removeChild( ele.childNodes[0] ); + + return true; +} + + + + + + + + + +_bsn.DOM.removeElement = function ( ele ) +{ + var e = this.getElement(ele); + + if (!e) + return false; + else if (e.parentNode.removeChild(e)) + return true; + else + return false; +} + + + + + +_bsn.DOM.replaceContent = function ( id, cont, html ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + this.clearElement( ele ); + + if (typeof(cont) == "string" && !html) + ele.appendChild( document.createTextNode(cont) ); + else if (typeof(cont) == "string" && html) + ele.innerHTML = cont; + else if (typeof(cont) == "object") + ele.appendChild( cont ); +} + + + + + + + + + +_bsn.DOM.getElement = function ( ele ) +{ + if (typeof(ele) == "undefined") + { + return false; + } + else if (typeof(ele) == "string") + { + var re = document.getElementById( ele ); + if (!re) + return false; + else if (typeof(re.appendChild) != "undefined" ) { + return re; + } else { + return false; + } + } + else if (typeof(ele.appendChild) != "undefined") + return ele; + else + return false; +} + + + + + + + +_bsn.DOM.appendChildren = function ( id, arr ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + + if (typeof(arr) != "object") + return false; + + for (var i=0;i' + // Wraps and hides input textarea + '' + + '
' + + '
' + // Set to the height of the text, causes scrolling + '
' + // Moved around its parent to cover visible view + '
' + + // Provides positioning relative to (visible) text origin + '
' + + '
' + + '
 
' + // Absolutely positioned blinky cursor + '
' + // DIVs containing the selection and the actual code + '
'; + if (place.appendChild) place.appendChild(wrapper); else place(wrapper); + // I've never seen more elegant code in my life. + var inputDiv = wrapper.firstChild, input = inputDiv.firstChild, + scroller = wrapper.lastChild, code = scroller.firstChild, + mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild, + lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild, + cursor = measure.nextSibling, selectionDiv = cursor.nextSibling, + lineDiv = selectionDiv.nextSibling; + themeChanged(); + // Needed to hide big blue blinking cursor on Mobile Safari + if (ios) input.style.width = "0px"; + if (!webkit) lineSpace.draggable = true; + lineSpace.style.outline = "none"; + if (options.tabindex != null) input.tabIndex = options.tabindex; + if (options.autofocus) focusInput(); + if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + // Needed to handle Tab key in KHTML + if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute"; + + // Check for problem with IE innerHTML not working when we have a + // P (or similar) parent node. + try { stringWidth("x"); } + catch (e) { + if (e.message.match(/runtime/i)) + e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)"); + throw e; + } + + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. + var poll = new Delayed(), highlight = new Delayed(), blinker; + + // mode holds a mode API object. doc is the tree of Line objects, + // work an array of lines that should be parsed, and history the + // undo history (instance of History constructor). + var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused; + loadMode(); + // The selection. These are always maintained to point at valid + // positions. Inverted is used to remember that the user is + // selecting bottom-to-top. + var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false}; + // Selection-related flags. shiftSelecting obviously tracks + // whether the user is holding shift. + var shiftSelecting, lastClick, lastDoubleClick, lastScrollPos = 0, draggingText, + overwrite = false, suppressEdits = false; + // Variables used by startOperation/endOperation to track what + // happened during the operation. + var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone, + gutterDirty, callbacks; + // Current visible range (may be bigger than the view window). + var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0; + // bracketHighlighted is used to remember that a bracket has been + // marked. + var bracketHighlighted; + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + var maxLine = "", maxWidth; + var tabCache = {}; + + // Initialize the content. + operation(function(){setValue(options.value || ""); updateInput = false;})(); + var history = new History(); + + // Register our event handlers. + connect(scroller, "mousedown", operation(onMouseDown)); + connect(scroller, "dblclick", operation(onDoubleClick)); + connect(lineSpace, "dragstart", onDragStart); + connect(lineSpace, "selectstart", e_preventDefault); + // Gecko browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for Gecko. + if (!gecko) connect(scroller, "contextmenu", onContextMenu); + connect(scroller, "scroll", function() { + lastScrollPos = scroller.scrollTop; + updateDisplay([]); + if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px"; + if (options.onScroll) options.onScroll(instance); + }); + connect(window, "resize", function() {updateDisplay(true);}); + connect(input, "keyup", operation(onKeyUp)); + connect(input, "input", fastPoll); + connect(input, "keydown", operation(onKeyDown)); + connect(input, "keypress", operation(onKeyPress)); + connect(input, "focus", onFocus); + connect(input, "blur", onBlur); + + connect(scroller, "dragenter", e_stop); + connect(scroller, "dragover", e_stop); + connect(scroller, "drop", operation(onDrop)); + connect(scroller, "paste", function(){focusInput(); fastPoll();}); + connect(input, "paste", fastPoll); + connect(input, "cut", operation(function(){ + if (!options.readOnly) replaceSelection(""); + })); + + // Needed to handle Tab key in KHTML + if (khtml) connect(code, "mouseup", function() { + if (document.activeElement == input) input.blur(); + focusInput(); + }); + + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (document.activeElement == input); } catch(e) { } + if (hasFocus || options.autofocus) setTimeout(onFocus, 20); + else onBlur(); + + function isLine(l) {return l >= 0 && l < doc.size;} + // The instance object that we'll return. Mostly calls out to + // local functions in the CodeMirror function. Some do some extra + // range checking and/or clipping. operation is used to wrap the + // call so that changes it makes are tracked, and the display is + // updated afterwards. + var instance = wrapper.CodeMirror = { + getValue: getValue, + setValue: operation(setValue), + getSelection: getSelection, + replaceSelection: operation(replaceSelection), + focus: function(){window.focus(); focusInput(); onFocus(); fastPoll();}, + setOption: function(option, value) { + var oldVal = options[option]; + options[option] = value; + if (option == "mode" || option == "indentUnit") loadMode(); + else if (option == "readOnly" && value == "nocursor") {onBlur(); input.blur();} + else if (option == "readOnly" && !value) {resetInput(true);} + else if (option == "theme") themeChanged(); + else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)(); + else if (option == "tabSize") updateDisplay(true); + if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") { + gutterChanged(); + updateDisplay(true); + } + }, + getOption: function(option) {return options[option];}, + undo: operation(undo), + redo: operation(redo), + indentLine: operation(function(n, dir) { + if (typeof dir != "string") { + if (dir == null) dir = options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(n)) indentLine(n, dir); + }), + indentSelection: operation(indentSelected), + historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, + clearHistory: function() {history = new History();}, + matchBrackets: operation(function(){matchBrackets(true);}), + getTokenAt: operation(function(pos) { + pos = clipPos(pos); + return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch); + }), + getStateAfter: function(line) { + line = clipLine(line == null ? doc.size - 1: line); + return getStateBefore(line + 1); + }, + cursorCoords: function(start, mode) { + if (start == null) start = sel.inverted; + return this.charCoords(start ? sel.from : sel.to, mode); + }, + charCoords: function(pos, mode) { + pos = clipPos(pos); + if (mode == "local") return localCoords(pos, false); + if (mode == "div") return localCoords(pos, true); + return pageCoords(pos); + }, + coordsChar: function(coords) { + var off = eltOffset(lineSpace); + return coordsChar(coords.x - off.left, coords.y - off.top); + }, + markText: operation(markText), + setBookmark: setBookmark, + findMarksAt: findMarksAt, + setMarker: operation(addGutterMarker), + clearMarker: operation(removeGutterMarker), + setLineClass: operation(setLineClass), + hideLine: operation(function(h) {return setLineHidden(h, true);}), + showLine: operation(function(h) {return setLineHidden(h, false);}), + onDeleteLine: function(line, f) { + if (typeof line == "number") { + if (!isLine(line)) return null; + line = getLine(line); + } + (line.handlers || (line.handlers = [])).push(f); + return line; + }, + lineInfo: lineInfo, + addWidget: function(pos, node, scroll, vert, horiz) { + pos = localCoords(clipPos(pos)); + var top = pos.yBot, left = pos.x; + node.style.position = "absolute"; + code.appendChild(node); + if (vert == "over") top = pos.y; + else if (vert == "near") { + var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), + hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft(); + if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) + top = pos.y - node.offsetHeight; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = (top + paddingTop()) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = code.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2; + node.style.left = (left + paddingLeft()) + "px"; + } + if (scroll) + scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + lineCount: function() {return doc.size;}, + clipPos: clipPos, + getCursor: function(start) { + if (start == null) start = sel.inverted; + return copyPos(start ? sel.from : sel.to); + }, + somethingSelected: function() {return !posEq(sel.from, sel.to);}, + setCursor: operation(function(line, ch, user) { + if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user); + else setCursor(line, ch, user); + }), + setSelection: operation(function(from, to, user) { + (user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from)); + }), + getLine: function(line) {if (isLine(line)) return getLine(line).text;}, + getLineHandle: function(line) {if (isLine(line)) return getLine(line);}, + setLine: operation(function(line, text) { + if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); + }), + removeLine: operation(function(line) { + if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0})); + }), + replaceRange: operation(replaceRange), + getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));}, + + triggerOnKeyDown: operation(onKeyDown), + execCommand: function(cmd) {return commands[cmd](instance);}, + // Stuff used by commands, probably not much use to outside code. + moveH: operation(moveH), + deleteH: operation(deleteH), + moveV: operation(moveV), + toggleOverwrite: function() { + if(overwrite){ + overwrite = false; + cursor.className = cursor.className.replace(" CodeMirror-overwrite", ""); + } else { + overwrite = true; + cursor.className += " CodeMirror-overwrite"; + } + }, + + posFromIndex: function(off) { + var lineNo = 0, ch; + doc.iter(0, doc.size, function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos({line: lineNo, ch: ch}); + }, + indexFromPos: function (coords) { + if (coords.line < 0 || coords.ch < 0) return 0; + var index = coords.ch; + doc.iter(0, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + scrollTo: function(x, y) { + if (x != null) scroller.scrollLeft = x; + if (y != null) scroller.scrollTop = y; + updateDisplay([]); + }, + + operation: function(f){return operation(f)();}, + refresh: function(){ + updateDisplay(true); + if (scroller.scrollHeight > lastScrollPos) + scroller.scrollTop = lastScrollPos; + }, + getInputField: function(){return input;}, + getWrapperElement: function(){return wrapper;}, + getScrollerElement: function(){return scroller;}, + getGutterElement: function(){return gutter;} + }; + + function getLine(n) { return getLineAt(doc, n); } + function updateLineHeight(line, height) { + gutterDirty = true; + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + + function setValue(code) { + var top = {line: 0, ch: 0}; + updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length}, + splitLines(code), top, top); + updateInput = true; + } + function getValue(code) { + var text = []; + doc.iter(0, doc.size, function(line) { text.push(line.text); }); + return text.join("\n"); + } + + function onMouseDown(e) { + setShift(e_prop(e, "shiftKey")); + // Check whether this is a click in a widget + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == code && n != mover) return; + + // See if this is a click in the gutter + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) { + if (options.onGutterClick) + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); + return e_preventDefault(e); + } + + var start = posFromMouse(e); + + switch (e_button(e)) { + case 3: + if (gecko && !mac) onContextMenu(e); + return; + case 2: + if (start) setCursor(start.line, start.ch, true); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} + + if (!focused) onFocus(); + + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + e_preventDefault(e); + setTimeout(focusInput, 20); + return selectLine(start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + return selectWordAt(start); + } else { lastClick = {time: now, pos: start}; } + + var last = start, going; + if (dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start)) { + // Let the drag handler handle this. + if (webkit) lineSpace.draggable = true; + var up = connect(document, "mouseup", operation(function(e2) { + if (webkit) lineSpace.draggable = false; + draggingText = false; + up(); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + setCursor(start.line, start.ch, true); + focusInput(); + } + }), true); + draggingText = true; + // IE's approach to draggable + if (lineSpace.dragDrop) lineSpace.dragDrop(); + return; + } + e_preventDefault(e); + setCursor(start.line, start.ch, true); + + function extend(e) { + var cur = posFromMouse(e, true); + if (cur && !posEq(cur, last)) { + if (!focused) onFocus(); + last = cur; + setSelectionUser(start, cur); + updateInput = false; + var visible = visibleLines(); + if (cur.line >= visible.to || cur.line < visible.from) + going = setTimeout(operation(function(){extend(e);}), 150); + } + } + + function done(e) { + clearTimeout(going); + var cur = posFromMouse(e); + if (cur) setSelectionUser(start, cur); + e_preventDefault(e); + focusInput(); + updateInput = true; + move(); up(); + } + var move = connect(document, "mousemove", operation(function(e) { + clearTimeout(going); + e_preventDefault(e); + if (!ie && !e_button(e)) done(e); + else extend(e); + }), true); + var up = connect(document, "mouseup", operation(done), true); + } + function onDoubleClick(e) { + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) return e_preventDefault(e); + var start = posFromMouse(e); + if (!start) return; + lastDoubleClick = {time: +new Date, pos: start}; + e_preventDefault(e); + selectWordAt(start); + } + function onDrop(e) { + e.preventDefault(); + var pos = posFromMouse(e, true), files = e.dataTransfer.files; + if (!pos || options.readOnly) return; + if (files && files.length && window.FileReader && window.File) { + function loadFile(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(pos); + operation(function() { + var end = replaceRange(text.join(""), pos, pos); + setSelectionUser(pos, end); + })(); + } + }; + reader.readAsText(file); + } + var n = files.length, text = Array(n), read = 0; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } + else { + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var curFrom = sel.from, curTo = sel.to; + setSelectionUser(pos, pos); + if (draggingText) replaceRange("", curFrom, curTo); + replaceSelection(text); + focusInput(); + } + } + catch(e){} + } + } + function onDragStart(e) { + var txt = getSelection(); + e.dataTransfer.setData("Text", txt); + + // Use dummy image instead of default browsers image. + if (gecko || chrome) { + var img = document.createElement('img'); + img.scr = 'data:image/gif;base64,R0lGODdhAgACAIAAAAAAAP///ywAAAAAAgACAAACAoRRADs='; //1x1 image + e.dataTransfer.setDragImage(img, 0, 0); + } + } + + function doHandleBinding(bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + var prevShift = shiftSelecting; + try { + if (options.readOnly) suppressEdits = true; + if (dropShift) shiftSelecting = null; + bound(instance); + } catch(e) { + if (e != Pass) throw e; + return false; + } finally { + shiftSelecting = prevShift; + suppressEdits = false; + } + return true; + } + function handleKeyBinding(e) { + // Handle auto keymap transitions + var startMap = getKeyMap(options.keyMap), next = startMap.auto; + clearTimeout(maybeTransition); + if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { + if (getKeyMap(options.keyMap) == startMap) { + options.keyMap = (next.call ? next.call(null, instance) : next); + } + }, 50); + + var name = keyNames[e_prop(e, "keyCode")], handled = false; + if (name == null || e.altGraphKey) return false; + if (e_prop(e, "altKey")) name = "Alt-" + name; + if (e_prop(e, "ctrlKey")) name = "Ctrl-" + name; + if (e_prop(e, "metaKey")) name = "Cmd-" + name; + + if (e_prop(e, "shiftKey")) { + handled = lookupKey("Shift-" + name, options.extraKeys, options.keyMap, + function(b) {return doHandleBinding(b, true);}) + || lookupKey(name, options.extraKeys, options.keyMap, function(b) { + if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(b); + }); + } else { + handled = lookupKey(name, options.extraKeys, options.keyMap, doHandleBinding); + } + if (handled) { + e_preventDefault(e); + if (ie) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } + } + return handled; + } + function handleCharBinding(e, ch) { + var handled = lookupKey("'" + ch + "'", options.extraKeys, + options.keyMap, doHandleBinding); + if (handled) e_preventDefault(e); + return handled; + } + + var lastStoppedKey = null, maybeTransition; + function onKeyDown(e) { + if (!focused) onFocus(); + if (ie && e.keyCode == 27) { e.returnValue = false; } + if (pollingFast) { if (readInput()) pollingFast = false; } + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + var code = e_prop(e, "keyCode"); + // IE does strange things with escape. + setShift(code == 16 || e_prop(e, "shiftKey")); + // First give onKeyEvent option a chance to handle this. + var handled = handleKeyBinding(e); + if (window.opera) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && e_prop(e, mac ? "metaKey" : "ctrlKey")) + replaceSelection(""); + } + } + function onKeyPress(e) { + if (pollingFast) readInput(); + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + var keyCode = e_prop(e, "keyCode"), charCode = e_prop(e, "charCode"); + if (window.opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((window.opera && !e.which) || khtml) && handleKeyBinding(e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (options.electricChars && mode.electricChars && options.smartIndent && !options.readOnly) { + if (mode.electricChars.indexOf(ch) > -1) + setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75); + } + if (handleCharBinding(e, ch)) return; + fastPoll(); + } + function onKeyUp(e) { + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + if (e_prop(e, "keyCode") == 16) shiftSelecting = null; + } + + function onFocus() { + if (options.readOnly == "nocursor") return; + if (!focused) { + if (options.onFocus) options.onFocus(instance); + focused = true; + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + wrapper.className += " CodeMirror-focused"; + if (!leaveInputAlone) resetInput(true); + } + slowPoll(); + restartBlink(); + } + function onBlur() { + if (focused) { + if (options.onBlur) options.onBlur(instance); + focused = false; + if (bracketHighlighted) + operation(function(){ + if (bracketHighlighted) { bracketHighlighted(); bracketHighlighted = null; } + })(); + wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(blinker); + setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); + } + + // Replace the range from from to to by the strings in newText. + // Afterwards, set the selection to selFrom, selTo. + function updateLines(from, to, newText, selFrom, selTo) { + if (suppressEdits) return; + + //This block ensures that widget lines don't have any text inserted on the same line. + if (from.ch > 0 && (newText[0] != '' || newText.length == 1) && getLine(from.line).widgetFunction) { + newText.unshift(''); + var widgetLine = getLine(from.line); + function moveSel(sel) { + if (sel.line == from.line && sel.ch > 0){ + return {line: sel.line + 1, ch: sel.ch - widgetLine.text.length}; + } else if (sel.line > from.line) { + return {line: sel.line + 1, ch: sel.ch}; + } + } + selFrom = moveSel(selFrom, widgetLine); + selTo = moveSel(selTo, widgetLine); + } + if (to.ch == 0 && (newText[newText.length - 1] != '' || newText.length == 1) && getLine(to.line).widgetFunction) { + newText.push(''); + } + + if (history) { + var old = []; + doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); }); + history.addChange(from.line, newText.length, old); + while (history.done.length > options.undoDepth) history.done.shift(); + } + updateLinesNoUndo(from, to, newText, selFrom, selTo); + } + function unredoHelper(from, to) { + if (!from.length) return; + var set = from.pop(), out = []; + for (var i = set.length - 1; i >= 0; i -= 1) { + var change = set[i]; + var replaced = [], end = change.start + change.added; + doc.iter(change.start, end, function(line) { replaced.push(line.text); }); + out.push({start: change.start, added: change.old.length, old: replaced}); + var pos = clipPos({line: change.start + change.old.length - 1, + ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])}); + updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos); + } + updateInput = true; + to.push(out); + } + function undo() {unredoHelper(history.done, history.undone);} + function redo() {unredoHelper(history.undone, history.done);} + + function updateLinesNoUndo(from, to, newText, selFrom, selTo) { + if (suppressEdits) return; + var recomputeMaxLength = false, maxLineLength = maxLine.length; + if (!options.lineWrapping) + doc.iter(from.line, to.line, function(line) { + if (line.text.length == maxLineLength) {recomputeMaxLength = true; return true;} + }); + if (from.line != to.line || newText.length > 1) gutterDirty = true; + + var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); + // First adjust the line structure, taking some care to leave highlighting intact. + if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = [], prevLine = null; + if (from.line) { + prevLine = getLine(from.line - 1); + prevLine.fixMarkEnds(lastLine); + } else lastLine.fixMarkStarts(); + for (var i = 0, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], prevLine)); + if (nlines) doc.remove(from.line, nlines, callbacks); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (newText.length == 1) + firstLine.replace(from.ch, to.ch, newText[0]); + else { + lastLine = firstLine.split(to.ch, newText[newText.length-1]); + firstLine.replace(from.ch, null, newText[0]); + firstLine.fixMarkEnds(lastLine); + var added = []; + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + added.push(lastLine); + doc.insert(from.line + 1, added); + } + } else if (newText.length == 1) { + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, ""); + firstLine.append(lastLine); + doc.remove(from.line + 1, nlines, callbacks); + } else { + var added = []; + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, newText[newText.length-1]); + firstLine.fixMarkEnds(lastLine); + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks); + doc.insert(from.line + 1, added); + } + + // Add these lines to the work array, so that they will be + // highlighted. Adjust work lines if lines were added/removed. + var newWork = [], lendiff = newText.length - nlines - 1; + for (var j = 0, l = work.length; j < l; ++j) { + var task = work[j]; + if (task < from.line) newWork.push(task); + else if (task > to.line) newWork.push(task + lendiff); + } + var hlEnd = from.line + Math.min(newText.length, 500); + highlightLines(from.line, hlEnd); + newWork.push(hlEnd); + work = newWork; + startWorker(100); + // Remember that these lines changed, for updating the display + changes.push({from: from.line, to: to.line + 1, diff: lendiff}); + var changeObj = {from: from, to: to, text: newText}; + if (textChanged) { + for (var cur = textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else textChanged = changeObj; + + + if (options.lineWrapping) { + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(from.line, from.line + newText.length, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (line.widgetFunction) + guess = line.widgetFunction.size(line.text).height / textHeight(); + if (guess != line.height) updateLineHeight(line, guess); + }); + } else { + doc.iter(from.line, from.line + newText.length, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLine = l; maxLineLength = l.length; maxWidth = null; + recomputeMaxLength = false; + } + if (line.widgetFunction) { + var guess = line.widgetFunction.size(line.text).height / textHeight(); + if (guess != line.height) updateLineHeight(line, guess); + } else if (line.height != 1) { + updateLineHeight(line, 1); + } + }); + if (recomputeMaxLength) { + maxLineLength = 0; maxLine = ""; maxWidth = null; + doc.iter(0, doc.size, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLineLength = l.length; maxLine = l; + } + }); + } + } + + // Update the selection + function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} + setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line)); + + // Make sure the scroll-size div has the correct height. + if (scroller.clientHeight) + code.style.height = (doc.height * textHeight() + 2 * paddingTop()) + "px"; + } + + function replaceRange(code, from, to) { + from = clipPos(from); + if (!to) to = from; else to = clipPos(to); + code = splitLines(code); + function adjustPos(pos) { + if (posLess(pos, from)) return pos; + if (!posLess(to, pos)) return end; + var line = pos.line + code.length - (to.line - from.line) - 1; + var ch = pos.ch; + if (pos.line == to.line) + ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0)); + return {line: line, ch: ch}; + } + var end; + replaceRange1(code, from, to, function(end1) { + end = end1; + return {from: adjustPos(sel.from), to: adjustPos(sel.to)}; + }); + return end; + } + function replaceSelection(code, collapse) { + replaceRange1(splitLines(code), sel.from, sel.to, function(end) { + if (collapse == "end") return {from: end, to: end}; + else if (collapse == "start") return {from: sel.from, to: sel.from}; + else return {from: sel.from, to: end}; + }); + } + function replaceRange1(code, from, to, computeSel) { + var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length; + var newSel = computeSel({line: from.line + code.length - 1, ch: endch}); + updateLines(from, to, code, newSel.from, newSel.to); + } + + function getRange(from, to) { + var l1 = from.line, l2 = to.line; + if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch); + var code = [getLine(l1).text.slice(from.ch)]; + doc.iter(l1 + 1, l2, function(line) { code.push(line.text); }); + code.push(getLine(l2).text.slice(0, to.ch)); + return code.join("\n"); + } + function getSelection() { + return getRange(sel.from, sel.to); + } + + var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll + function slowPoll() { + if (pollingFast) return; + poll.set(options.pollInterval, function() { + startOperation(); + readInput(); + if (focused) slowPoll(); + endOperation(); + }); + } + function fastPoll() { + var missed = false; + pollingFast = true; + function p() { + startOperation(); + var changed = readInput(); + if (!changed && !missed) {missed = true; poll.set(60, p);} + else {pollingFast = false; slowPoll();} + endOperation(); + } + poll.set(20, p); + } + + // Previnput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + var prevInput = ""; + function readInput() { + if (leaveInputAlone || !focused || hasSelection(input) || options.readOnly) return false; + var text = input.value; + if (text == prevInput) return false; + shiftSelecting = null; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput[same] == text[same]) ++same; + if (same < prevInput.length) + sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)}; + else if (overwrite && posEq(sel.from, sel.to)) + sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))}; + replaceSelection(text.slice(same), "end"); + prevInput = text; + return true; + } + function resetInput(user) { + if (!posEq(sel.from, sel.to)) { + prevInput = ""; + input.value = getSelection(); + selectInput(input); + } else if (user) prevInput = input.value = ""; + } + + function focusInput() { + if (options.readOnly != "nocursor") input.focus(); + } + + function scrollEditorIntoView() { + if (!cursor.getBoundingClientRect) return; + var rect = cursor.getBoundingClientRect(); + // IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden + if (ie && rect.top == rect.bottom) return; + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView(); + } + function scrollCursorIntoView() { + var cursor = localCoords(sel.inverted ? sel.from : sel.to); + var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x; + return scrollIntoView(x, cursor.y, x, cursor.yBot); + } + function scrollIntoView(x1, y1, x2, y2) { + var pl = paddingLeft(), pt = paddingTop(); + y1 += pt; y2 += pt; x1 += pl; x2 += pl; + var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true; + if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1); scrolled = true;} + else if (y2 > screentop + screen) {scroller.scrollTop = y2 - screen; scrolled = true;} + + var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; + var gutterw = options.fixedGutter ? gutter.clientWidth : 0; + if (x1 < screenleft + gutterw) { + if (x1 < 50) x1 = 0; + scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw); + scrolled = true; + } + else if (x2 > screenw + screenleft - 3) { + scroller.scrollLeft = x2 + 10 - screenw; + scrolled = true; + if (x2 > code.clientWidth) result = false; + } + if (scrolled && options.onScroll) options.onScroll(instance); + return result; + } + + function visibleLines() { + var lh = textHeight(), top = scroller.scrollTop - paddingTop(); + var from_height = Math.max(0, Math.floor(top / lh)); + var to_height = Math.ceil((top + scroller.clientHeight) / lh); + return {from: lineAtHeight(doc, from_height), + to: lineAtHeight(doc, to_height)}; + } + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplay(changes, suppressCallback) { + if (!scroller.clientWidth) { + showingFrom = showingTo = displayOffset = 0; + return; + } + // Compute the new visible window + var visible = visibleLines(); + // Bail out if the visible area is already rendered and nothing changed. + if (changes !== true && changes.length == 0 && visible.from > showingFrom && visible.to < showingTo) return; + var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100); + if (showingFrom < from && from - showingFrom < 20) from = showingFrom; + if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo); + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = changes === true ? [] : + computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes); + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) {range.domStart += (from - range.from); range.from = from;} + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (intactLines == to - from) return; + intact.sort(function(a, b) {return a.domStart - b.domStart;}); + + var th = textHeight(), gutterDisplay = gutter.style.display; + lineDiv.style.display = "none"; + patchDisplay(from, to, intact); + lineDiv.style.display = gutter.style.display = ""; + + // Position the mover div to align with the lines it's supposed + // to be showing (which will cover the visible display) + var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) lastSizeC = scroller.clientHeight + th; + showingFrom = from; showingTo = to; + displayOffset = heightAtLine(doc, from); + mover.style.top = (displayOffset * th) + "px"; + if (scroller.clientHeight) + code.style.height = (doc.height * th + 2 * paddingTop()) + "px"; + + // Since this is all rather error prone, it is honoured with the + // only assertion in the whole file. + if (lineDiv.childNodes.length != showingTo - showingFrom) + throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) + + " nodes=" + lineDiv.childNodes.length); + + if (options.lineWrapping) { + maxWidth = scroller.clientWidth; + var curNode = lineDiv.firstChild, heightChanged = false; + doc.iter(showingFrom, showingTo, function(line) { + if (!line.hidden) { + var height = Math.round(curNode.offsetHeight / th) || 1; + if (line.widgetFunction) height = line.widgetFunction.size(line.text).height / textHeight(); + if (line.height != height) { + updateLineHeight(line, height); + gutterDirty = heightChanged = true; + } + } + curNode = curNode.nextSibling; + }); + if (heightChanged) + code.style.height = (doc.height * th + 2 * paddingTop()) + "px"; + } else { + if (maxWidth == null) maxWidth = stringWidth(maxLine); + if (maxWidth > scroller.clientWidth) { + lineSpace.style.width = maxWidth + "px"; + // Needed to prevent odd wrapping/hiding of widgets placed in here. + code.style.width = ""; + code.style.width = scroller.scrollWidth + "px"; + } else { + lineSpace.style.width = code.style.width = ""; + } + } + gutter.style.display = gutterDisplay; + if (different || gutterDirty) updateGutter(); + updateSelection(); + if (!suppressCallback && options.onUpdate) options.onUpdate(instance); + return true; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) + intact2.push({from: range.from + diff, to: range.to + diff, + domStart: range.domStart}); + else if (change.to <= range.from || change.from >= range.to) + intact2.push(range); + else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from, domStart: range.domStart}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff, + domStart: range.domStart + (change.to - range.from)}); + } + } + intact = intact2; + } + return intact; + } + + function patchDisplay(from, to, intact) { + // The first pass removes the DOM nodes that aren't intact. + if (!intact.length) lineDiv.innerHTML = ""; + else { + function killNode(node) { + var tmp = node.nextSibling; + node.parentNode.removeChild(node); + return tmp; + } + var domPos = 0, curNode = lineDiv.firstChild, n; + for (var i = 0; i < intact.length; ++i) { + var cur = intact[i]; + while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;} + for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;} + } + while (curNode) curNode = killNode(curNode); + } + // This pass fills in the lines that actually changed. + var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from; + var scratch = document.createElement("div"); + doc.iter(from, to, function(line) { + if (nextIntact && nextIntact.to == j) nextIntact = intact.shift(); + if (!nextIntact || nextIntact.from > j) { + if (line.hidden) var html = scratch.innerHTML = "
";
+          else {
+            var html = line.getHTML(makeTab);
+            if (!line.widgetFunction) {
+              html = '' + html + '';
+            }
+            // Kludge to make sure the styled element lies behind the selection (by z-index)
+            if (line.bgClassName)
+              html = '
 
' + html + "
"; + } + scratch.innerHTML = html; + var insertChild = scratch.firstChild; + lineDiv.insertBefore(insertChild, curNode); + line.nodeAdded(insertChild); + } else { + curNode = curNode.nextSibling; + } + ++j; + }); + } + + function updateGutter() { + if (!options.gutter && !options.lineNumbers) return; + var hText = mover.offsetHeight, hEditor = scroller.clientHeight; + gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px"; + var html = [], i = showingFrom, normalNode; + doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) { + if (line.hidden) { + html.push("
");
+        } else {
+          var marker = line.gutterMarker;
+          var text = options.lineNumbers ? i + options.firstLineNumber : null;
+          if (marker && marker.text)
+            text = marker.text.replace("%N%", text != null ? text : "");
+          else if (text == null)
+            text = "\u00a0";
+          html.push((marker && marker.style ? '
' : "
"), text);
+          for (var j = 1; j < line.height; ++j) html.push("
 "); + html.push("
"); + if (!marker) normalNode = i; + } + ++i; + }); + gutter.style.display = "none"; + gutterText.innerHTML = html.join(""); + // Make sure scrolling doesn't cause number gutter size to pop + if (normalNode != null) { + var node = gutterText.childNodes[normalNode - showingFrom]; + var minwidth = String(doc.size).length, val = eltText(node), pad = ""; + while (val.length + pad.length < minwidth) pad += "\u00a0"; + if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild); + } + gutter.style.display = ""; + lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; + } + function updateSelection() { + var collapsed = posEq(sel.from, sel.to); + var fromPos = localCoords(sel.from, true); + var toPos = collapsed ? fromPos : localCoords(sel.to, true); + var headPos = sel.inverted ? fromPos : toPos, th = textHeight(); + var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); + inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px"; + inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px"; + if (collapsed) { + cursor.style.top = headPos.y + "px"; + cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px"; + cursor.style.display = ""; + selectionDiv.style.display = "none"; + } else { + var sameLine = fromPos.y == toPos.y, html = ""; + function add(left, top, right, height) { + html += '
'; + } + var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth; + var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight; + if (sel.from.ch && fromPos.y >= 0) { + var right = sameLine ? clientWidth - toPos.x : 0; + add(fromPos.x, fromPos.y, right, th); + } + var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0)); + var middleHeight = Math.min(toPos.y, clientHeight) - middleStart; + if (middleHeight > 0.2 * th) + add(0, middleStart, 0, middleHeight); + if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th) + add(0, toPos.y, clientWidth - toPos.x, th); + selectionDiv.innerHTML = html; + cursor.style.display = "none"; + selectionDiv.style.display = ""; + } + } + + function setShift(val) { + if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + else shiftSelecting = null; + } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + userSelChange = true; + } + // Update the selection. Last two args are only used by + // updateLines, since they have to be expressed in the line + // numbers before the update. + function setSelection(from, to, oldFrom, oldTo) { + goalColumn = null; + if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;} + if (posEq(sel.from, from) && posEq(sel.to, to)) return; + if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} + + // Skip over hidden lines. + if (from.line != oldFrom) { + var from1 = skipHidden(from, oldFrom, sel.from.ch); + // If there is no non-hidden line left, force visibility on current line + if (!from1) setLineHidden(from.line, false); + else from = from1; + } + if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch); + + if (posEq(from, to)) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; + + if (options.autoClearEmptyLines && posEq(sel.from, sel.to)) { + var head = sel.inverted ? from : to; + if (head.line != sel.from.line && sel.from.line < doc.size) { + var oldLine = getLine(sel.from.line); + if (/^\s+$/.test(oldLine.text)) + setTimeout(operation(function() { + if (oldLine.parent && /^\s+$/.test(oldLine.text)) { + var no = lineNo(oldLine); + replaceRange("", {line: no, ch: 0}, {line: no, ch: oldLine.text.length}); + } + }, 10)); + } + } + + sel.from = from; sel.to = to; + selectionChanged = true; + } + function skipHidden(pos, oldLine, oldCh) { + function getNonHidden(dir) { + var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1; + while (lNo != end) { + var line = getLine(lNo); + if (!line.hidden) { + var ch = pos.ch; + if (ch > oldCh || ch > line.text.length) ch = line.text.length; + return {line: lNo, ch: ch}; + } + lNo += dir; + } + } + var line = getLine(pos.line); + if (!line.hidden) return pos; + if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1); + else return getNonHidden(-1) || getNonHidden(1); + } + function setCursor(line, ch, user) { + var pos = clipPos({line: line, ch: ch || 0}); + (user ? setSelectionUser : setSelection)(pos, pos); + } + + function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));} + function clipPos(pos) { + if (pos.line < 0) return {line: 0, ch: 0}; + if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length}; + var ch = pos.ch, line = getLine(pos.line), linelen =line.text.length; + if (line.widgetFunction && ch != 0) return {line: pos.line, ch: linelen}; + if (ch == null || ch > linelen) return {line: pos.line, ch: linelen}; + else if (ch < 0) return {line: pos.line, ch: 0}; + else return pos; + } + + function findPosH(dir, unit) { + var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch; + var lineObj = getLine(line); + function findNextLine() { + for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) { + var lo = getLine(l); + if (!lo.hidden) { line = l; lineObj = lo; return true; } + } + } + function moveOnce(boundToLine) { + if (ch == (dir < 0 ? 0 : lineObj.text.length)) { + if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0; + else return false; + } else if (lineObj.widgetFunction) { //Select the entire line + ch = dir < 0 ? 0 : lineObj.text.length; + } else ch += dir; + return true; + } + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word") { + var sawWord = false; + for (;;) { + if (dir < 0) if (!moveOnce()) break; + if (isWordChar(lineObj.text.charAt(ch))) sawWord = true; + else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;} + if (dir > 0) if (!moveOnce()) break; + } + } + return {line: line, ch: ch}; + } + function moveH(dir, unit) { + var pos = dir < 0 ? sel.from : sel.to; + if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit); + setCursor(pos.line, pos.ch, true); + } + function deleteH(dir, unit) { + var from = sel.from; + var to = sel.to; + if (posEq(sel.from, sel.to)) { + if (dir < 0) { + from = findPosH(dir, unit); + if (getLine(from.line).widgetFunction) from.ch = 0; + } + else { + to = findPosH(dir, unit); + if (getLine(to.line).widgetFunction) to.ch = getLine(to.line).text.length; + } + } + replaceRange("", from, to); + userSelChange = true; + } + var goalColumn = null; + function moveV(dir, unit) { + var dist = 0, pos = sel.inverted ? sel.from : sel.to, loc = localCoords(pos, true); + if (goalColumn != null) pos.x = goalColumn; + if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); + else if (unit == "line") { + var line = getLine(pos.line); + if (dir > 0 && line.widgetFunction) dist = Math.ceil(getLine(pos.line).height); + else dist = 1; + dist *= textHeight(); + } + var target = coordsChar(loc.x, loc.y + dist * dir + 2); + if (unit == "page") scroller.scrollTop += localCoords(target, true).y - loc.y; + setCursor(target.line, target.ch, true); + goalColumn = loc.x; + } + + function selectWordAt(pos) { + var line = getLine(pos.line).text; + var start = pos.ch, end = pos.ch; + while (start > 0 && isWordChar(line.charAt(start - 1))) --start; + while (end < line.length && isWordChar(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0})); + } + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } + + function indentLine(n, how) { + if (!how) how = "add"; + if (how == "smart") { + if (!mode.indent) how = "prev"; + else var state = getStateBefore(n); + } + + var line = getLine(n), curSpace = line.indentation(options.tabSize), + curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "prev") { + if (n) indentation = getLine(n-1).indentation(options.tabSize); + else indentation = 0; + } + else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text); + else if (how == "add") indentation = curSpace + options.indentUnit; + else if (how == "subtract") indentation = curSpace - options.indentUnit; + indentation = Math.max(0, indentation); + var diff = indentation - curSpace; + + if (!diff) { + if (sel.from.line != n && sel.to.line != n) return; + var indentString = curSpaceString; + } + else { + var indentString = "", pos = 0; + if (options.indentWithTabs) + for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} + while (pos < indentation) {++pos; indentString += " ";} + } + + replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); + } + + function loadMode() { + mode = CodeMirror.getMode(options, options.mode); + doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); + work = [0]; + startWorker(); + } + function gutterChanged() { + var visible = options.gutter || options.lineNumbers; + gutter.style.display = visible ? "" : "none"; + if (visible) gutterDirty = true; + else lineDiv.parentNode.style.marginLeft = 0; + } + function wrappingChanged(from, to) { + if (options.lineWrapping) { + wrapper.className += " CodeMirror-wrap"; + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(0, doc.size, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != 1) updateLineHeight(line, guess); + }); + lineSpace.style.width = code.style.width = ""; + } else { + wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); + maxWidth = null; maxLine = ""; + doc.iter(0, doc.size, function(line) { + if (line.height != 1 && !line.hidden && !line.widgetFunction) updateLineHeight(line, 1); + if (line.text.length > maxLine.length) maxLine = line.text; + }); + } + changes.push({from: 0, to: doc.size}); + } + function makeTab(col) { + var w = options.tabSize - col % options.tabSize, cached = tabCache[w]; + if (cached) return cached; + for (var str = '', i = 0; i < w; ++i) str += " "; + return (tabCache[w] = {html: str + "", width: w}); + } + function themeChanged() { + scroller.className = scroller.className.replace(/\s*cm-s-\w+/g, "") + + options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + } + + function TextMarker() { this.set = []; } + TextMarker.prototype.clear = operation(function() { + var min = Infinity, max = -Infinity; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + if (!mk || !line.parent) continue; + var lineN = lineNo(line); + min = Math.min(min, lineN); max = Math.max(max, lineN); + for (var j = 0; j < mk.length; ++j) + if (mk[j].marker == this) mk.splice(j--, 1); + } + if (min != Infinity) + changes.push({from: min, to: max + 1}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.marker == this) { + if (mark.from != null || mark.to != null) { + var found = lineNo(line); + if (found != null) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } + } + } + } + return {from: from, to: to}; + }; + + function markText(from, to, className) { + from = clipPos(from); to = clipPos(to); + var tm = new TextMarker(); + if (!posLess(from, to)) return tm; + function add(line, from, to, className) { + getLine(line).addMark(new MarkedText(from, to, className, tm)); + } + if (from.line == to.line) add(from.line, from.ch, to.ch, className); + else { + add(from.line, from.ch, null, className); + for (var i = from.line + 1, e = to.line; i < e; ++i) + add(i, null, null, className); + add(to.line, null, to.ch, className); + } + changes.push({from: from.line, to: to.line + 1}); + return tm; + } + + function setBookmark(pos) { + pos = clipPos(pos); + var bm = new Bookmark(pos.ch); + getLine(pos.line).addMark(bm); + return bm; + } + + function findMarksAt(pos) { + pos = clipPos(pos); + var markers = [], marked = getLine(pos.line).marked; + if (!marked) return markers; + for (var i = 0, e = marked.length; i < e; ++i) { + var m = marked[i]; + if ((m.from == null || m.from <= pos.ch) && + (m.to == null || m.to >= pos.ch)) + markers.push(m.marker || m); + } + return markers; + } + + function addGutterMarker(line, text, className) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = {text: text, style: className}; + gutterDirty = true; + return line; + } + function removeGutterMarker(line) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = null; + gutterDirty = true; + } + + function changeLine(handle, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(clipLine(handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) changes.push({from: no, to: no + 1}); + else return null; + return line; + } + function setLineClass(handle, className, bgClassName) { + return changeLine(handle, function(line) { + if (line.className != className || line.bgClassName != bgClassName) { + line.className = className; + line.bgClassName = bgClassName; + return true; + } + }); + } + function setLineHidden(handle, hidden) { + return changeLine(handle, function(line, no) { + if (line.hidden != hidden) { + line.hidden = hidden; + updateLineHeight(line, hidden ? 0 : 1); + var fline = sel.from.line, tline = sel.to.line; + if (hidden && (fline == no || tline == no)) { + var from = fline == no ? skipHidden({line: fline, ch: 0}, fline, 0) : sel.from; + var to = tline == no ? skipHidden({line: tline, ch: 0}, tline, 0) : sel.to; + // Can't hide the last visible line, we'd have no place to put the cursor + if (!to) return; + setSelection(from, to); + } + return (gutterDirty = true); + } + }); + } + + function lineInfo(line) { + if (typeof line == "number") { + if (!isLine(line)) return null; + var n = line; + line = getLine(line); + if (!line) return null; + } + else { + var n = lineNo(line); + if (n == null) return null; + } + var marker = line.gutterMarker; + return {line: n, handle: line, text: line.text, markerText: marker && marker.text, + markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName}; + } + + function stringWidth(str) { + measure.innerHTML = "
x
"; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } + // These are used to go from pixel positions to character + // positions, taking varying character widths into account. + function charFromX(line, x) { + if (x <= 0) return 0; + var lineObj = getLine(line), text = lineObj.text; + function getX(len) { + measure.innerHTML = "
" + lineObj.getHTML(makeTab, len) + "
"; + return measure.firstChild.firstChild.offsetWidth; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / charWidth())); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + + var tempId = Math.floor(Math.random() * 0xffffff).toString(16); + function measureLine(line, ch) { + if (ch == 0) return {top: 0, left: 0}; + if (line.widgetFunction) { + var size = line.widgetFunction.size(line.text); + return {top: -1, left: size.width}; + } + var extra = ""; + // Include extra text at the end to make sure the measured line is wrapped in the right way. + if (options.lineWrapping) { + var end = line.text.indexOf(" ", ch + 6); + extra = htmlEscape(line.text.slice(ch + 1, end < 0 ? line.text.length : end + (ie ? 5 : 0))); + } + measure.innerHTML = "
" + line.getHTML(makeTab, ch) +
+        '' + htmlEscape(line.text.charAt(ch) || " ") + "" +
+        extra + "
"; + var elt = document.getElementById("CodeMirror-temp-" + tempId); + var top = elt.offsetTop, left = elt.offsetLeft; + // Older IEs report zero offsets for spans directly after a wrap + if (ie && top == 0 && left == 0) { + var backup = document.createElement("span"); + backup.innerHTML = "x"; + elt.parentNode.insertBefore(backup, elt.nextSibling); + top = backup.offsetTop; + } + return {top: top, left: left}; + } + function localCoords(pos, inLineWrap) { + var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); + if (pos.ch == 0) x = 0; + else { + var sp = measureLine(getLine(pos.line), pos.ch); + x = sp.left; + if (options.lineWrapping) y += Math.max(0, sp.top); + } + return {x: x, y: y, yBot: y + lh}; + } + // Coords must be lineSpace-local + function coordsChar(x, y) { + if (y < 0) y = 0; + var th = textHeight(), cw = charWidth(), heightPos = displayOffset + y / th; + var lineNo = lineAtHeight(doc, heightPos); + if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; + var lineObj = getLine(lineNo), text = lineObj.text; + var tw = options.lineWrapping, innerOff = tw ? Math.floor(heightPos - heightAtLine(doc, lineNo)) : 0; + if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; + function getX(len) { + var sp = measureLine(lineObj, len); + if (tw) { + var off = Math.round(sp.top / th); + return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); + } + return sp.left; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw)); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return {line: lineNo, ch: to}; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to}; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + function pageCoords(pos) { + var local = localCoords(pos, true), off = eltOffset(lineSpace); + return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; + } + + var cachedHeight, cachedHeightFor, measureText; + function textHeight() { + if (measureText == null) { + measureText = "
";
+        for (var i = 0; i < 49; ++i) measureText += "x
"; + measureText += "x
"; + } + var offsetHeight = lineDiv.clientHeight; + if (offsetHeight == cachedHeightFor) return cachedHeight; + cachedHeightFor = offsetHeight; + measure.innerHTML = measureText; + cachedHeight = measure.firstChild.offsetHeight / 50 || 1; + measure.innerHTML = ""; + return cachedHeight; + } + var cachedWidth, cachedWidthFor = 0; + function charWidth() { + if (scroller.clientWidth == cachedWidthFor) return cachedWidth; + cachedWidthFor = scroller.clientWidth; + return (cachedWidth = stringWidth("x")); + } + function paddingTop() {return lineSpace.offsetTop;} + function paddingLeft() {return lineSpace.offsetLeft;} + + function posFromMouse(e, liberal) { + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + return coordsChar(x - offL.left, y - offL.top); + } + function onContextMenu(e) { + var pos = posFromMouse(e), scrollPos = scroller.scrollTop; + if (!pos || window.opera) return; // Opera is difficult. + if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) + operation(setCursor)(pos.line, pos.ch); + + var oldCSS = input.style.cssText; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; + var val = input.value = getSelection(); + focusInput(); + selectInput(input); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; + input.style.cssText = oldCSS; + if (ie_lt9) scroller.scrollTop = scrollPos; + leaveInputAlone = false; + resetInput(true); + slowPoll(); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } else { + setTimeout(rehide, 50); + } + } + + // Cursor-blinking + function restartBlink() { + clearInterval(blinker); + var on = true; + cursor.style.visibility = ""; + blinker = setInterval(function() { + cursor.style.visibility = (on = !on) ? "" : "hidden"; + }, 650); + } + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + function matchBrackets(autoclear) { + var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return; + var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles; + for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2) + if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;} + + var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; + function scan(line, from, to) { + if (!line.text) return; + var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur; + for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) { + var text = st[i]; + if (st[i+1] != null && st[i+1] != style) {pos += d * text.length; continue;} + for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) { + if (pos >= from && pos < to && re.test(cur = text.charAt(j))) { + var match = matching[cur]; + if (match.charAt(1) == ">" == forward) stack.push(cur); + else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; + else if (!stack.length) return {pos: pos, match: true}; + } + } + } + } + for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) { + var line = getLine(i), first = i == head.line; + var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); + if (found) break; + } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(n) { + var minindent, minline; + for (var search = n, lim = n - 40; search > lim; --search) { + if (search == 0) return 0; + var line = getLine(search-1); + if (line.stateAfter) return search; + var indented = line.indentation(options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + function getStateBefore(n) { + var start = findStartLine(n), state = start && getLine(start-1).stateAfter; + if (!state) state = startState(mode); + else state = copyState(mode, state); + doc.iter(start, n, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + if (start < n) changes.push({from: start, to: n}); + if (n < doc.size && !getLine(n).stateAfter) work.push(n); + return state; + } + function highlightLines(start, end) { + var state = getStateBefore(start); + doc.iter(start, end, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + } + function highlightWorker() { + var end = +new Date + options.workTime; + var foundWork = work.length; + while (work.length) { + if (!getLine(showingFrom).stateAfter) var task = showingFrom; + else var task = work.pop(); + if (task >= doc.size) continue; + var start = findStartLine(task), state = start && getLine(start-1).stateAfter; + if (state) state = copyState(mode, state); + else state = startState(mode); + + var unchanged = 0, compare = mode.compareStates, realChange = false, + i = start, bail = false; + doc.iter(i, doc.size, function(line) { + var hadState = line.stateAfter; + if (+new Date > end) { + work.push(i); + startWorker(options.workDelay); + if (realChange) changes.push({from: task, to: i + 1}); + return (bail = true); + } + var changed = line.highlight(mode, state, options.tabSize); + if (changed) realChange = true; + line.stateAfter = copyState(mode, state); + if (compare) { + if (hadState && compare(hadState, state)) return true; + } else { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, ""))) + return true; + } + ++i; + }); + if (bail) return; + if (realChange) changes.push({from: task, to: i + 1}); + } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); + } + function startWorker(time) { + if (!work.length) return; + highlight.set(time, operation(highlightWorker)); + } + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + function startOperation() { + updateInput = userSelChange = textChanged = null; + changes = []; selectionChanged = false; callbacks = []; + } + function endOperation() { + var reScroll = false, updated; + if (selectionChanged) reScroll = !scrollCursorIntoView(); + if (changes.length) updated = updateDisplay(changes, true); + else { + if (selectionChanged) updateSelection(); + if (gutterDirty) updateGutter(); + } + if (reScroll) scrollCursorIntoView(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} + + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) + resetInput(userSelChange); + + if (selectionChanged && options.matchBrackets) + setTimeout(operation(function() { + if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} + if (posEq(sel.from, sel.to)) matchBrackets(false); + }), 20); + var tc = textChanged, cbs = callbacks; // these can be reset by callbacks + if (selectionChanged && options.onCursorActivity) + options.onCursorActivity(instance); + if (tc && options.onChange && instance) + options.onChange(instance, tc); + for (var i = 0; i < cbs.length; ++i) cbs[i](instance); + if (updated && options.onUpdate) options.onUpdate(instance); + } + var nestedOperation = 0; + function operation(f) { + return function() { + if (!nestedOperation++) startOperation(); + try {var result = f.apply(this, arguments);} + finally {if (!--nestedOperation) endOperation();} + return result; + }; + } + + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; + return instance; + } // (end of function CodeMirror) + + // The default configuration options. + CodeMirror.defaults = { + value: "", + mode: null, + theme: "default", + indentUnit: 2, + indentWithTabs: false, + smartIndent: true, + tabSize: 4, + keyMap: "default", + extraKeys: null, + electricChars: true, + autoClearEmptyLines: false, + onKeyEvent: null, + lineWrapping: false, + lineNumbers: false, + gutter: false, + fixedGutter: false, + firstLineNumber: 1, + readOnly: false, + onChange: null, + onCursorActivity: null, + onGutterClick: null, + onHighlightComplete: null, + onUpdate: null, + onFocus: null, onBlur: null, onScroll: null, + matchBrackets: false, + workTime: 100, + workDelay: 200, + pollInterval: 100, + undoDepth: 40, + tabindex: null, + autofocus: null + }; + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var win = /Win/.test(navigator.platform); + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + modes[name] = mode; + }; + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) + spec = mimeModes[spec]; + else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) + return CodeMirror.resolveMode("application/xml"); + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) { + if (window.console) console.warn("No mode " + spec.name + " found, falling back to plain text."); + return CodeMirror.getMode(options, "text/plain"); + } + return mfactory(options, spec); + }; + CodeMirror.listModes = function() { + var list = []; + for (var m in modes) + if (modes.propertyIsEnumerable(m)) list.push(m); + return list; + }; + CodeMirror.listMIMEs = function() { + var list = []; + for (var m in mimeModes) + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); + return list; + }; + + var extensions = CodeMirror.extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0}); + else cm.replaceRange("", from, sel ? to : {line: from.line}); + }, + deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});}, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.setCursor(0, 0, true);}, + goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);}, + goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);}, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(); + var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/)); + cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true); + }, + goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);}, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharLeft: function(cm) {cm.deleteH(-1, "char");}, + delCharRight: function(cm) {cm.deleteH(1, "char");}, + delWordLeft: function(cm) {cm.deleteH(-1, "word");}, + delWordRight: function(cm) {cm.deleteH(1, "word");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end");}, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1}); + }, + newlineAndIndent: function(cm) { + cm.replaceSelection("\n", "end"); + cm.indentLine(cm.getCursor().line); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "insertTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft", + "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft", + "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft", + "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + function lookupKey(name, extraMap, map, handle) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found != null && handle(found)) return true; + if (map.catchall) return handle(map.catchall); + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0, e = fallthrough.length; i < e; ++i) { + if (lookup(fallthrough[i])) return true; + } + return false; + } + if (extraMap && lookup(extraMap)) return true; + return lookup(map); + } + function isModifierKey(event) { + var name = keyNames[e_prop(event, "keyCode")]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (options.autofocus == null && textarea.getAttribute("autofocus") != null) + options.autofocus = true; + + function save() {textarea.value = instance.getValue();} + if (textarea.form) { + // Deplorable hack to make the submit method do the right thing. + var rmSubmit = connect(textarea.form, "submit", save, true); + if (typeof textarea.form.submit == "function") { + var realSubmit = textarea.form.submit; + function wrappedSubmit() { + save(); + textarea.form.submit = realSubmit; + textarea.form.submit(); + textarea.form.submit = wrappedSubmit; + } + textarea.form.submit = wrappedSubmit; + } + } + + textarea.style.display = "none"; + var instance = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + instance.save = save; + instance.getTextArea = function() { return textarea; }; + instance.toTextArea = function() { + save(); + textarea.parentNode.removeChild(instance.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + rmSubmit(); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return instance; + }; + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + } + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos);}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() {return countColumn(this.string, this.start, this.tabSize);}, + indentation: function() {return countColumn(this.string, null, this.tabSize);}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } + else { + var match = this.string.slice(this.pos).match(pattern); + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);} + }; + CodeMirror.StringStream = StringStream; + + function MarkedText(from, to, className, marker) { + this.from = from; this.to = to; this.style = className; this.marker = marker; + } + MarkedText.prototype = { + attach: function(line) { this.marker.set.push(line); }, + detach: function(line) { + var ix = indexOf(this.marker.set, line); + if (ix > -1) this.marker.set.splice(ix, 1); + }, + split: function(pos, lenBefore) { + if (this.to <= pos && this.to != null) return null; + var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore; + var to = this.to == null ? null : this.to - pos + lenBefore; + return new MarkedText(from, to, this.style, this.marker); + }, + dup: function() { return new MarkedText(null, null, this.style, this.marker); }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if (fromOpen && to > this.from && (to < this.to || this.to == null)) + this.from = null; + else if (this.from != null && this.from >= from) + this.from = Math.max(to, this.from) + diff; + if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null)) + this.to = null; + else if (this.to != null && this.to > from) + this.to = to < this.to ? this.to + diff : from; + }, + isDead: function() { return this.from != null && this.to != null && this.from >= this.to; }, + sameSet: function(x) { return this.marker == x.marker; } + }; + + function Bookmark(pos) { + this.from = pos; this.to = pos; this.line = null; + } + Bookmark.prototype = { + attach: function(line) { this.line = line; }, + detach: function(line) { if (this.line == line) this.line = null; }, + split: function(pos, lenBefore) { + if (pos < this.from) { + this.from = this.to = (this.from - pos) + lenBefore; + return this; + } + }, + isDead: function() { return this.from > this.to; }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if ((fromOpen || from < this.from) && (toOpen || to > this.to)) { + this.from = 0; this.to = -1; + } else if (this.from > from) { + this.from = this.to = Math.max(to, this.from) + diff; + } + }, + sameSet: function(x) { return false; }, + find: function() { + if (!this.line || !this.line.parent) return null; + return {line: lineNo(this.line), ch: this.from}; + }, + clear: function() { + if (this.line) { + var found = indexOf(this.line.marked, this); + if (found != -1) this.line.marked.splice(found, 1); + this.line = null; + } + } + }; + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + function Line(text, styles) { + this.styles = styles || [text, null]; + this.text = text; + this.height = 1; + this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null; + this.stateAfter = this.parent = this.hidden = null; + this.widgetFunction = null; + } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig && orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null && mk[i].style) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln); + } + } + } + return ln; + } + Line.prototype = { + // Replace a piece of a line, keeping the styles around it intact. + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; + copyStyles(0, from, this.styles, st); + if (text) st.push(text, null); + copyStyles(to, this.text.length, this.styles, st); + this.styles = st; + this.text = this.text.slice(0, from) + text + this.text.slice(to); + this.stateAfter = null; + if (mk) { + var diff = text.length - (to - from); + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + mark.clipTo(from == null, from || 0, to_ == null, to, diff); + if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);} + } + } + }, + // Split a part off a line, keeping styles and markers intact. + split: function(pos, textBefore) { + var st = [textBefore, null], mk = this.marked; + copyStyles(pos, this.text.length, this.styles, st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + var newmark = mark.split(pos, textBefore.length); + if (newmark) { + if (!taken.marked) taken.marked = []; + taken.marked.push(newmark); newmark.attach(taken); + if (newmark == mark) mk.splice(i--, 1); + } + } + } + return taken; + }, + append: function(line) { + var mylen = this.text.length, mk = line.marked, mymk = this.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mymk) { + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + } + if (mk && mk.length) { + if (!mymk) this.marked = mymk = []; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.sameSet(mark)) { + mymark.to = mark.to == null ? null : mark.to + mylen; + if (mymark.isDead()) { + mymark.detach(this); + mk.splice(i--, 1); + } + continue outer; + } + } + } + mymk.push(mark); + mark.attach(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + fixMarkEnds: function(other) { + var mk = this.marked, omk = other.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i], close = mark.to == null; + if (close && omk) { + for (var j = 0; j < omk.length; ++j) + if (omk[j].sameSet(mark)) {close = false; break;} + } + if (close) mark.to = this.text.length; + } + }, + fixMarkStarts: function() { + var mk = this.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) + if (mk[i].from == null) mk[i].from = 0; + }, + addMark: function(mark) { + mark.attach(this); + if (this.marked == null) this.marked = []; + this.marked.push(mark); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); + }, + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + highlight: function(mode, state, tabSize) { + var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + var substr = this.text.slice(stream.start, stream.pos); + stream.start = stream.pos; + if (pos && st[pos-1] == style) + st[pos-2] += substr; + else if (substr) { + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; + st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; + } + // Give up when line is ridiculously long + if (stream.pos > 5000) { + st[pos++] = this.text.slice(stream.pos); st[pos++] = null; + break; + } + } + if (st.length != pos) {st.length = pos; changed = true;} + if (pos && st[pos-2] != prevWord) changed = true; + if (st.length == 2 && typeof st[1] == 'object') { + this.widgetFunction = st[1]; + st[1] = null; + } else { + this.widgetFunction = null; + } + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); + }, + nodeAdded: function(node) { + if (this.widgetFunction) this.widgetFunction.callback(node, this); + }, + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(mode, state, ch) { + var txt = this.text, stream = new StringStream(txt); + while (stream.pos < ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, + state: state}; + }, + indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, + // Produces an HTML fragment for the line, taking selection, + // marking, and highlighting into account. + getHTML: function(makeTab, endAt) { + var html = [], first = true, col = 0; + function span(text, style) { + if (!text) return; + // Work around a bug where, in some compat modes, IE ignores leading spaces + if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1); + first = false; + if (text.indexOf("\t") == -1) { + col += text.length; + var escaped = htmlEscape(text); + } else { + var escaped = ""; + for (var pos = 0;;) { + var idx = text.indexOf("\t", pos); + if (idx == -1) { + escaped += htmlEscape(text.slice(pos)); + col += text.length - pos; + break; + } else { + col += idx - pos; + var tab = makeTab(col); + escaped += htmlEscape(text.slice(pos, idx)) + tab.html; + col += tab.width; + pos = idx + 1; + } + } + } + if (style) html.push('', escaped, ""); + else html.push(escaped); + } + var st = this.styles, allText = this.text, marked = this.marked; + var len = allText.length; + if (this.widgetFunction) return this.widgetFunction.creator(allText); + if (endAt != null) len = Math.min(endAt, len); + function styleToClass(style) { + if (!style) return null; + return "cm-" + style.replace(/ +/g, " cm-"); + } + + if (!allText && endAt == null) + span(" "); + else if (!marked || !marked.length) + for (var i = 0, ch = 0; ch < len; i+=2) { + var str = st[i], style = st[i+1], l = str.length; + if (ch + l > len) str = str.slice(0, len - ch); + ch += l; + span(str, styleToClass(style)); + } + else { + var pos = 0, i = 0, text = "", style, sg = 0; + var nextChange = marked[0].from || 0, marks = [], markpos = 0; + function advanceMarks() { + var m; + while (markpos < marked.length && + ((m = marked[markpos]).from == pos || m.from == null)) { + if (m.style != null) marks.push(m); + ++markpos; + } + nextChange = markpos < marked.length ? marked[markpos].from : Infinity; + for (var i = 0; i < marks.length; ++i) { + var to = marks[i].to || Infinity; + if (to == pos) marks.splice(i--, 1); + else nextChange = Math.min(to, nextChange); + } + } + var m = 0; + while (pos < len) { + if (nextChange == pos) advanceMarks(); + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + var appliedStyle = style; + for (var j = 0; j < marks.length; ++j) + appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style; + span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle); + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + } + text = st[i++]; style = styleToClass(st[i++]); + } + } + } + return html.join(""); + }, + cleanUp: function() { + this.parent = null; + if (this.marked) + for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this); + } + }; + // Utility used by replace and split above + function copyStyles(from, to, source, dest) { + for (var i = 0, pos = 0, state = 0; pos < to; i+=2) { + var part = source[i], end = pos + part.length; + if (state == 0) { + if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]); + if (end >= from) state = 1; + } + else if (state == 1) { + if (end > to) dest.push(part.slice(0, to - pos), source[i+1]); + else dest.push(part, source[i+1]); + } + pos = end; + } + } + + // Data structure that holds the sequence of lines. + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + remove: function(at, n, callbacks) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + line.cleanUp(); + if (line.handlers) + for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertHeight: function(at, lines, height) { + this.height += height; + this.lines.splice.apply(this.lines, [at, 0].concat(lines)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + remove: function(at, n, callbacks) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.remove(at, rm, callbacks); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertHeight(at, lines, height); + }, + insertHeight: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertHeight(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iter: function(from, to, op) { this.iterN(from, to - from, op); }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + function getLineAt(chunk, n) { + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0, e = chunk.children.length; ; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no; + } + function lineAtHeight(chunk, h) { + var n = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + function heightAtLine(chunk, n) { + var h = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; continue outer; } + n -= sz; + h += child.height; + } + return h; + } while (!chunk.lines); + for (var i = 0; i < n; ++i) h += chunk.lines[i].height; + return h; + } + + // The history object 'chunks' changes that are made close together + // and at almost the same time into bigger undoable units. + function History() { + this.time = 0; + this.done = []; this.undone = []; + } + History.prototype = { + addChange: function(start, added, old) { + this.undone.length = 0; + var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1]; + var dtime = time - this.time; + if (dtime > 400 || !last) { + this.done.push([{start: start, added: added, old: old}]); + } else if (last.start > start + old.length || last.start + last.added < start - last.added + last.old.length) { + cur.push({start: start, added: added, old: old}); + } else { + var oldoff = 0; + if (start < last.start) { + for (var i = last.start - start - 1; i >= 0; --i) + last.old.unshift(old[i]); + oldoff = Math.min(0, added - old.length); + last.added += last.start - start + oldoff; + last.start = start; + } else if (last.start < start) { + oldoff = start - last.start; + added += oldoff; + } + for (var i = last.added - oldoff, e = old.length; i < e; ++i) + last.old.push(old[i]); + if (last.added < added) last.added = added; + } + this.time = time; + } + }; + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + if (e.which) return e.which; + else if (e.button & 1) return 1; + else if (e.button & 2) return 3; + else if (e.button & 4) return 2; + } + + // Allow 3rd-party code to override event properties by adding an override + // object to an event object. + function e_prop(e, prop) { + var overridden = e.override && e.override.hasOwnProperty(prop); + return overridden ? e.override[prop] : e[prop]; + } + + // Event handler registration. If disconnect is true, it'll return a + // function that unregisters the handler. + function connect(node, type, handler, disconnect) { + if (typeof node.addEventListener == "function") { + node.addEventListener(type, handler, false); + if (disconnect) return function() {node.removeEventListener(type, handler, false);}; + } + else { + var wrapHandler = function(event) {handler(event || window.event);}; + node.attachEvent("on" + type, wrapHandler); + if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);}; + } + } + CodeMirror.connect = connect; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent); + var webkit = /WebKit\//.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var khtml = /KHTML\//.test(navigator.userAgent); + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie_lt9) return false; + var div = document.createElement('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var lineSep = "\n"; + // Feature-detect whether newlines in textareas are converted to \r\n + (function () { + var te = document.createElement("textarea"); + te.value = "foo\nbar"; + if (te.value.indexOf("\r") > -1) lineSep = "\r\n"; + }()); + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = 0, n = 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + + function computedStyle(elt) { + if (elt.currentStyle) return elt.currentStyle; + return window.getComputedStyle(elt, null); + } + + // Find the position of an element by following the offsetParent chain. + // If screen==true, it returns screen (rather than page) coordinates. + function eltOffset(node, screen) { + var bod = node.ownerDocument.body; + var x = 0, y = 0, skipBody = false; + for (var n = node; n; n = n.offsetParent) { + var ol = n.offsetLeft, ot = n.offsetTop; + // Firefox reports weird inverted offsets when the body has a border. + if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); } + else { x += ol, y += ot; } + if (screen && computedStyle(n).position == "fixed") + skipBody = true; + } + var e = screen && !skipBody ? null : bod; + for (var n = node.parentNode; n != e; n = n.parentNode) + if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} + return {left: x, top: y}; + } + // Use the faster and saner getBoundingClientRect method when possible. + if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) { + // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, + // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) + try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } + catch(e) { box = {top: 0, left: 0}; } + if (!screen) { + // Get the toplevel scroll, working around browser differences. + if (window.pageYOffset == null) { + var t = document.documentElement || document.body.parentNode; + if (t.scrollTop == null) t = document.body; + box.top += t.scrollTop; box.left += t.scrollLeft; + } else { + box.top += window.pageYOffset; box.left += window.pageXOffset; + } + } + return box; + }; + + // Get a node's text content. + function eltText(node) { + return node.textContent || node.innerText || node.nodeValue || ""; + } + function selectInput(node) { + if (ios) { // Mobile Safari apparently has a bug where select() is broken. + node.selectionStart = 0; + node.selectionEnd = node.value.length; + } else node.select(); + } + + // Operations on {line, ch} objects. + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return {line: x.line, ch: x.ch};} + + var escapeElement = document.createElement("pre"); + function htmlEscape(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML; + } + // Recent (late 2011) Opera betas insert bogus newlines at the start + // of the textContent, so we strip those. + if (htmlEscape("a") == "\na") + htmlEscape = function(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML.slice(1); + }; + // Some IEs don't preserve tabs through innerHTML + else if (htmlEscape("\t") != "\t") + htmlEscape = function(str) { + escapeElement.innerHTML = ""; + escapeElement.appendChild(document.createTextNode(str)); + return escapeElement.innerHTML; + }; + CodeMirror.htmlEscape = htmlEscape; + + // Used to position the cursor after an undo/redo by finding the + // last edited character. + function editEnd(from, to) { + if (!to) return 0; + if (!from) return to.length; + for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j) + if (from.charAt(i) != to.charAt(j)) break; + return j + 1; + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + function isWordChar(ch) { + return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase(); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, nl, result = []; + while ((nl = string.indexOf("\n", pos)) > -1) { + result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); + pos = nl + 1; + } + result.push(string.slice(pos)); + return result; + } : function(string){return string.split(/\r?\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 127: "Delete", 186: ";", 187: "=", 188: ",", + 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 63276: "PageUp", + 63277: "PageDown", 63275: "End", 63273: "Home", 63234: "Left", 63232: "Up", 63235: "Right", + 63233: "Down", 63302: "Insert", 63272: "Delete"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + return CodeMirror; +})(); diff --git a/lms/static/js/vendor/CodeMirror/mitx_markdown.js b/lms/static/js/vendor/CodeMirror/mitx_markdown.js new file mode 100644 index 0000000000..b6fe4dd459 --- /dev/null +++ b/lms/static/js/vendor/CodeMirror/mitx_markdown.js @@ -0,0 +1,373 @@ +var schematic_height = 480; +var schematic_width = 640; + +$(function(){ + $(document).ready(function() { + //$("a[rel*=leanModal]").leanModal(); //TODO: Make this work with the new modal library. Try and integrate this with the "slices" + + $("body").append('
'+ + '' + + '
'); + + //This is the editor that pops up as a modal + var editorCircuit = $("#schematic_editor").get(0); + //This is the circuit that they last clicked. The one being edited. + var editingCircuit = null; + //Notice we use live, because new circuits can be inserted + $(".schematic_open").live("click", function() { + //Find the new editingCircuit. Transfer its contents to the editorCircuit + editingCircuit = $(this).children("input.schematic").get(0); + + editingCircuit.schematic.update_value(); + var circuit_so_far = $(editingCircuit).val(); + + var n = editorCircuit.schematic.components.length; + for (var i = 0; i < n; i++) + editorCircuit.schematic.components[n - 1 - i].remove(); + + editorCircuit.schematic.load_schematic(circuit_so_far, ""); + }); + + $("#circuit_save_btn").click(function () { + //Take the circuit from the editor and put it back into editingCircuit + editorCircuit.schematic.update_value(); + var saving_circuit = $(editorCircuit).val(); + + var n = editingCircuit.schematic.components.length; + for (var i = 0; i < n; i++) + editingCircuit.schematic.components[n - 1 - i].remove(); + + editingCircuit.schematic.load_schematic(saving_circuit, ""); + + if (editingCircuit.codeMirrorLine) { + editingCircuit.codeMirrorLine.replace(0, null, "circuit-schematic:" + saving_circuit); + } + + $(".modal_close").first().click(); + }); + }); +}); + + +CodeMirror.defineMode("mitx_markdown", function(cmCfg, modeCfg) { + + var htmlMode = CodeMirror.getMode(cmCfg, { name: 'xml', htmlMode: true }); + + var header = 'header' + , code = 'comment' + , quote = 'quote' + , list = 'string' + , hr = 'hr' + , linktext = 'link' + , linkhref = 'string' + , em = 'em' + , strong = 'strong' + , emstrong = 'emstrong'; + + function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + var circuit_formatter = { + creator: function(text) { + var circuit_value = text.match(circuitRE)[1] + + circuit_value = escapeHtml(circuit_value); + + var html = ""; + + return html; + }, + size: function(text) { + return {width: schematic_width, height:schematic_height}; + }, + callback: function(node, line) { + try { + update_schematics(); + var schmInput = node.firstChild.firstChild; + schmInput.codeMirrorLine = line; + if (schmInput.schematic) { //This is undefined if there was an error making the schematic + schmInput.schematic.canvas.style.display = "block"; //Otherwise, it gets line height and is a weird size + schmInput.schematic.always_draw_grid = true; + schmInput.schematic.redraw_background(); + } + $(node.firstChild).leanModal(); + } catch (err) { + console.log("Error in mitx_markdown callback: " + err); + } + + } + }; + + + var hrRE = /^[*-=_]/ + , ulRE = /^[*-+]\s+/ + , olRE = /^[0-9]+\.\s+/ + , headerRE = /^(?:\={3,}|-{3,})$/ + , codeRE = /^(k:\t|\s{4,})/ + , textRE = /^[^\[*_\\<>`]+/ + , circuitRE = /^circuit-schematic:(.*)$/; + + function switchInline(stream, state, f) { + state.f = state.inline = f; + return f(stream, state); + } + + function switchBlock(stream, state, f) { + state.f = state.block = f; + return f(stream, state); + } + + + // Blocks + + function blockNormal(stream, state) { + var match; + if (stream.match(circuitRE)) { + stream.skipToEnd(); + return circuit_formatter; + } else if (stream.match(codeRE)) { + stream.skipToEnd(); + return code; + } else if (stream.eatSpace()) { + return null; + } else if (stream.peek() === '#' || stream.match(headerRE)) { + state.header = true; + } else if (stream.eat('>')) { + state.indentation++; + state.quote = true; + } else if (stream.peek() === '[') { + return switchInline(stream, state, footnoteLink); + } else if (hrRE.test(stream.peek())) { + var re = new RegExp('(?:\s*['+stream.peek()+']){3,}$'); + if (stream.match(re, true)) { + return hr; + } + } else if (match = stream.match(ulRE, true) || stream.match(olRE, true)) { + state.indentation += match[0].length; + return list; + } + + return switchInline(stream, state, state.inline); + } + + function htmlBlock(stream, state) { + var style = htmlMode.token(stream, state.htmlState); + if (style === 'tag' && state.htmlState.type !== 'openTag' && !state.htmlState.context) { + state.f = inlineNormal; + state.block = blockNormal; + } + return style; + } + + + // Inline + function getType(state) { + + // Set defaults + returnValue = ''; + + // Strong / Emphasis + if(state.strong){ + if(state.em){ + returnValue += (returnValue ? ' ' : '') + emstrong; + } else { + returnValue += (returnValue ? ' ' : '') + strong; + } + } else { + if(state.em){ + returnValue += (returnValue ? ' ' : '') + em; + } + } + + // Header + if(state.header){ + returnValue += (returnValue ? ' ' : '') + header; + } + + // Quotes + if(state.quote){ + returnValue += (returnValue ? ' ' : '') + quote; + } + + // Check valud and return + if(!returnValue){ + returnValue = null; + } + return returnValue; + + } + + function handleText(stream, state) { + if (stream.match(textRE, true)) { + return getType(state); + } + return undefined; + } + + function inlineNormal(stream, state) { + var style = state.text(stream, state) + if (typeof style !== 'undefined') + return style; + + var ch = stream.next(); + + if (ch === '\\') { + stream.next(); + return getType(state); + } + if (ch === '`') { + return switchInline(stream, state, inlineElement(code, '`')); + } + if (ch === '[') { + return switchInline(stream, state, linkText); + } + if (ch === '<' && stream.match(/^\w/, false)) { + stream.backUp(1); + return switchBlock(stream, state, htmlBlock); + } + + var t = getType(state); + if (ch === '*' || ch === '_') { + if (stream.eat(ch)) { + return (state.strong = !state.strong) ? getType(state) : t; + } + return (state.em = !state.em) ? getType(state) : t; + } + + return getType(state); + } + + function linkText(stream, state) { + while (!stream.eol()) { + var ch = stream.next(); + if (ch === '\\') stream.next(); + if (ch === ']') { + state.inline = state.f = linkHref; + return linktext; + } + } + return linktext; + } + + function linkHref(stream, state) { + stream.eatSpace(); + var ch = stream.next(); + if (ch === '(' || ch === '[') { + return switchInline(stream, state, inlineElement(linkhref, ch === '(' ? ')' : ']')); + } + return 'error'; + } + + function footnoteLink(stream, state) { + if (stream.match(/^[^\]]*\]:/, true)) { + state.f = footnoteUrl; + return linktext; + } + return switchInline(stream, state, inlineNormal); + } + + function footnoteUrl(stream, state) { + stream.eatSpace(); + stream.match(/^[^\s]+/, true); + state.f = state.inline = inlineNormal; + return linkhref; + } + + function inlineRE(endChar) { + if (!inlineRE[endChar]) { + // match any not-escaped-non-endChar and any escaped char + // then match endChar or eol + inlineRE[endChar] = new RegExp('^(?:[^\\\\\\' + endChar + ']|\\\\.)*(?:\\' + endChar + '|$)'); + } + return inlineRE[endChar]; + } + + function inlineElement(type, endChar, next) { + next = next || inlineNormal; + return function(stream, state) { + stream.match(inlineRE(endChar)); + state.inline = state.f = next; + return type; + }; + } + + return { + startState: function() { + return { + f: blockNormal, + + block: blockNormal, + htmlState: htmlMode.startState(), + indentation: 0, + + inline: inlineNormal, + text: handleText, + em: false, + strong: false, + header: false, + quote: false + }; + }, + + copyState: function(s) { + return { + f: s.f, + + block: s.block, + htmlState: CodeMirror.copyState(htmlMode, s.htmlState), + indentation: s.indentation, + + inline: s.inline, + text: s.text, + em: s.em, + strong: s.strong, + header: s.header, + quote: s.quote + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + // Reset EM state + state.em = false; + // Reset STRONG state + state.strong = false; + // Reset state.header + state.header = false; + // Reset state.quote + state.quote = false; + + state.f = state.block; + var previousIndentation = state.indentation + , currentIndentation = 0; + while (previousIndentation > 0) { + if (stream.eat(' ')) { + previousIndentation--; + currentIndentation++; + } else if (previousIndentation >= 4 && stream.eat('\t')) { + previousIndentation -= 4; + currentIndentation += 4; + } else { + break; + } + } + state.indentation = currentIndentation; + + if (currentIndentation > 0) return null; + } + return state.f(stream, state); + }, + + getType: getType + }; + +}); + +CodeMirror.defineMIME("text/x-markdown", "markdown"); diff --git a/lms/static/js/vendor/CodeMirror/python.js b/lms/static/js/vendor/CodeMirror/python.js new file mode 100644 index 0000000000..cb59e5cbc4 --- /dev/null +++ b/lms/static/js/vendor/CodeMirror/python.js @@ -0,0 +1,341 @@ +CodeMirror.defineMode("python", function(conf, parserConf) { + var ERRORCLASS = 'error'; + + function wordRegexp(words) { + return new RegExp("^((" + words.join(")|(") + "))\\b"); + } + + var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!]"); + var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]'); + var doubleOperators = new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))"); + var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); + var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); + var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); + + var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'in']); + var commonkeywords = ['as', 'assert', 'break', 'class', 'continue', + 'def', 'del', 'elif', 'else', 'except', 'finally', + 'for', 'from', 'global', 'if', 'import', + 'lambda', 'pass', 'raise', 'return', + 'try', 'while', 'with', 'yield']; + var commonBuiltins = ['abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'callable', 'chr', + 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod', + 'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset', + 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', + 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', + 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', + 'object', 'oct', 'open', 'ord', 'pow', 'property', 'range', + 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', + 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', + 'type', 'vars', 'zip', '__import__', 'NotImplemented', + 'Ellipsis', '__debug__']; + var py2 = {'builtins': ['apply', 'basestring', 'buffer', 'cmp', 'coerce', 'execfile', + 'file', 'intern', 'long', 'raw_input', 'reduce', 'reload', + 'unichr', 'unicode', 'xrange', 'False', 'True', 'None'], + 'keywords': ['exec', 'print']}; + var py3 = {'builtins': ['ascii', 'bytes', 'exec', 'print'], + 'keywords': ['nonlocal', 'False', 'True', 'None']}; + + if (!!parserConf.version && parseInt(parserConf.version, 10) === 3) { + commonkeywords = commonkeywords.concat(py3.keywords); + commonBuiltins = commonBuiltins.concat(py3.builtins); + var stringPrefixes = new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))", "i"); + } else { + commonkeywords = commonkeywords.concat(py2.keywords); + commonBuiltins = commonBuiltins.concat(py2.builtins); + var stringPrefixes = new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); + } + var keywords = wordRegexp(commonkeywords); + var builtins = wordRegexp(commonBuiltins); + + var indentInfo = null; + + // tokenizers + function tokenBase(stream, state) { + // Handle scope changes + if (stream.sol()) { + var scopeOffset = state.scopes[0].offset; + if (stream.eatSpace()) { + var lineOffset = stream.indentation(); + if (lineOffset > scopeOffset) { + indentInfo = 'indent'; + } else if (lineOffset < scopeOffset) { + indentInfo = 'dedent'; + } + return null; + } else { + if (scopeOffset > 0) { + dedent(stream, state); + } + } + } + if (stream.eatSpace()) { + return null; + } + + var ch = stream.peek(); + + // Handle Comments + if (ch === '#') { + stream.skipToEnd(); + return 'comment'; + } + + // Handle Number Literals + if (stream.match(/^[0-9\.]/, false)) { + var floatLiteral = false; + // Floats + if (stream.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } + if (stream.match(/^\d+\.\d*/)) { floatLiteral = true; } + if (stream.match(/^\.\d+/)) { floatLiteral = true; } + if (floatLiteral) { + // Float literals may be "imaginary" + stream.eat(/J/i); + return 'number'; + } + // Integers + var intLiteral = false; + // Hex + if (stream.match(/^0x[0-9a-f]+/i)) { intLiteral = true; } + // Binary + if (stream.match(/^0b[01]+/i)) { intLiteral = true; } + // Octal + if (stream.match(/^0o[0-7]+/i)) { intLiteral = true; } + // Decimal + if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) { + // Decimal literals may be "imaginary" + stream.eat(/J/i); + // TODO - Can you have imaginary longs? + intLiteral = true; + } + // Zero by itself with no other piece of number. + if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; } + if (intLiteral) { + // Integer literals may be "long" + stream.eat(/L/i); + return 'number'; + } + } + + // Handle Strings + if (stream.match(stringPrefixes)) { + state.tokenize = tokenStringFactory(stream.current()); + return state.tokenize(stream, state); + } + + // Handle operators and Delimiters + if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) { + return null; + } + if (stream.match(doubleOperators) + || stream.match(singleOperators) + || stream.match(wordOperators)) { + return 'operator'; + } + if (stream.match(singleDelimiters)) { + return null; + } + + if (stream.match(keywords)) { + return 'keyword'; + } + + if (stream.match(builtins)) { + return 'builtin'; + } + + if (stream.match(identifiers)) { + return 'variable'; + } + + // Handle non-detected items + stream.next(); + return ERRORCLASS; + } + + function tokenStringFactory(delimiter) { + while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) { + delimiter = delimiter.substr(1); + } + var singleline = delimiter.length == 1; + var OUTCLASS = 'string'; + + return function tokenString(stream, state) { + while (!stream.eol()) { + stream.eatWhile(/[^'"\\]/); + if (stream.eat('\\')) { + stream.next(); + if (singleline && stream.eol()) { + return OUTCLASS; + } + } else if (stream.match(delimiter)) { + state.tokenize = tokenBase; + return OUTCLASS; + } else { + stream.eat(/['"]/); + } + } + if (singleline) { + if (parserConf.singleLineStringErrors) { + return ERRORCLASS; + } else { + state.tokenize = tokenBase; + } + } + return OUTCLASS; + }; + } + + function indent(stream, state, type) { + type = type || 'py'; + var indentUnit = 0; + if (type === 'py') { + if (state.scopes[0].type !== 'py') { + state.scopes[0].offset = stream.indentation(); + return; + } + for (var i = 0; i < state.scopes.length; ++i) { + if (state.scopes[i].type === 'py') { + indentUnit = state.scopes[i].offset + conf.indentUnit; + break; + } + } + } else { + indentUnit = stream.column() + stream.current().length; + } + state.scopes.unshift({ + offset: indentUnit, + type: type + }); + } + + function dedent(stream, state, type) { + type = type || 'py'; + if (state.scopes.length == 1) return; + if (state.scopes[0].type === 'py') { + var _indent = stream.indentation(); + var _indent_index = -1; + for (var i = 0; i < state.scopes.length; ++i) { + if (_indent === state.scopes[i].offset) { + _indent_index = i; + break; + } + } + if (_indent_index === -1) { + return true; + } + while (state.scopes[0].offset !== _indent) { + state.scopes.shift(); + } + return false + } else { + if (type === 'py') { + state.scopes[0].offset = stream.indentation(); + return false; + } else { + if (state.scopes[0].type != type) { + return true; + } + state.scopes.shift(); + return false; + } + } + } + + function tokenLexer(stream, state) { + indentInfo = null; + var style = state.tokenize(stream, state); + var current = stream.current(); + + // Handle '.' connected identifiers + if (current === '.') { + style = state.tokenize(stream, state); + current = stream.current(); + if (style === 'variable' || style === 'builtin') { + return 'variable'; + } else { + return ERRORCLASS; + } + } + + // Handle decorators + if (current === '@') { + style = state.tokenize(stream, state); + current = stream.current(); + if (style === 'variable' + || current === '@staticmethod' + || current === '@classmethod') { + return 'meta'; + } else { + return ERRORCLASS; + } + } + + // Handle scope changes. + if (current === 'pass' || current === 'return') { + state.dedent += 1; + } + if (current === 'lambda') state.lambda = true; + if ((current === ':' && !state.lambda && state.scopes[0].type == 'py') + || indentInfo === 'indent') { + indent(stream, state); + } + var delimiter_index = '[({'.indexOf(current); + if (delimiter_index !== -1) { + indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1)); + } + if (indentInfo === 'dedent') { + if (dedent(stream, state)) { + return ERRORCLASS; + } + } + delimiter_index = '])}'.indexOf(current); + if (delimiter_index !== -1) { + if (dedent(stream, state, current)) { + return ERRORCLASS; + } + } + if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'py') { + if (state.scopes.length > 1) state.scopes.shift(); + state.dedent -= 1; + } + + return style; + } + + var external = { + startState: function(basecolumn) { + return { + tokenize: tokenBase, + scopes: [{offset:basecolumn || 0, type:'py'}], + lastToken: null, + lambda: false, + dedent: 0 + }; + }, + + token: function(stream, state) { + var style = tokenLexer(stream, state); + + state.lastToken = {style:style, content: stream.current()}; + + if (stream.eol() && stream.lambda) { + state.lambda = false; + } + + return style; + }, + + indent: function(state, textAfter) { + if (state.tokenize != tokenBase) { + return 0; + } + + return state.scopes[0].offset; + } + + }; + return external; +}); + +CodeMirror.defineMIME("text/x-python", "python"); diff --git a/lms/static/js/vendor/CodeMirror/xml.js b/lms/static/js/vendor/CodeMirror/xml.js new file mode 100644 index 0000000000..f467bddccc --- /dev/null +++ b/lms/static/js/vendor/CodeMirror/xml.js @@ -0,0 +1,267 @@ +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, + "meta": true, "col": true, "frame": true, "base": true, "area": true}, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: false + } : {autoSelfClosers: {}, doNotIndent: {}, allowUnquoted: false, allowMissing: false}; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var tagName, type; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } + else if (stream.match("--")) return chain(inBlock("comment", "-->")); + else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } + else return null; + } + else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } + else { + type = stream.eat("/") ? "closeTag" : "openTag"; + stream.eatSpace(); + tagName = ""; + var c; + while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + state.tokenize = inTag; + return "tag"; + } + } + else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } + else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag"; + } + else if (ch == "=") { + type = "equals"; + return null; + } + else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + return state.tokenize(stream, state); + } + else { + stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/); + return "word"; + } + } + + function inAttribute(quote) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + var curState, setStyle; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + + function pushContext(tagName, startOfLine) { + var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent); + curState.context = { + prev: curState.context, + tagName: tagName, + indent: curState.indented, + startOfLine: startOfLine, + noIndent: noIndent + }; + } + function popContext() { + if (curState.context) curState.context = curState.context.prev; + } + + function element(type) { + if (type == "openTag") { + curState.tagName = tagName; + return cont(attributes, endtag(curState.startOfLine)); + } else if (type == "closeTag") { + var err = false; + if (curState.context) { + err = curState.context.tagName != tagName; + } else { + err = true; + } + if (err) setStyle = "error"; + return cont(endclosetag(err)); + } + return cont(); + } + function endtag(startOfLine) { + return function(type) { + if (type == "selfcloseTag" || + (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) + return cont(); + if (type == "endTag") {pushContext(curState.tagName, startOfLine); return cont();} + return cont(); + }; + } + function endclosetag(err) { + return function(type) { + if (err) setStyle = "error"; + if (type == "endTag") { popContext(); return cont(); } + setStyle = "error"; + return cont(arguments.callee); + } + } + + function attributes(type) { + if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);} + if (type == "endTag" || type == "selfcloseTag") return pass(); + setStyle = "error"; + return cont(attributes); + } + function attribute(type) { + if (type == "equals") return cont(attvalue, attributes); + if (!Kludges.allowMissing) setStyle = "error"; + return (type == "endTag" || type == "selfcloseTag") ? pass() : cont(); + } + function attvalue(type) { + if (type == "string") return cont(attvaluemaybe); + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();} + setStyle = "error"; + return (type == "endTag" || type == "selfCloseTag") ? pass() : cont(); + } + function attvaluemaybe(type) { + if (type == "string") return cont(attvaluemaybe); + else return pass(); + } + + return { + startState: function() { + return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, context: null}; + }, + + token: function(stream, state) { + if (stream.sol()) { + state.startOfLine = true; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + + setStyle = type = tagName = null; + var style = state.tokenize(stream, state); + state.type = type; + if ((style || type) && style != "comment") { + curState = state; + while (true) { + var comb = state.cc.pop() || element; + if (comb(type || style)) break; + } + } + state.startOfLine = false; + return setStyle || style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + if ((state.tokenize != inTag && state.tokenize != inText) || + context && context.noIndent) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + if (alignCDATA && /Textbook
  • Discussion
  • % endif -
  • Wiki
  • +
  • Wiki
  • % if user.is_authenticated():
  • Profile
  • % endif diff --git a/lms/templates/simplewiki_base.html b/lms/templates/simplewiki/simplewiki_base.html similarity index 64% rename from lms/templates/simplewiki_base.html rename to lms/templates/simplewiki/simplewiki_base.html index 6a9be3bf08..e3da3a754d 100644 --- a/lms/templates/simplewiki_base.html +++ b/lms/templates/simplewiki/simplewiki_base.html @@ -1,14 +1,15 @@ ##This file is based on the template from the SimpleWiki source which carries the GPL license -<%inherit file="main.html"/> -<%namespace name='static' file='static_content.html'/> +<%inherit file="../main.html"/> +<%namespace name='static' file='../static_content.html'/> + +<%! + from django.core.urlresolvers import reverse + from simplewiki.views import wiki_reverse +%> <%block name="headextra"> - - - <%! - from django.core.urlresolvers import reverse - %> + + + + + + + + + + +<%block name="wiki_body"> +
    +
    + +
    + ${wiki_form} + %if create_article: + + %else: + + + %endif +
    + +<%include file="simplewiki_instructions.html"/> + + diff --git a/lms/templates/simplewiki_error.html b/lms/templates/simplewiki/simplewiki_error.html similarity index 58% rename from lms/templates/simplewiki_error.html rename to lms/templates/simplewiki/simplewiki_error.html index 6244b99dcf..0ce0763def 100644 --- a/lms/templates/simplewiki_error.html +++ b/lms/templates/simplewiki/simplewiki_error.html @@ -3,7 +3,7 @@ <%inherit file="simplewiki_base.html"/> <%! - from django.core.urlresolvers import reverse + from simplewiki.views import wiki_reverse %> <%block name="title">Wiki Error – MITx 6.002x @@ -21,30 +21,17 @@ ${wiki_error} %endif %if wiki_err_notfound is not UNDEFINED: - %if wiki_url is not UNDEFINED: -

    - The page you requested could not be found. - Click here to create it. -

    - %else: -

    - Or maybe rather: Congratulations! It seems that there's no root - article, which is probably because you just installed simple-wiki - and your installation is working. Now you can create the root article. - Click here to create it. -

    - %endif -%else: -%if wiki_err_noparent is not UNDEFINED:

    - You cannot create this page, because its parent - does not exist. Click here - to create it. + The page you requested could not be found. + Click here to create it.

    -%else: -%if wiki_err_keyword is not UNDEFINED and wiki_err_keyword: +%elif wiki_err_no_namespace is not UNDEFINED and wiki_err_no_namespace:

    - The page you're trying to create ${wiki_url} starts with _, which is reserved for internal use. + You must specify a namespace to create an article in. +

    +%elif wiki_err_bad_namespace is not UNDEFINED and wiki_err_bad_namespace: +

    + The namespace for this article does not exist. This article cannot be created.

    %elif wiki_err_locked is not UNDEFINED and wiki_err_locked:

    @@ -75,7 +62,7 @@ ${wiki_error}

    %elif wiki_err_deleted is not UNDEFINED and wiki_err_deleted:

    - The article you tried to access has been deleted. You may be able to restore it to an earlier version in its history, or create a new version. + The article you tried to access has been deleted. You may be able to restore it to an earlier version in its history, or create a new version.

    %elif wiki_err_norevision is not UNDEFINED:

    @@ -86,8 +73,6 @@ ${wiki_error} An error has occured.

    %endif -%endif -%endif diff --git a/lms/templates/simplewiki_history.html b/lms/templates/simplewiki/simplewiki_history.html similarity index 90% rename from lms/templates/simplewiki_history.html rename to lms/templates/simplewiki/simplewiki_history.html index 65f9e71a33..0fc77eeb0c 100644 --- a/lms/templates/simplewiki_history.html +++ b/lms/templates/simplewiki/simplewiki_history.html @@ -6,6 +6,7 @@ <%! from django.core.urlresolvers import reverse + from simplewiki.views import wiki_reverse %> <%block name="wiki_page_title"> @@ -64,10 +65,10 @@ ${ wiki_article.title } %if wiki_prev_page: - Previous page + Previous page %endif %if wiki_next_page: - Next page + Next page %endif diff --git a/lms/templates/simplewiki_instructions.html b/lms/templates/simplewiki/simplewiki_instructions.html similarity index 100% rename from lms/templates/simplewiki_instructions.html rename to lms/templates/simplewiki/simplewiki_instructions.html diff --git a/lms/templates/simplewiki_revision_feed.html b/lms/templates/simplewiki/simplewiki_revision_feed.html similarity index 75% rename from lms/templates/simplewiki_revision_feed.html rename to lms/templates/simplewiki/simplewiki_revision_feed.html index b9efd77b45..69b69afdff 100644 --- a/lms/templates/simplewiki_revision_feed.html +++ b/lms/templates/simplewiki/simplewiki_revision_feed.html @@ -5,7 +5,7 @@ <%block name="title">Wiki - Revision feed - MITx 6.002x <%! - from django.core.urlresolvers import reverse + from simplewiki.views import wiki_reverse %> <%block name="wiki_page_title"> @@ -29,7 +29,7 @@ <% loopCount += 1 %> - ${revision.article.title} - ${revision} + ${revision.article.title} - ${revision} ${ revision.revision_text if revision.revision_text else "None" } @@ -50,10 +50,10 @@ %if wiki_prev_page: - Previous page + Previous page %endif %if wiki_next_page: - Next page + Next page %endif diff --git a/lms/templates/simplewiki_searchresults.html b/lms/templates/simplewiki/simplewiki_searchresults.html similarity index 80% rename from lms/templates/simplewiki_searchresults.html rename to lms/templates/simplewiki/simplewiki_searchresults.html index f9866b3fae..d94cbf9c25 100644 --- a/lms/templates/simplewiki_searchresults.html +++ b/lms/templates/simplewiki/simplewiki_searchresults.html @@ -5,7 +5,7 @@ <%block name="title">Wiki - Search Results - MITx 6.002x <%! -from django.core.urlresolvers import reverse + from simplewiki.views import wiki_reverse %> <%block name="wiki_page_title"> @@ -23,7 +23,7 @@ Displaying all articles
      %for article in wiki_search_results: <% article_deleted = not article.current_revision.deleted == 0 %> -
    • ${article.title} ${'(Deleted)' if article_deleted else ''}

    • +
    • ${article.title} ${'(Deleted)' if article_deleted else ''}

    • %endfor %if not wiki_search_results: diff --git a/lms/templates/simplewiki_updateprogressbar.html b/lms/templates/simplewiki/simplewiki_updateprogressbar.html similarity index 100% rename from lms/templates/simplewiki_updateprogressbar.html rename to lms/templates/simplewiki/simplewiki_updateprogressbar.html diff --git a/lms/templates/simplewiki_view.html b/lms/templates/simplewiki/simplewiki_view.html similarity index 100% rename from lms/templates/simplewiki_view.html rename to lms/templates/simplewiki/simplewiki_view.html diff --git a/lms/templates/simplewiki_edit.html b/lms/templates/simplewiki_edit.html deleted file mode 100644 index 434a23e8bb..0000000000 --- a/lms/templates/simplewiki_edit.html +++ /dev/null @@ -1,77 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title"> - -%if create_article: -Wiki – Create Article – MITx 6.002x -%else: -${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002x Wiki -%endif - - -<%block name="wiki_page_title"> -%if create_article: -

      Create article

      -%else: -

      ${ wiki_article.title }

      -%endif - - -<%block name="wiki_head"> - - - - - - - - - - -<%block name="wiki_body"> -
      -
      - -
      - ${wiki_form} - %if create_article: - - %else: - - - %endif -
      - -<%include file="simplewiki_instructions.html"/> - - diff --git a/lms/urls.py b/lms/urls.py index e3427533c2..328efacc13 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -52,7 +52,6 @@ if settings.PERFSTATS: if settings.COURSEWARE_ENABLED: urlpatterns += ( - url(r'^wiki/', include('simplewiki.urls')), url(r'^masquerade/', include('masquerade.urls')), url(r'^jumpto/(?P[^/]+)/$', 'courseware.views.jump_to'), url(r'^modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), @@ -80,6 +79,12 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$', 'student.views.course_info', name="about_course"), ) + + # Multicourse wiki + urlpatterns += ( + url(r'^wiki/', include('simplewiki.urls')), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/wiki/', include('simplewiki.urls')), + ) if settings.ENABLE_MULTICOURSE: urlpatterns += (url(r'^mitxhome$', 'multicourse.views.mitxhome'),)