src/cm/views/texts.py
changeset 0 40c8f766c9b8
child 12 f69ff46d3240
equal deleted inserted replaced
-1:000000000000 0:40c8f766c9b8
       
     1 from cm.activity import register_activity
       
     2 from cm.client import jsonize, get_filter_datas, edit_comment, remove_comment, \
       
     3     add_comment, RequestComplexEncoder, comments_thread
       
     4 from cm.cm_settings import NEW_TEXT_VERSION_ON_EDIT
       
     5 from cm.diff import text_diff as _text_diff, text_history as inner_text_history, \
       
     6     get_colors
       
     7 from cm.exception import UnauthorizedException
       
     8 from cm.message import *
       
     9 from cm.models import *
       
    10 from cm.models_base import generate_key
       
    11 from cm.security import get_texts_with_perm, has_perm, get_viewable_comments, \
       
    12     has_perm_on_text
       
    13 from cm.utils import get_among, get_among, get_int
       
    14 from cm.utils.comment_positioning import compute_new_comment_positions, \
       
    15     insert_comment_markers
       
    16 from cm.utils.html import cleanup_textarea
       
    17 from cm.utils.spannifier import spannify
       
    18 from cm.views import get_keys_from_dict, get_text_by_keys_or_404, redirect
       
    19 from cm.views.export import content_export2
       
    20 from cm.views.user import AnonUserRoleForm, cm_login
       
    21 from difflib import unified_diff
       
    22 from django import forms
       
    23 from django.conf import settings
       
    24 from django.contrib.auth import login as django_login
       
    25 from django.contrib.auth.forms import AuthenticationForm
       
    26 from django.contrib.auth.models import User
       
    27 from django.core.urlresolvers import reverse
       
    28 from django.db.models import Q
       
    29 from django.forms import ModelForm
       
    30 from django.forms.models import BaseModelFormSet, modelformset_factory
       
    31 from django.http import HttpResponse, HttpResponseRedirect, Http404
       
    32 from django.shortcuts import render_to_response
       
    33 from django.template import RequestContext
       
    34 from django.template.loader import render_to_string
       
    35 from django.utils.translation import ugettext as _, ugettext_lazy
       
    36 from django.views.generic.list_detail import object_list
       
    37 import difflib
       
    38 import logging
       
    39 import mimetypes
       
    40 import simplejson
       
    41 import sys
       
    42 
       
    43 
       
    44 def get_text_and_admin(key, adminkey, assert_admin = False):
       
    45     """
       
    46     assert_admin => redirect to unauthorized if not admin 
       
    47     """
       
    48     admin = False
       
    49     if adminkey:
       
    50         text = Text.objects.get(key = key, adminkey = adminkey)
       
    51         if text:
       
    52             admin = True
       
    53     else:
       
    54         text = Text.objects.get(key=key)
       
    55     if assert_admin and not admin:
       
    56         raise UnauthorizedException('Is not admin')        
       
    57     return text, admin
       
    58 
       
    59 
       
    60 
       
    61 ACTIVITY_PAGINATION = 10
       
    62 RECENT_TEXT_NB = 5
       
    63 RECENT_COMMENT_NB = RECENT_TEXT_NB
       
    64 
       
    65 MODERATE_NB = 5
       
    66 
       
    67 def dashboard(request):
       
    68     request.session.set_test_cookie()
       
    69     if request.user.is_authenticated():
       
    70         act_view = {
       
    71                     'view_texts' : get_int(request.GET,'view_texts',1),
       
    72                     'view_comments' : get_int(request.GET,'view_comments',1),
       
    73                     'view_users' : get_int(request.GET,'view_users',1),
       
    74                     }
       
    75             
       
    76         paginate_by = get_int(request.GET,'paginate',ACTIVITY_PAGINATION)
       
    77                 
       
    78         # texts with can_view_unapproved_comment perms
       
    79         moderator_texts = get_texts_with_perm(request, 'can_view_unapproved_comment')
       
    80         viewer_texts = get_texts_with_perm(request, 'can_view_approved_comment')
       
    81         all_texts_ids = [t.id for t in moderator_texts] + [t.id for t in viewer_texts]
       
    82                     
       
    83         span = get_among(request.GET,'span',('day','month','week',),'week')        
       
    84         template_dict = { 
       
    85                          'span' : span,
       
    86                          'last_texts' : get_texts_with_perm(request, 'can_view_text').order_by('-modified')[:RECENT_TEXT_NB],
       
    87                          'last_comments' : Comment.objects.filter(text_version__text__in=all_texts_ids).order_by('-created')[:RECENT_COMMENT_NB],# TODO: useful?
       
    88                          #'last_users' : User.objects.all().order_by('-date_joined')[:5],
       
    89                          }
       
    90         template_dict.update(act_view)
       
    91         
       
    92         all_activities = {
       
    93                                'view_comments' : ['comment_created','comment_removed'],
       
    94                                'view_users' : ['user_created', 'user_activated', 'user_suspended','user_enabled',],
       
    95                                'view_texts' : ['text_created','text_removed', 'text_edited', 'text_edited_new_version'],
       
    96                                }
       
    97         
       
    98         selected_activities = []
       
    99         [selected_activities.extend(all_activities[k]) for k in act_view.keys() if act_view[k]]
       
   100         
       
   101         activities = Activity.objects.filter(type__in = selected_activities)
       
   102         if not has_perm(request,'can_manage_workspace'):
       
   103             texts = get_texts_with_perm(request, 'can_view_text')
       
   104             activities = activities.filter(Q(text__in=texts))
       
   105             
       
   106             comments = [] 
       
   107             [comments.extend(get_viewable_comments(request, t.last_text_version.comment_set.all(), t)) for t in texts]
       
   108 
       
   109             activities = activities.filter(Q(comment__in=comments) | Q(comment=None) )
       
   110             template_dict['to_mod_profiles'] = []
       
   111         else:
       
   112             template_dict['to_mod_profiles'] = UserProfile.objects.filter(user__is_active=False).filter(is_suspended=True).order_by('-user__date_joined')[:MODERATE_NB]
       
   113         template_dict['to_mod_comments'] = Comment.objects.filter(state='pending').filter(text_version__text__in=moderator_texts).order_by('-modified')[:MODERATE_NB-len(template_dict['to_mod_profiles'])]
       
   114 
       
   115         activities = activities.order_by('-created')
       
   116         return object_list(request, activities,
       
   117                            template_name = 'site/dashboard.html',
       
   118                            paginate_by = paginate_by,
       
   119                            extra_context = template_dict,
       
   120                            )
       
   121         
       
   122     else:
       
   123         if request.method == 'POST':
       
   124             form = AuthenticationForm(request, request.POST)
       
   125             if form.is_valid():
       
   126                 user = form.get_user()
       
   127                 user.backend = 'django.contrib.auth.backends.ModelBackend'
       
   128                 cm_login(request, user)            
       
   129                 display_message(request, _(u"You're logged in!"))
       
   130                 return HttpResponseRedirect(reverse('index'))
       
   131         else:
       
   132             form = AuthenticationForm()        
       
   133 
       
   134 
       
   135         public_texts = get_texts_with_perm(request, 'can_view_text').order_by('-modified')
       
   136 
       
   137         template_dict = {
       
   138                          'form' : form,
       
   139                          'texts' : public_texts,
       
   140                          }
       
   141         return render_to_response('site/non_authenticated_index.html', template_dict, context_instance=RequestContext(request))
       
   142         
       
   143 TEXT_PAGINATION = 10
       
   144 # security check inside view
       
   145 # TODO: set global access perm
       
   146 def text_list(request):
       
   147     paginate_by = get_int(request.GET,'paginate',TEXT_PAGINATION)
       
   148     
       
   149     order_by = get_among(request.GET,'order',('title','author','modified','-title','-author','-modified'),'-modified')
       
   150 
       
   151     if request.method == 'POST':
       
   152         action = request.POST.get('action',None)
       
   153         text_keys = get_keys_from_dict(request.POST, 'check-').keys()        
       
   154         if action == 'delete':
       
   155             for text_key in text_keys:
       
   156                 text = Text.objects.get(key=text_key)
       
   157                 if has_perm(request, 'can_delete_text', text=text):
       
   158                     text.delete()
       
   159                 else:
       
   160                     raise UnauthorizedException('No perm can_delete_text on comment') 
       
   161             display_message(request, _(u'%(nb_texts)i text(s) deleted') %{'nb_texts':len(text_keys)})
       
   162             return HttpResponseRedirect(reverse('text'))
       
   163 
       
   164     texts = get_texts_with_perm(request, 'can_view_text').order_by(order_by)
       
   165     return object_list(request, texts,
       
   166                        template_name = 'site/text_list.html',
       
   167                        paginate_by = paginate_by,
       
   168                        )
       
   169     
       
   170 @has_perm_on_text('can_view_text')
       
   171 def text_view(request, key, adminkey=None):
       
   172     
       
   173     text = get_text_by_keys_or_404(key)
       
   174     register_activity(request, "text_view", text=text)    
       
   175     
       
   176     text_version = text.get_latest_version()
       
   177     template_dict = { 'text' : text, 'text_version' : text_version, 'title' : text_version.title, 'content' : text_version.get_content()}
       
   178     return render_to_response('site/text_view.html', template_dict, context_instance=RequestContext(request))
       
   179 
       
   180 @has_perm_on_text('can_delete_text')
       
   181 def text_delete(request, key):
       
   182     text = Text.objects.get(key=key)
       
   183     if request.method != 'POST':
       
   184         raise UnauthorizedException('Unauthorized')
       
   185     display_message(request, _(u'Text %(text_title)s deleted') %{'text_title':text.title})
       
   186     register_activity(request, "text_removed", text=text)    
       
   187     text.delete()
       
   188     return HttpResponse('') # no redirect because this is called by js
       
   189 
       
   190 @has_perm_on_text('can_view_text') # only protected by text_view / comment filtering done in view
       
   191 def text_view_comments(request, key, adminkey=None):
       
   192     text = get_text_by_keys_or_404(key)
       
   193     #TODO: stupid why restrict to latest ? 
       
   194     text_version = text.get_latest_version()
       
   195     comments = get_viewable_comments(request, text_version.comment_set.filter(reply_to__isnull=True),text)
       
   196     filter_datas = get_filter_datas(request, text_version, text)
       
   197     
       
   198     get_params = simplejson.dumps(request.GET)
       
   199     
       
   200     wrapped_text_version, _ , _ = spannify(text_version.get_content())
       
   201     template_dict = {'text' : text,
       
   202                                'text_version' : text_version,
       
   203                                'title' : text_version.title, # TODO use it ...
       
   204                                'get_params' : get_params,
       
   205                                'json_comments':jsonize(comments, request),
       
   206                                'json_filter_datas':jsonize(filter_datas, request),
       
   207                                'content' : wrapped_text_version,
       
   208                                'client_date_fmt' : settings.CLIENT_DATE_FMT,
       
   209                                }
       
   210     return render_to_response('site/text_view_comments.html',
       
   211                               template_dict,
       
   212                               context_instance=RequestContext(request))
       
   213 def client_exchange(request):
       
   214     ret = None
       
   215     function_name = request.POST['fun']# function called from client
       
   216     user = request.user
       
   217     if function_name == 'experiment' :
       
   218         ret = experiment()
       
   219     elif function_name == 'warn' :
       
   220 # TODO: (RBE to RBA) send mail withinfos
       
   221         ret = "warn test"
       
   222         #print request.POST
       
   223     elif request.POST:
       
   224         key = request.POST['key']
       
   225 
       
   226         text = Text.objects.get(key=key) ;
       
   227         #TODO: stupid why restrict to latest ? 
       
   228         text_version = text.get_latest_version()
       
   229         
       
   230         if (text != None) :
       
   231             if function_name in ('editComment', 'addComment', 'removeComment',) :
       
   232                 if function_name == 'editComment' :
       
   233                     ret = edit_comment(request=request, key=key, comment_key=request.POST['comment_key'])
       
   234                 elif function_name == 'addComment' :
       
   235                     ret = add_comment(request=request, key=key)
       
   236                 elif function_name == 'removeComment' :
       
   237                     ret = remove_comment(request=request, key=key, comment_key=request.POST['comment_key'])
       
   238                     
       
   239                 ret['filterData'] = get_filter_datas(request, text_version, text)
       
   240                 #ret['tagCloud'] = get_tagcloud(key)
       
   241     if ret :
       
   242         if type(ret) != HttpResponseRedirect :
       
   243             ret = HttpResponse(simplejson.dumps(ret, cls=RequestComplexEncoder, request=request))        
       
   244     else :
       
   245         ret = HttpResponse(simplejson.dumps({}))
       
   246         ret.status_code = 403
       
   247 
       
   248     return ret 
       
   249 
       
   250 
       
   251 #NOTE : some arguments like : withcolor = "yes" + format = "markdown" are incompatible
       
   252 #http://localhost:8000/text/text_key_1/export/pdf/1/all/1
       
   253 def text_export(request, key, format, download, whichcomments, withcolor, adminkey=None):
       
   254     text, admin = get_text_and_admin(key, adminkey)
       
   255     text_version = text.get_latest_version()
       
   256     original_content = text_version.content
       
   257     original_format = text_version.format # BD : html or markdown for  now ...
       
   258 
       
   259     download_response = download == "1"
       
   260     with_color = withcolor == "1"
       
   261     
       
   262     comments = [] # whichcomments=="none"
       
   263     
       
   264     if whichcomments == "filtered" or whichcomments == "all":
       
   265         #comments = text_version.comment_set.filter(reply_to__isnull=True)# whichcomments=="all"
       
   266         #comments = get_viewable_comments(request, text_version.comment_set.filter(reply_to__isnull=True), text, order_by=('start_wrapper','start_offset','end_wrapper','end_offset'))# whichcomments=="all"
       
   267         comments = get_viewable_comments(request, text_version.comment_set.all(), text, order_by=('start_wrapper','start_offset','end_wrapper','end_offset'))# whichcomments=="all"
       
   268  
       
   269         if whichcomments == "filtered" :
       
   270             if request.method == 'POST' :
       
   271                 ll = request.POST.get('filteredIds',[]).split(",")
       
   272                 filteredIds = [ int(l) for l in ll if l]
       
   273                 comments = comments.filter(id__in=filteredIds) # security ! TODO CROSS PERMISSIONS WITH POST CONTENT
       
   274             else :
       
   275                 comments = []
       
   276 
       
   277     if len(comments) == 0 : #want to bypass html conversion in this case
       
   278         return content_export2(request, original_content, text_version.title, original_format, format, False, download_response)
       
   279     else : # case comments to be added  
       
   280         #comments = comments.order_by('start_wrapper','start_offset','end_wrapper','end_offset')
       
   281         html = text_version.get_content()
       
   282         wrapped_text_version, _ , _ = spannify(html)
       
   283         with_markers = True
       
   284         marked_content = insert_comment_markers(wrapped_text_version, comments, with_markers, with_color)
       
   285 
       
   286         viewable_comments = comments_thread(request, text_version, text) 
       
   287 #        viewable_commentsnoreply = get_viewable_comments(request, commentsnoreply, text, order_by = ('start_wrapper','start_offset','end_wrapper','end_offset'))
       
   288 #        viewable_comments = []
       
   289 #        for cc in viewable_commentsnoreply :
       
   290 #            viewable_comments += list_viewable_comments(request, [cc], text)
       
   291             
       
   292         # numerotation{  id --> numbered as a child}
       
   293         extended_comments = {}
       
   294         nb_children = {}
       
   295         for cc in viewable_comments :
       
   296             id = 0 #<-- all top comments are children of comment with id 0
       
   297             if cc.is_reply() :
       
   298                 id = cc.reply_to_id
       
   299                 
       
   300             nb_children[id] = nb_children.get(id, 0) + 1
       
   301             
       
   302             cc.num = "%d"%nb_children[id]
       
   303             
       
   304             extended_comments[cc.id] = cc
       
   305         
       
   306             if cc.is_reply() :
       
   307                 cc.num = "%s.%s"%(extended_comments[cc.reply_to_id].num, cc.num)
       
   308         
       
   309 #        viewable_comments += list_viewable_comments(request, viewable_commentsnoreply, text)
       
   310         html_comments=render_to_string('site/macros/text_comments.html',{'comments':viewable_comments }, context_instance=RequestContext(request))
       
   311         
       
   312         content = "%s%s"%(marked_content, html_comments)
       
   313         content_format = "html" 
       
   314         # impossible to satisfy because of color then no colors instead:
       
   315         if with_color and format in ('markdown', 'tex') : #TODO : add S5
       
   316             with_color = False  
       
   317 
       
   318         # decide to use pandoc or not
       
   319         if with_color :
       
   320             use_pandoc = False  # pandoc wouldn't preserve comments scope background colors
       
   321         else :
       
   322             if format in ('markdown', 'tex') : 
       
   323                 use_pandoc = True
       
   324             elif format in ('pdf', 'odt') : 
       
   325                 use_pandoc = (original_format == "markdown")
       
   326             elif format in ('doc', 'html') : 
       
   327                 use_pandoc = False
       
   328         
       
   329         return content_export2(request, content, text_version.title, content_format, format, use_pandoc, download_response)
       
   330 
       
   331 def text_print(request, key, adminkey=None):
       
   332     text, admin = get_text_and_admin(key, adminkey)
       
   333     
       
   334     text_version = text.get_latest_version()
       
   335 
       
   336 #    chosen default url behaviour is export all comments + bckcolor + pdf  
       
   337     comments = Comment.objects.filter(text_version=text_version, reply_to__isnull=True)
       
   338 
       
   339     with_markers = True
       
   340     with_colors = True 
       
   341     download_requested = True   
       
   342     action = 'export' # client origine dialog
       
   343     requested_format = 'pdf' # requested output format
       
   344 
       
   345     if request.method == 'POST':
       
   346         # colors or not ?
       
   347         with_colors = (u'yes' == request.POST.get('p_color', u'no'))
       
   348 
       
   349         # restrict comments to ones that should be exported / printed          
       
   350         p_comments = request.POST.get('p_comments')
       
   351         if p_comments == "filtered" or p_comments == "none" : 
       
   352             filteredIds = [] # "none" case  
       
   353             if p_comments == "filtered" : 
       
   354                 ll = request.POST.get('filteredIds').split(",")
       
   355                 filteredIds = [ int(l) for l in ll if l]
       
   356   
       
   357             comments = comments.filter(id__in=filteredIds)
       
   358 
       
   359         # print or export ?
       
   360         action = request.POST.get('print_export_action')
       
   361         requested_format = request.POST.get('p_method')
       
   362 
       
   363     comments = comments.order_by('start_wrapper','start_offset','end_wrapper','end_offset')
       
   364 
       
   365     download_requested = (action == 'export') or (action == 'print' and requested_format != 'html')   
       
   366 
       
   367     ori_format = text_version.format # BD : html or markdown for  now ...
       
   368     src_format = ori_format # as expected by convert functions ...
       
   369     src = text_version.content
       
   370     
       
   371     if len(comments) > 0 and (with_markers or with_colors) :
       
   372         html = text_version.get_content()
       
   373         wrapped_text_version, _ , _ = spannify(html)
       
   374         marked_text_version = insert_comment_markers(wrapped_text_version, comments, with_markers, with_colors)
       
   375 
       
   376         src_format = 'html'
       
   377         src = marked_text_version
       
   378         html_comments=render_to_string('site/macros/text_comments.html',{'comments':comments}, context_instance=RequestContext(request))
       
   379         src += html_comments
       
   380         
       
   381     if download_requested :
       
   382         use_pandoc = (requested_format == 'html' or requested_format == 'markdown')
       
   383         return content_export(request, src, text_version.title, src_format, requested_format, use_pandoc)
       
   384     else : # action == 'print' and requested_format == 'html' (no colors)
       
   385         template_dict = {'text' : text,
       
   386                        'text_version' : text_version,
       
   387                        'title' : text_version.title, # TODO use it ...
       
   388                        'comments': comments,
       
   389                        'content' : marked_text_version,
       
   390                        'client_date_fmt' : settings.CLIENT_DATE_FMT
       
   391                        }
       
   392         if admin:
       
   393              template_dict['adminkey'] = text.adminkey   
       
   394              template_dict['admin'] = True       
       
   395         return render_to_response('site/text_print.html',
       
   396                                   template_dict,
       
   397                                   context_instance=RequestContext(request))
       
   398 
       
   399 @has_perm_on_text('can_view_text')
       
   400 def text_view_frame(request, key, adminkey=None):
       
   401     text = get_text_by_keys_or_404(key)
       
   402     
       
   403     text_version = text.get_latest_version()
       
   404     template_dict = {'text' : text}
       
   405     return render_to_response('site/text_view_frame.html',
       
   406                               template_dict,
       
   407                               context_instance=RequestContext(request))
       
   408 
       
   409 
       
   410 @has_perm_on_text('can_view_text')
       
   411 def text_history(request, key, v1_nid=None, v2_nid=None, adminkey=False):
       
   412     text = get_text_by_keys_or_404(key)    
       
   413     text_versions = text.get_versions()
       
   414     author_colors = get_colors([t.get_name() for t in text.get_inversed_versions()])
       
   415     
       
   416     if v1_nid:
       
   417         v1_nid = int(v1_nid)
       
   418     else:
       
   419         v1_nid = text.get_versions_number()
       
   420         
       
   421     v1 = text.get_version(v1_nid)
       
   422 
       
   423     v1_id = v1.id
       
   424     
       
   425     v2_id = None
       
   426     v2 = None
       
   427     if v2_nid:
       
   428         v2_nid = int(v2_nid)
       
   429         v2 = text.get_version(v2_nid)
       
   430         v2_id = v2.id
       
   431 
       
   432     versions = text.get_inversed_versions()
       
   433     paired_versions = []
       
   434     colors_dict = dict(author_colors)
       
   435     for index in range(len(versions)):
       
   436         vv1 = versions[index]
       
   437         if index + 1 < len(versions):
       
   438             vv2 = versions[index + 1]
       
   439         else:
       
   440             vv2 = None
       
   441         paired_versions.append((vv1, vv2, colors_dict.get(vv1.get_name(), '#D9D9D9')))
       
   442 
       
   443     if v1_nid and not v2_nid:
       
   444         content = v1.get_content()
       
   445     else:
       
   446         content = get_uniffied_inner_diff_table(cleanup_textarea(v1.content), cleanup_textarea(v2.content))
       
   447 
       
   448     template_dict = {'paired_versions' : paired_versions,
       
   449                      'text' : text,
       
   450                      'v1_nid' : v1_nid,
       
   451                      'v2_nid' : v2_nid,
       
   452                      'v1_id' : v1_id,
       
   453                      'v2_id' : v2_id,
       
   454                      'version1': v1,
       
   455                      'version2': v2,
       
   456                      'content' : content,
       
   457                      'author_colors' : author_colors,
       
   458                      }
       
   459     return render_to_response('site/text_history.html', template_dict, context_instance=RequestContext(request))
       
   460 
       
   461 # taken from trac
       
   462 def _get_change_extent(str1, str2):
       
   463     """
       
   464     Determines the extent of differences between two strings. Returns a tuple
       
   465     containing the offset at which the changes start, and the negative offset
       
   466     at which the changes end. If the two strings have neither a common prefix
       
   467     nor a common suffix, (0, 0) is returned.
       
   468     """
       
   469     start = 0
       
   470     limit = min(len(str1), len(str2))
       
   471     while start < limit and str1[start] == str2[start]:
       
   472         start += 1
       
   473     end = -1
       
   474     limit = limit - start
       
   475     while - end <= limit and str1[end] == str2[end]:
       
   476         end -= 1
       
   477     return (start, end + 1)
       
   478 
       
   479 def diff_decorate(minus, plus):
       
   480     return minus, plus
       
   481 
       
   482 def get_uniffied_inner_diff_table(text1, text2):
       
   483     """
       
   484     Return the inner of the html table for text1 vs text2 diff
       
   485     """
       
   486     gen = unified_diff(text1.split('\n'), text2.split('\n'), n=3)
       
   487     index = 0
       
   488     res = ['<table class="diff"><tbody>']
       
   489     #res.append('<tr><td width="50%" colspan="2"></td><td width="50%" colspan="2"></td></tr>')
       
   490     
       
   491     for g in gen:
       
   492         if index > 1:
       
   493             col_in = None
       
   494             if g.startswith('@@'):
       
   495                 line_number = g.split(' ')[1][1:].split(',')[0]
       
   496                 if index != 2:
       
   497                     res.append('<tr><td></td>&nbsp;<td></td><td></td><td>&nbsp;</td></tr>')                    
       
   498                 res.append('<tr><td class="diff-lineno" colspan="2">Line %s</td><td class="diff-separator"></td><td class="diff-lineno" colspan="2">Line %s</td></tr>' % (line_number, line_number))
       
   499             if g.startswith(' '):
       
   500                 res.append('<tr><td class="diff-marker"></td><td class="diff-context">%s</td><td class="diff-separator"></td><td class="diff-marker"></td><td class="diff-context">%s</td></tr>' % (g, g))
       
   501             if g.startswith('-') or g.startswith('+'):
       
   502                 plus = []
       
   503                 minus = []
       
   504                 while g.startswith('-') or g.startswith('+'):
       
   505                     if g.startswith('-'):
       
   506                         minus.append(g[1:])
       
   507                     else:
       
   508                         plus.append(g[1:])
       
   509                     try:
       
   510                         g = gen.next()
       
   511                     except StopIteration:
       
   512                         break
       
   513                 minus, plus = diff_decorate(minus, plus)
       
   514                 res.append('<tr><td class="diff-marker">-</td><td class="diff-deletedline">%s</td><td class="diff-separator"></td><td class="diff-marker">+</td><td class="diff-addedline">%s</td></tr>' % ('<br />'.join(minus), '<br />'.join(plus)))
       
   515              
       
   516         index += 1
       
   517     res.append('</tbody></table>')
       
   518     return ''.join(res)
       
   519 
       
   520 @has_perm_on_text('can_view_text')
       
   521 def text_history_compare(request, key, v1_nid=None, v2_nid=None, adminkey=None):
       
   522     text = get_text_by_keys_or_404(key)
       
   523 
       
   524     vis_diff = difflib.HtmlDiff()
       
   525     v1 = text.get_version(int(v1_nid))
       
   526     v2 = text.get_version(int(v2_nid))
       
   527     content = _text_diff(v2.get_content(), v1.get_content())
       
   528     #content = vis_diff.make_table(v1.content.split('\n'), v2.content.split('\n'), v1_nid, v2_nid, context=None)
       
   529     
       
   530     template_dict = {
       
   531                      'text' : text,
       
   532                      'content' : content,
       
   533                      }
       
   534     return render_to_response('site/text_history_compare.html', template_dict, context_instance=RequestContext(request))
       
   535 
       
   536 #def text_history_version(request, key):
       
   537 #    text = get_text_by_keys_or_404(key=key)
       
   538 #    return _text_history_version(request, text)
       
   539 #
       
   540 #def text_history_version_admin(request, key, adminkey):
       
   541 #    text = get_text_by_keys_or_404(key=key, adminkey=adminkey)
       
   542 #    return _text_history_version(request, text, True)
       
   543 #    if admin:
       
   544 #         template_dict['adminkey'] = text.adminkey
       
   545 #         template_dict['admin'] = True          
       
   546 #    return render_to_response('site/text_history.html', template_dict, context_instance=RequestContext(request))
       
   547 #
       
   548 #def _text_history_version(request, text, admin=False):
       
   549 #    pass
       
   550 #    
       
   551 class TextVersionForm(ModelForm):
       
   552     class Meta:
       
   553         model = TextVersion
       
   554         fields = ('title', 'content', 'format')
       
   555 
       
   556 @has_perm_on_text('can_view_text')
       
   557 def text_diff(request, key, id_v1, id_v2):
       
   558     text = get_text_by_keys_or_404(key)
       
   559     text_version_1 = TextVersion.objects.get(pk=id_v1)
       
   560     text_version_2 = TextVersion.objects.get(pk=id_v2)
       
   561     content = _text_diff(text_version_1.get_content(), text_version_2.get_content())
       
   562     title = _text_diff(text_version_1.title, text_version_2.title)
       
   563     return render_to_response('site/text_view.html', {'text' : text, 'text_version_1' : text_version_1, 'text_version_2' : text_version_2, 'title' : title, 'content' : content}, context_instance=RequestContext(request))
       
   564 
       
   565 
       
   566 @has_perm_on_text('can_view_text')
       
   567 def text_version(request, key, id_version):
       
   568     text = get_text_by_keys_or_404(key)
       
   569     text_version = TextVersion.objects.get(pk=id_version)
       
   570     # TODO : assert text_v in text ...
       
   571     # TODO : do not use db id    
       
   572     return render_to_response('site/text_view.html', {'text' : text, 'text_version' : text_version, 'title' : text_version.title, 'content' : text_version.get_content()}, context_instance=RequestContext(request))
       
   573 
       
   574 class EditTextForm(ModelForm):
       
   575     title = forms.CharField(label=_("Title"), widget=forms.TextInput)
       
   576     #format = forms.CharField(label=_("Format"))
       
   577     #content = forms.TextField(label=_("Content"))
       
   578 
       
   579     note = forms.CharField(label=_("Note (optional)"),
       
   580                            widget=forms.TextInput,
       
   581                            required=False,
       
   582                            help_text=_("Add a note to explain the modifications made to the text")
       
   583                            )
       
   584 
       
   585     #tags = forms.CharField(label=_("Tags (optional)"),
       
   586     #                       widget=forms.TextInput,
       
   587     #                       required=False,
       
   588     #                       #help_text=_("Add a note to explain the modifications made to the text")
       
   589     #                       )
       
   590 
       
   591 
       
   592     new_version = forms.BooleanField(label=_("New version (optional)"),
       
   593                            required=False,
       
   594                            initial=True,
       
   595                            help_text=_("Create a new version of this text (recommended)")
       
   596                            )
       
   597 
       
   598     keep_comments = forms.BooleanField(label=_("Keep comments (optional)"),
       
   599                            required=False,
       
   600                            initial=True,
       
   601                            help_text=_("Keep comments (if not affected by the edit)")
       
   602                            )
       
   603     
       
   604     class Meta:
       
   605         model = TextVersion
       
   606         fields = ('title', 'format', 'content', 'new_version', 'tags', 'note')
       
   607         
       
   608     def save_into_text(self, text, request):
       
   609         new_content = request.POST.get('content')
       
   610         new_title = request.POST.get('title')
       
   611         new_format = request.POST.get('format')
       
   612         new_note = request.POST.get('note',None)
       
   613         new_tags = request.POST.get('tags',None)
       
   614         version = text.get_latest_version()
       
   615         version.edit(new_title, new_format, new_content, new_tags, new_note, True)
       
   616         
       
   617         return version
       
   618 
       
   619     def save_new_version(self, text, request):
       
   620         new_text_version = TextVersion.objects.duplicate(text.get_latest_version(), True) 
       
   621         new_text_version.user = request.user if request.user.is_authenticated() else None
       
   622         new_text_version.note = request.POST.get('note','')
       
   623         new_text_version.email = request.POST.get('email','')
       
   624         new_text_version.name = request.POST.get('name','')
       
   625         new_text_version.save()
       
   626         
       
   627         new_content = request.POST.get('content')
       
   628         new_title = request.POST.get('title')
       
   629         new_format = request.POST.get('format')        
       
   630         new_note = request.POST.get('note',None)
       
   631         new_tags = request.POST.get('tags',None)
       
   632         new_text_version.edit(new_title, new_format, new_content, new_tags, new_note, True)
       
   633         
       
   634         return new_text_version
       
   635 
       
   636 @has_perm_on_text('can_edit_text')
       
   637 def text_pre_edit(request, key, adminkey=None):
       
   638     text = get_text_by_keys_or_404(key)
       
   639     
       
   640     text_version = text.get_latest_version()
       
   641     comments = text_version.get_comments() ;
       
   642     new_content = request.POST['new_content']
       
   643     new_format = request.POST['new_format']
       
   644 
       
   645     # TODO: RBE : si commentaire mal forme : (position non existante : boom par key error)
       
   646     _tomodify_comments, toremove_comments = compute_new_comment_positions(text_version.content, text_version.format, new_content, new_format, comments)
       
   647     return HttpResponse(simplejson.dumps({'nb_removed' : len(toremove_comments) }))
       
   648 
       
   649 class EditTextFormAnon(EditTextForm):
       
   650     name = forms.CharField(label=ugettext_lazy("Name (optional)"), widget=forms.TextInput, required=False)
       
   651     email = forms.EmailField(label=ugettext_lazy("Email (optional)"), required=False)
       
   652     content = forms.CharField(label=ugettext_lazy("Content"), required=True, widget=forms.Textarea(attrs={'rows':'30', 'cols': '70'}))
       
   653     
       
   654     class Meta:
       
   655         model = TextVersion
       
   656         fields = ('title', 'format', 'content', 'tags', 'note', 'name', 'email')
       
   657 
       
   658 @has_perm_on_text('can_edit_text')
       
   659 def text_edit(request, key, adminkey=None):
       
   660     text = get_text_by_keys_or_404(key)
       
   661     text_version = text.get_latest_version()
       
   662     if request.method == 'POST':
       
   663         if request.user.is_authenticated():
       
   664             form = EditTextForm(request.POST)
       
   665         else:
       
   666             form = EditTextFormAnon(request.POST)
       
   667 
       
   668         if form.is_valid():
       
   669             if request.POST.get('new_version'):
       
   670                 new_version = form.save_new_version(text, request)
       
   671                 register_activity(request, "text_edited_new_version", text=text, text_version=new_version)
       
   672             else:
       
   673                 form.save_into_text(text, request)
       
   674                 register_activity(request, "text_edited", text=text)    
       
   675             return redirect(request, 'text-view', args=[text.key]) 
       
   676     else:
       
   677         default_data = {
       
   678                         'content': text_version.content,
       
   679                         'title': text_version.title,
       
   680                         'format': text_version.format,
       
   681                         'tags': text_version.tags,
       
   682                         'new_version': NEW_TEXT_VERSION_ON_EDIT,
       
   683                         'note' : text_version.note,
       
   684                         'keep_comments' : True,
       
   685                        }        
       
   686         if request.user.is_authenticated():
       
   687             form = EditTextForm(default_data)
       
   688         else:
       
   689             form = EditTextFormAnon(default_data)
       
   690 
       
   691     template_dict = {'text' : text, 'form' : form}
       
   692 
       
   693     return render_to_response('site/text_edit.html', template_dict , context_instance=RequestContext(request))
       
   694 
       
   695 # TODO: modif de la base => if POST
       
   696 @has_perm_on_text('can_edit_text')
       
   697 def text_revert(request, key, v1_nid, adminkey=None):
       
   698     text = get_text_by_keys_or_404(key)
       
   699 
       
   700     text.revert_to_version(v1_nid)
       
   701     display_message(request, _(u'A new version (copied from version %(version_id)s) has been created') % {'version_id':v1_nid})
       
   702 
       
   703     return HttpResponseRedirect(reverse('text-history', args=[text.key]))
       
   704     
       
   705 @has_perm_on_text('can_view_text')
       
   706 def text_attach(request, key, attach_key):
       
   707     attach = Attachment.objects.get(key=attach_key, text_version__text__key=key)
       
   708     content = file(attach.data.path).read()
       
   709     mimetype, _encoding = mimetypes.guess_type(attach.data.path)
       
   710     response = HttpResponse(content, mimetype)
       
   711     return response
       
   712 
       
   713     
       
   714 
       
   715 
       
   716 def fix_anon_in_formset(formset):
       
   717     # fix role choice in formset for anon (not all role are allowed)
       
   718     role_field = [f.fields['role'] for f in formset.forms if f.instance.user == None][0]
       
   719     role_field.choices = [(u'', u'---------')] + [(r.id, str(r)) for r in Role.objects.filter(anon = True)] # limit anon choices
       
   720 
       
   721 class BaseUserRoleFormSet(BaseModelFormSet):
       
   722     def clean(self):
       
   723         """Checks that anon users are given roles with anon=True."""
       
   724         for i in range(0, self.total_form_count()):
       
   725             form = self.forms[i]
       
   726             print form.cleaned_data
       
   727             user_role = form.cleaned_data['id']
       
   728             if user_role.user == None:
       
   729                 role = form.cleaned_data['role']
       
   730                 if not role.anon:
       
   731                     # nasty stuff: cannot happen so not dealt with in tempate
       
   732                     logging.warn('Cannot give such role to anon user.')
       
   733                     raise forms.ValidationError, "Cannot give such role to anon user."
       
   734 
       
   735 #@has_perm_on_text('can_manage_text')
       
   736 #def xtext_share(request, key):
       
   737 #    text = get_text_by_keys_or_404(key)
       
   738 #    order_by = get_among(request.GET,'order',('user__username','-user__username','role__name','-role__name'),'user__username')
       
   739 #        
       
   740 #    UserRole.objects.create_userroles_text(text)
       
   741 #    UserRoleFormSet = modelformset_factory(UserRole, fields=('role', ), extra=0, formset = BaseUserRoleFormSet)
       
   742 #    
       
   743 #    # put anon users on top no matter what the order says (TODO: ?)
       
   744 #    userrole_queryset = UserRole.objects.filter(text=text).extra(select={'anon':'"cm_userrole"."user_id">-1'}).order_by('-anon',order_by)
       
   745 #    if request.method == 'POST':
       
   746 #        formset = UserRoleFormSet(request.POST, queryset = userrole_queryset)
       
   747 #
       
   748 #        if formset.is_valid():
       
   749 #            formset.save()
       
   750 #            display_message(request, "Sharing updated.")
       
   751 #            return HttpResponseRedirect(reverse('text-share',args=[text.key]))
       
   752 #
       
   753 #    else:
       
   754 #        formset = UserRoleFormSet(queryset = userrole_queryset)
       
   755 #        fix_anon_in_formset(formset)
       
   756 #         
       
   757 #    global_anon_userrole = UserRole.objects.get(text=None, user=None)
       
   758 #    return render_to_response('site/text_share.html', {'text' : text, 
       
   759 #                                                       'formset' : formset,
       
   760 #                                                       'global_anon_userrole' : global_anon_userrole,
       
   761 #                                                       } , context_instance=RequestContext(request))
       
   762 
       
   763 # TODO: permission protection ? format value check ?
       
   764 def text_wysiwyg_preview(request, format):
       
   765     html_content = ""
       
   766     if request.POST : # if satisfied in the no html case, in html case : no POST (cf. markitup) previewTemplatePath and previewParserPath
       
   767         html_content = pandoc_convert(request.POST['data'], format, "html", full=False)
       
   768         
       
   769     return render_to_response('site/wysiwyg_preview.html', {'content':html_content} , context_instance=RequestContext(request))
       
   770         #return HttpResponse(pandoc_convert(content, format, "html", full=False))
       
   771 
       
   772 @has_perm_on_text('can_manage_text')
       
   773 def text_share(request, key):
       
   774     text = get_text_by_keys_or_404(key)
       
   775     order_by = get_among(request.GET,'order',('user__username',
       
   776                                               'user__email',
       
   777                                               '-user__username',
       
   778                                               '-user__email',
       
   779                                               'role__name',
       
   780                                               '-role__name',
       
   781                                               ),
       
   782                           'user__username')
       
   783     paginate_by = 10
       
   784     
       
   785     UserRole.objects.create_userroles_text(text)
       
   786     
       
   787     if request.method == 'POST':
       
   788         if 'save' in request.POST:
       
   789             user_profile_keys_roles = get_keys_from_dict(request.POST, 'user-role-')
       
   790             count = 0
       
   791             for user_profile_key in user_profile_keys_roles:
       
   792                 role_id = user_profile_keys_roles[user_profile_key]
       
   793                 if not user_profile_key:
       
   794                     user_role = UserRole.objects.get(user = None, text = text)
       
   795                 else:                    
       
   796                     user_role = UserRole.objects.get(user__userprofile__key = user_profile_key, text = text)
       
   797                 if (role_id != u'' or user_role.role_id!=None) and role_id!=unicode(user_role.role_id):
       
   798                     if role_id:
       
   799                         user_role.role_id = int(role_id)
       
   800                     else:
       
   801                         user_role.role_id = None
       
   802                     user_role.save()
       
   803                     count += 1
       
   804             display_message(request, _(u'%(count)i user(s) role modified') %{'count':count})                
       
   805             return HttpResponseRedirect(reverse('text-share', args=[text.key]))    
       
   806     
       
   807     anon_role = UserRole.objects.get(user = None, text = text).role
       
   808     global_anon_role = UserRole.objects.get(user = None, text = None).role
       
   809         
       
   810     context = {
       
   811                'anon_role' : anon_role,
       
   812                'global_anon_role' : global_anon_role,
       
   813                'all_roles' : Role.objects.all(),
       
   814                'anon_roles' : Role.objects.filter(anon = True),
       
   815                'text' : text,
       
   816                }
       
   817 
       
   818     return object_list(request, UserRole.objects.filter(text=text).filter(~Q(user=None)).order_by(order_by),
       
   819                        template_name = 'site/text_share.html',
       
   820                        paginate_by = paginate_by,
       
   821                        extra_context = context,
       
   822                        )
       
   823     
       
   824     
       
   825 class SettingsTextForm(ModelForm):
       
   826     # example name = forms.CharField(label=_("Name (optional)"), widget=forms.TextInput, required=False)
       
   827     
       
   828     class Meta:
       
   829         model = TextVersion
       
   830         fields = ('mod_posteriori',)
       
   831 
       
   832 @has_perm_on_text('can_manage_text')
       
   833 def text_settings(request, key):
       
   834     text = get_text_by_keys_or_404(key)
       
   835         
       
   836     text_version = text.get_latest_version()
       
   837     if request.method == 'POST':
       
   838         form = SettingsTextForm(request.POST, instance = text_version)
       
   839         
       
   840         if form.is_valid():
       
   841             form.save()
       
   842             display_message(request, _(u'Text settings updated'))                            
       
   843             return redirect(request, 'text-view', args=[text.key])
       
   844     else:
       
   845         form = SettingsTextForm(instance = text_version)
       
   846 
       
   847     template_dict = {'text' : text, 'form' : form}
       
   848 
       
   849     return render_to_response('site/text_settings.html', template_dict , context_instance=RequestContext(request))
       
   850