views.py 20.5 KB
Newer Older
Piotr Mitros committed
1
# -*- coding: utf-8 -*-
2
from django.conf import settings as settings
3 4
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
Piotr Mitros committed
5 6
from django.core.urlresolvers import reverse
from django.db.models import Q
7
from django.http import HttpResponse, HttpResponseRedirect, Http404
8 9
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
Calen Pennington committed
10
from mitxmako.shortcuts import render_to_response
11

12
from courseware.courses import check_course
13 14
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
15

16
from models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm
17
import wiki_settings
18 19 20 21


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'}
22 23 24 25 26
    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:
27 28
        kwargs['namespace'] = namespace
    return reverse(wiki_page, kwargs=kwargs, args=args)
29 30 31


def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None):
32 33
    if article:
        dictionary['wiki_article'] = article
34
        dictionary['wiki_title'] = article.title  # TODO: What is the title when viewing the article in a course?
35 36
        if not course and 'namespace' not in dictionary:
            dictionary['namespace'] = article.namespace.name
37

38 39 40 41 42 43
    if course:
        dictionary['course'] = course
        if 'namespace' not in dictionary:
            dictionary['namespace'] = course.wiki_namespace
    else:
        dictionary['course'] = None
44

45 46 47
    if revision:
        dictionary['wiki_article_revision'] = revision
        dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0)
48

49 50
    if request:
        dictionary.update(csrf(request))
51 52


53 54
def view(request, article_path, course_id=None):
    course = check_course(course_id, course_required=False)
55 56

    (article, err) = get_article(request, article_path, course)
Piotr Mitros committed
57 58
    if err:
        return err
59

60
    perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True)
Piotr Mitros committed
61 62
    if perm_err:
        return perm_err
63

64 65
    d = {}
    update_template_dictionary(d, request, course, article, article.current_revision)
66
    return render_to_response('simplewiki/simplewiki_view.html', d)
67 68


69 70
def view_revision(request, revision_number, article_path, course_id=None):
    course = check_course(course_id, course_required=False)
71 72

    (article, err) = get_article(request, article_path, course)
73 74
    if err:
        return err
75

76 77 78
    try:
        revision = Revision.objects.get(counter=int(revision_number), article=article)
    except:
79 80
        d = {'wiki_err_norevision': revision_number}
        update_template_dictionary(d, request, course, article)
81
        return render_to_response('simplewiki/simplewiki_error.html', d)
82

83
    perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision)
84 85
    if perm_err:
        return perm_err
86

87 88
    d = {}
    update_template_dictionary(d, request, course, article, revision)
89

90
    return render_to_response('simplewiki/simplewiki_view.html', d)
91

92

93 94
def root_redirect(request, course_id=None):
    course = check_course(course_id, course_required=False)
95

96 97
    #TODO: Add a default namespace to settings.
    namespace = course.wiki_namespace if course else "edX"
98

Piotr Mitros committed
99
    try:
100
        root = Article.get_root(namespace)
101
        return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()}))
Piotr Mitros committed
102
    except:
103 104
        # 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.
105
        Namespace.ensure_namespace(namespace)
106

107
        err = not_found(request, namespace + '/', course)
Piotr Mitros committed
108 109
        return err

110 111

def create(request, article_path, course_id=None):
112
    course = check_course(course_id, course_required=False)
113

114
    article_path_components = article_path.split('/')
Piotr Mitros committed
115

116 117
    # Ensure the namespace exists
    if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0:
118 119
        d = {'wiki_err_no_namespace': True}
        update_template_dictionary(d, request, course)
120
        return render_to_response('simplewiki/simplewiki_error.html', d)
121

122 123
    namespace = None
    try:
124
        namespace = Namespace.objects.get(name__exact=article_path_components[0])
125
    except Namespace.DoesNotExist, ValueError:
126 127
        d = {'wiki_err_bad_namespace': True}
        update_template_dictionary(d, request, course)
128
        return render_to_response('simplewiki/simplewiki_error.html', d)
129

130 131 132
    # 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)
133

134
    try:
135
        existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug)
136
        #It already exists, so we just redirect to view the article
137
        return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course))
138 139 140
    except Article.DoesNotExist:
        #This is good. The article doesn't exist
        pass
141

142 143
    #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)
Piotr Mitros committed
144 145 146 147 148

    if request.method == 'POST':
        f = CreateArticleForm(request.POST)
        if f.is_valid():
            article = Article()
149
            article.slug = article_slug
Piotr Mitros committed
150 151 152
            if not request.user.is_anonymous():
                article.created_by = request.user
            article.title = f.cleaned_data.get('title')
153
            article.namespace = namespace
Piotr Mitros committed
154 155 156 157 158 159
            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()
160

161
            return HttpResponseRedirect(wiki_reverse("wiki_view", article, course))
Piotr Mitros committed
162
    else:
163 164 165 166
        f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug),
                                       'contents': _('Headline\n===\n\n')})

    d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name}
167
    update_template_dictionary(d, request, course)
Piotr Mitros committed
168

169
    return render_to_response('simplewiki/simplewiki_edit.html', d)
Piotr Mitros committed
170

171

172 173
def edit(request, article_path, course_id=None):
    course = check_course(course_id, course_required=False)
174 175

    (article, err) = get_article(request, article_path, course)
Piotr Mitros committed
176 177 178 179
    if err:
        return err

    # Check write permissions
180
    perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False)
Piotr Mitros committed
181 182 183
    if perm_err:
        return perm_err

184
    if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
Piotr Mitros committed
185 186 187
        EditForm = RevisionFormWithTitle
    else:
        EditForm = RevisionForm
188

Piotr Mitros committed
189 190 191 192 193
    if request.method == 'POST':
        f = EditForm(request.POST)
        if f.is_valid():
            new_revision = f.save(commit=False)
            new_revision.article = article
194

195
            if request.POST.__contains__('delete'):
196
                if (article.current_revision.deleted == 1):  # This article has already been deleted. Redirect
197
                    return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
198
                new_revision.contents = ""
199 200
                new_revision.deleted = 1
            elif not new_revision.get_diff():
201
                return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
202

Piotr Mitros committed
203 204 205
            if not request.user.is_anonymous():
                new_revision.revision_user = request.user
            new_revision.save()
206
            if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
Piotr Mitros committed
207 208
                new_revision.article.title = f.cleaned_data['title']
                new_revision.article.save()
209
            return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
Piotr Mitros committed
210
    else:
211
        startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n'
212

213
        f = EditForm({'contents': startContents, 'title': article.title})
214

215 216
    d = {'wiki_form': f}
    update_template_dictionary(d, request, course, article)
217
    return render_to_response('simplewiki/simplewiki_edit.html', d)
Piotr Mitros committed
218

219

220 221
def history(request, article_path, page=1, course_id=None):
    course = check_course(course_id, course_required=False)
222 223

    (article, err) = get_article(request, article_path, course)
Piotr Mitros committed
224 225 226
    if err:
        return err

227
    perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False)
Piotr Mitros committed
228 229 230 231
    if perm_err:
        return perm_err

    page_size = 10
232

233 234
    if page == None:
        page = 1
Piotr Mitros committed
235 236 237 238
    try:
        p = int(page)
    except ValueError:
        p = 1
239 240 241

    history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article')

Piotr Mitros committed
242
    if request.method == 'POST':
243
        if request.POST.__contains__('revision'):  # They selected a version, but they can be either deleting or changing the version
244
            perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
Piotr Mitros committed
245 246
            if perm_err:
                return perm_err
247

248
            redirectURL = wiki_reverse('wiki_view', article, course)
Piotr Mitros committed
249 250
            try:
                r = int(request.POST['revision'])
251 252 253 254
                revision = Revision.objects.get(id=r)
                if request.POST.__contains__('change'):
                    article.current_revision = revision
                    article.save()
255
                elif request.POST.__contains__('view'):
256
                    redirectURL = wiki_reverse('wiki_view_revision', course=course,
257
                                    kwargs={'revision_number': revision.counter, 'article_path': article.get_path()})
258
                #The rese of these are admin functions
259 260 261 262 263 264 265
                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:
266
                    Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2)
267 268 269
                elif request.POST.__contains__('lock_article'):
                    article.locked = not article.locked
                    article.save()
270 271
            except Exception as e:
                print str(e)
Piotr Mitros committed
272 273
                pass
            finally:
274
                return HttpResponseRedirect(redirectURL)
275 276
                #
                #
277 278 279 280 281
                # <input type="submit" name="delete" value="Delete revision"/>
                # <input type="submit" name="restore" value="Restore revision"/>
                # <input type="submit" name="delete_all" value="Delete all revisions">
                # %else:
                # <input type="submit" name="delete_article" value="Delete all revisions">
282 283 284
                #

    page_count = (history.count() + (page_size - 1)) / page_size
Piotr Mitros committed
285 286
    if p > page_count:
        p = 1
287 288
    beginItem = (p - 1) * page_size

Piotr Mitros committed
289 290
    next_page = p + 1 if page_count > p else None
    prev_page = p - 1 if p > 1 else None
291

292
    d = {'wiki_page': p,
293 294 295 296
            'wiki_next_page': next_page,
            'wiki_prev_page': prev_page,
            'wiki_history': history[beginItem:beginItem + page_size],
            'show_delete_revision': request.user.is_superuser}
297
    update_template_dictionary(d, request, course, article)
298

299
    return render_to_response('simplewiki/simplewiki_history.html', d)
300 301


302 303
def revision_feed(request, page=1, namespace=None, course_id=None):
    course = check_course(course_id, course_required=False)
304

305
    page_size = 10
306

307 308
    if page == None:
        page = 1
309 310 311 312
    try:
        p = int(page)
    except ValueError:
        p = 1
313

314
    history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision')
315 316

    page_count = (history.count() + (page_size - 1)) / page_size
317 318
    if p > page_count:
        p = 1
319 320
    beginItem = (p - 1) * page_size

321 322
    next_page = p + 1 if page_count > p else None
    prev_page = p - 1 if p > 1 else None
323

324
    d = {'wiki_page': p,
325 326 327 328 329
            'wiki_next_page': next_page,
            'wiki_prev_page': prev_page,
            'wiki_history': history[beginItem:beginItem + page_size],
            'show_delete_revision': request.user.is_superuser,
            'namespace': namespace}
330
    update_template_dictionary(d, request, course)
331

332
    return render_to_response('simplewiki/simplewiki_revision_feed.html', d)
Piotr Mitros committed
333

334 335

def search_articles(request, namespace=None, course_id=None):
336
    course = check_course(course_id, course_required=False)
337

Piotr Mitros committed
338 339 340
    # 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.
341

Piotr Mitros committed
342
    # todo: maybe do some perm checking here
343

344 345
    if request.method == 'GET':
        querystring = request.GET.get('value', '').strip()
346 347
    else:
        querystring = ""
348

349 350
    results = Article.objects.all()
    if namespace:
351 352
        results = results.filter(namespace__name__exact=namespace)

353 354 355
    if request.user.is_superuser:
        results = results.order_by('current_revision__deleted')
    else:
356
        results = results.filter(current_revision__deleted=0)
357 358 359 360 361 362 363 364 365

    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)
366 367 368 369 370 371 372

            results = results._search(Q(current_revision__contents__icontains=queryword) | \
                                      Q(title__icontains=queryword))

    results = results.select_related('current_revision__deleted', 'namespace')

    results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower()))
373

374
    if len(results) == 1 and querystring:
375
        return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course))
376
    else:
377
        d = {'wiki_search_results': results,
378 379
                'wiki_search_query': querystring,
                'namespace': namespace}
380
        update_template_dictionary(d, request, course)
381
        return render_to_response('simplewiki/simplewiki_searchresults.html', d)
382 383


384 385
def search_add_related(request, course_id, slug, namespace):
    course = check_course(course_id, course_required=False)
386 387

    (article, err) = get_article(request, slug, namespace if namespace else course_id)
Piotr Mitros committed
388 389 390
    if err:
        return err

391
    perm_err = check_permissions(request, article, course, check_read=True)
Piotr Mitros committed
392 393 394 395 396 397 398
    if perm_err:
        return perm_err

    search_string = request.GET.get('query', None)
    self_pk = request.GET.get('self', None)
    if search_string:
        results = []
399
        related = Article.objects.filter(title__istartswith=search_string)
Piotr Mitros committed
400 401 402 403
        others = article.related.all()
        if self_pk:
            related = related.exclude(pk=self_pk)
        if others:
404
            related = related.exclude(related__in=others)
Piotr Mitros committed
405 406 407 408 409 410 411
        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 = []
412

Piotr Mitros committed
413 414 415
    json = simplejson.dumps({'results': results})
    return HttpResponse(json, mimetype='application/json')

416

417 418
def add_related(request, course_id, slug, namespace):
    course = check_course(course_id, course_required=False)
419 420

    (article, err) = get_article(request, slug, namespace if namespace else course_id)
Piotr Mitros committed
421 422
    if err:
        return err
423

424
    perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
Piotr Mitros committed
425 426
    if perm_err:
        return perm_err
427

Piotr Mitros committed
428 429 430 431 432 433 434 435 436 437 438 439
    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(),)))

440

441 442
def remove_related(request, course_id, namespace, slug, related_id):
    course = check_course(course_id, course_required=False)
443 444 445

    (article, err) = get_article(request, slug, namespace if namespace else course_id)

Piotr Mitros committed
446 447 448
    if err:
        return err

449
    perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
Piotr Mitros committed
450 451 452 453 454 455 456 457 458 459 460 461 462
    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(),)))

463

464 465
def random_article(request, course_id=None):
    course = check_course(course_id, course_required=False)
466

Piotr Mitros committed
467 468
    from random import randint
    num_arts = Article.objects.count()
469 470 471 472
    article = Article.objects.all()[randint(0, num_arts - 1)]
    return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))


473
def not_found(request, article_path, course):
Piotr Mitros committed
474
    """Generate a NOT FOUND message for some URL"""
475
    d = {'wiki_err_notfound': True,
476
         'article_path': article_path,
477
         'namespace': course.wiki_namespace}
478
    update_template_dictionary(d, request, course)
479
    return render_to_response('simplewiki/simplewiki_error.html', d)
480 481


482
def get_article(request, article_path, course):
Piotr Mitros committed
483 484
    err = None
    article = None
485

Piotr Mitros committed
486
    try:
487 488 489
        article = Article.get_article(article_path)
    except Article.DoesNotExist, ValueError:
        err = not_found(request, article_path, course)
490

491
    return (article, err)
Piotr Mitros committed
492

493 494

def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None):
Piotr Mitros committed
495
    read_err = check_read and not article.can_read(request.user)
496

Piotr Mitros committed
497
    write_err = check_write and not article.can_write(request.user)
498

Piotr Mitros committed
499
    locked_err = check_locked and article.locked
500

501
    if revision is None:
502 503
        revision = article.current_revision
    deleted_err = check_deleted and not (revision.deleted == 0)
504
    if (request.user.is_superuser):
505
        deleted_err = False
506
        locked_err = False
507

508
    if read_err or write_err or locked_err or deleted_err:
509
        d = {'wiki_article': article,
510 511 512 513
                'wiki_err_noread': read_err,
                'wiki_err_nowrite': write_err,
                'wiki_err_locked': locked_err,
                'wiki_err_deleted': deleted_err, }
514
        update_template_dictionary(d, request, course)
Piotr Mitros committed
515 516 517 518
        # 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.
519
        return render_to_response('simplewiki/simplewiki_error.html', d)
Piotr Mitros committed
520 521 522 523 524 525 526
    else:
        return None

####################
# LOGIN PROTECTION #
####################

527

528
if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW:
529 530 531 532 533 534
    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)
535
    search_add_related = login_required(search_add_related)
536 537 538
    not_found = login_required(not_found)
    view_revision = login_required(view_revision)

539
if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT:
540 541 542 543
    create = login_required(create)
    edit = login_required(edit)
    add_related = login_required(add_related)
    remove_related = login_required(remove_related)
Piotr Mitros committed
544

545 546
if wiki_settings.WIKI_CONTEXT_PREPROCESSORS:
    settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS