From 256dd73dd43c24ebe01d8c285bcfcc294a7ece84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20A=2E=20S=C3=A1nchez=20L=C3=B3pez?= Date: Sun, 23 Jan 2011 10:23:45 +0000 Subject: [PATCH] Detailed stream info view. Fixes #573 - Legacy-Id: 2758 --- ietf/ietfworkflows/models.py | 32 ++++++- ietf/ietfworkflows/streams.py | 84 +++++++++++++++++++ ietf/ietfworkflows/templatetags/__init__.py | 0 .../templatetags/ietf_streams.py | 48 +++++++++++ ietf/ietfworkflows/urls.py | 7 ++ ietf/ietfworkflows/utils.py | 48 ++++++++--- ietf/ietfworkflows/views.py | 60 +++++++++++++ ietf/templates/idrfc/status_columns.html | 3 +- .../ietfworkflows/stream_history.html | 44 ++++++++++ .../templates/ietfworkflows/stream_state.html | 10 +++ .../ietfworkflows/workflow_history_entry.html | 12 +++ ietf/urls.py | 1 + ietf/wgchairs/forms.py | 5 +- static/css/base2.css | 11 +++ static/js/base.js | 50 +++++++++++ 15 files changed, 402 insertions(+), 13 deletions(-) create mode 100644 ietf/ietfworkflows/streams.py create mode 100644 ietf/ietfworkflows/templatetags/__init__.py create mode 100644 ietf/ietfworkflows/templatetags/ietf_streams.py create mode 100644 ietf/ietfworkflows/urls.py create mode 100644 ietf/ietfworkflows/views.py create mode 100644 ietf/templates/ietfworkflows/stream_history.html create mode 100644 ietf/templates/ietfworkflows/stream_state.html create mode 100644 ietf/templates/ietfworkflows/workflow_history_entry.html diff --git a/ietf/ietfworkflows/models.py b/ietf/ietfworkflows/models.py index 99634d769..ede8ac995 100644 --- a/ietf/ietfworkflows/models.py +++ b/ietf/ietfworkflows/models.py @@ -17,12 +17,16 @@ class ObjectHistoryEntry(models.Model): comment = models.TextField(_('Comment')) person = models.ForeignKey(PersonOrOrgInfo) + class Meta: + ordering = ('-date', ) + + def get_real_instance(self): if hasattr(self, '_real_instance'): return self._real_instance for i in ('objectworkflowhistoryentry', 'objectannotationtaghistoryentry', 'objectstreamhistoryentry'): try: - real_instance = getattr(self, 'objectworkflowhistoryentry', None) + real_instance = getattr(self, i, None) if real_instance: self._real_instance = real_instance return real_instance @@ -36,16 +40,42 @@ class ObjectWorkflowHistoryEntry(ObjectHistoryEntry): from_state = models.CharField(_('From state'), max_length=100) to_state = models.CharField(_('To state'), max_length=100) + def describe_change(self): + html = '

' + html += 'Changed state %s to %s' % (self.from_state, self.to_state) + html += '

' + return html + class ObjectAnnotationTagHistoryEntry(ObjectHistoryEntry): setted = models.TextField(_('Setted tags'), blank=True, null=True) unsetted = models.TextField(_('Unsetted tags'), blank=True, null=True) + def describe_change(self): + html = '' + if self.setted: + html += '

' + html += 'Annotation tags set: ' + html += self.setted + html += '

' + if self.unsetted: + html += '

' + html += 'Annotation tags reset: ' + html += self.unsetted + html += '

' + return html + class ObjectStreamHistoryEntry(ObjectHistoryEntry): from_stream = models.TextField(_('From stream'), blank=True, null=True) to_stream = models.TextField(_('To stream'), blank=True, null=True) + def describe_change(self): + html = '

' + html += 'Changed doc from stream %s to %s' % (self.from_stream, self.to_stream) + html += '

' + return html + class AnnotationTag(models.Model): name = models.CharField(_(u"Name"), max_length=100) diff --git a/ietf/ietfworkflows/streams.py b/ietf/ietfworkflows/streams.py new file mode 100644 index 000000000..18ce32f36 --- /dev/null +++ b/ietf/ietfworkflows/streams.py @@ -0,0 +1,84 @@ +from django.db import models + +from ietf.idrfc.idrfc_wrapper import IdRfcWrapper, IdWrapper +from ietf.ietfworkflows.models import StreamedID, Stream + + +def get_streamed_draft(draft): + if not draft: + return None + try: + return draft.streamedid + except StreamedID.DoesNotExist: + return None + + +def get_stream_from_draft(draft): + streamedid = get_streamed_draft(draft) + if streamedid: + return streamedid.stream + return False + + +def get_stream_from_id(stream_id): + try: + return Stream.objects.get(id=stream_id) + except Stream.DoesNotExist: + return None + + +def _get_group_from_acronym(group_model_str, acronym): + try: + app, model = group_model_str.split('.', 1) + except ValueError: + return None + group_model = models.get_model(app, model) + if not group_model: + return + if 'acronym' in group_model._meta.get_all_field_names(): + try: + return group_model._default_manager.get(acronym=acronym) + except group_model.DoesNotExist: + return None + elif 'group_acronym' in group_model._meta.get_all_field_names(): + try: + return group_model._default_manager.get(group_acronym__acronym=acronym) + except group_model.DoesNotExist: + return None + else: + return None + + +def _set_stream_automatically(draft, stream): + streamed = StreamedID.objects.create(stream=stream, draft=draft) + if not stream or not stream.with_groups: + return + try: + draft_literal, stream_name, group_name, extra = draft.filename.split('-', 3) + if stream_name.lower() == stream.name.lower(): + group = _get_group_from_acronym(stream.group_model, group_name) + if group: + streamed.group = group + streamed.save() + except ValueError: + return + + +def get_stream_from_wrapper(idrfc_wrapper): + idwrapper = None + if isinstance(idrfc_wrapper, IdRfcWrapper): + idwrapper = idrfc_wrapper.id + elif isinstance(idrfc_wrapper, IdWrapper): + idwrapper = idrfc_wrapper + if not idwrapper: + return None + draft = idwrapper._draft + stream = get_stream_from_draft(draft) + if stream == False: + stream_id = idwrapper.stream_id() + stream = get_stream_from_id(stream_id) + _set_stream_automatically(draft, stream) + return stream + else: + return stream + return None diff --git a/ietf/ietfworkflows/templatetags/__init__.py b/ietf/ietfworkflows/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/ietfworkflows/templatetags/ietf_streams.py b/ietf/ietfworkflows/templatetags/ietf_streams.py new file mode 100644 index 000000000..f18d7709e --- /dev/null +++ b/ietf/ietfworkflows/templatetags/ietf_streams.py @@ -0,0 +1,48 @@ +from django import template + +from ietf.idrfc.idrfc_wrapper import IdRfcWrapper, IdWrapper +from ietf.ietfworkflows.utils import (get_workflow_for_draft, + get_state_for_draft) +from ietf.ietfworkflows.streams import get_stream_from_wrapper + + +register = template.Library() + + +@register.inclusion_tag('ietfworkflows/stream_state.html', takes_context=True) +def stream_state(context, doc): + request = context.get('request', None) + user = request and request.user + data = {} + stream = get_stream_from_wrapper(doc) + data.update({'stream': stream}) + if not stream: + return data + + idwrapper = None + if isinstance(doc, IdRfcWrapper): + idwrapper = doc.id + elif isinstance(doc, IdWrapper): + idwrapper = doc + if not idwrapper: + return data + + draft = getattr(idwrapper, '_draft', None) + if not draft: + return data + + workflow = get_workflow_for_draft(draft) + state = get_state_for_draft(draft) + + data.update({'workflow': workflow, + 'draft': draft, + 'state': state}) + + return data + + +@register.inclusion_tag('ietfworkflows/workflow_history_entry.html', takes_context=True) +def workflow_history_entry(context, entry): + real_entry = entry.get_real_instance() + return {'entry': real_entry, + 'entry_class': real_entry.__class__.__name__.lower()} diff --git a/ietf/ietfworkflows/urls.py b/ietf/ietfworkflows/urls.py new file mode 100644 index 000000000..7ea564563 --- /dev/null +++ b/ietf/ietfworkflows/urls.py @@ -0,0 +1,7 @@ +# Copyright The IETF Trust 2008, All Rights Reserved + +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('ietf.ietfworkflows.views', + url(r'^(?P[^/]+)/history/$', 'stream_history', name='stream_history'), +) diff --git a/ietf/ietfworkflows/utils.py b/ietf/ietfworkflows/utils.py index bef2c064c..3f40b614b 100644 --- a/ietf/ietfworkflows/utils.py +++ b/ietf/ietfworkflows/utils.py @@ -10,8 +10,10 @@ from workflows.models import State from workflows.utils import (get_workflow_for_object, set_workflow_for_object, get_state) +from ietf.ietfworkflows.streams import get_streamed_draft from ietf.ietfworkflows.models import (WGWorkflow, AnnotationTagObjectRelation, - AnnotationTag, ObjectAnnotationTagHistoryEntry) + AnnotationTag, ObjectAnnotationTagHistoryEntry, + ObjectHistoryEntry) WAITING_WRITEUP = 'WG Consensus: Waiting for Write-Up' @@ -24,7 +26,8 @@ def get_default_workflow_for_wg(): return workflow except WGWorkflow.DoesNotExist: return None - + + def clone_transition(transition): new = copy.copy(transition) new.pk = None @@ -35,6 +38,7 @@ def clone_transition(transition): new.states.add(state) return new + def clone_workflow(workflow, name): new = WGWorkflow.objects.create(name=name, initial_state=workflow.initial_state) @@ -51,20 +55,25 @@ def clone_workflow(workflow, name): new.transitions.add(clone_transition(transition)) return new -def get_workflow_for_wg(wg): + +def get_workflow_for_wg(wg, default=None): workflow = get_workflow_for_object(wg) try: workflow = workflow and workflow.wgworkflow except WGWorkflow.DoesNotExist: workflow = None if not workflow: - workflow = get_default_workflow_for_wg() + if default: + workflow = default + else: + workflow = get_default_workflow_for_wg() if not workflow: return None workflow = clone_workflow(workflow, name='%s workflow' % wg) set_workflow_for_object(wg, workflow) return workflow + def get_workflow_for_draft(draft): workflow = get_workflow_for_object(draft) try: @@ -72,11 +81,29 @@ def get_workflow_for_draft(draft): except WGWorkflow.DoesNotExist: workflow = None if not workflow: - workflow = get_workflow_for_wg(draft.group.ietfwg) + streamed_draft = get_streamed_draft(draft) + if not streamed_draft or not streamed_draft.stream: + return None + stream = streamed_draft.stream + if stream.with_groups: + if not streamed_draft.group: + return None + else: + workflow = get_workflow_for_wg(streamed_draft.group, streamed_draft.stream.workflow) + else: + workflow = stream.workflow set_workflow_for_object(draft, workflow) return workflow +def get_workflow_history_for_draft(draft): + ctype = ContentType.objects.get_for_model(draft) + history = ObjectHistoryEntry.objects.filter(content_type=ctype, content_id=draft.pk).\ + select_related('objectworkflowhistoryentry', 'objectannotationtaghistoryentry', + 'objectstreamhistoryentry') + return history + + def get_annotation_tags_for_draft(draft): ctype = ContentType.objects.get_for_model(draft) tags = AnnotationTagObjectRelation.objects.filter(content_type=ctype, content_id=draft.pk) @@ -100,6 +127,7 @@ def get_annotation_tag_by_name(tag_name): except AnnotationTag.DoesNotExist: return None + def set_tag_by_name(obj, tag_name): ctype = ContentType.objects.get_for_model(obj) try: @@ -159,7 +187,7 @@ def notify_tag_entry(entry, extra_notify=[]): def notify_state_entry(entry, extra_notify=[]): return notify_entry(entry, 'ietfworkflows/state_updated_mail.txt', extra_notify) - + def update_tags(obj, comment, person, set_tags=[], reset_tags=[], extra_notify=[]): ctype = ContentType.objects.get_for_model(obj) setted = [] @@ -173,9 +201,9 @@ def update_tags(obj, comment, person, set_tags=[], reset_tags=[], extra_notify=[ entry = ObjectAnnotationTagHistoryEntry.objects.create( content_type=ctype, content_id=obj.pk, - setted = ','.join(setted), - unsetted = ','.join(resetted), - change_date = datetime.datetime.now(), - comment = comment, + setted=','.join(setted), + unsetted=','.join(resetted), + date=datetime.datetime.now(), + comment=comment, person=person) notify_tag_entry(entry, extra_notify) diff --git a/ietf/ietfworkflows/views.py b/ietf/ietfworkflows/views.py new file mode 100644 index 000000000..a11f3b3d0 --- /dev/null +++ b/ietf/ietfworkflows/views.py @@ -0,0 +1,60 @@ +from ietf.idtracker.models import IETFWG, InternetDraft, IESGLogin +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext +from django.http import HttpResponseForbidden, Http404 + +from ietf.idrfc.views_search import SearchForm, search_query +from ietf.wgchairs.forms import (RemoveDelegateForm, add_form_factory, + workflow_form_factory, TransitionFormSet, + WriteUpEditForm) +from ietf.wgchairs.accounts import (can_manage_delegates_in_group, get_person_for_user, + can_manage_shepherds_in_group, + can_manage_workflow_in_group, + can_manage_shepherd_of_a_document, + can_manage_writeup_of_a_document, + can_manage_writeup_of_a_document_no_state, + ) +from ietf.ietfworkflows.streams import (get_stream_from_draft, + get_streamed_draft) +from ietf.ietfworkflows.utils import (get_workflow_for_wg, + get_default_workflow_for_wg, + get_workflow_history_for_draft, + get_workflow_for_draft, + get_state_by_name, + get_annotation_tags_for_draft, + get_state_for_draft, WAITING_WRITEUP, + FOLLOWUP_TAG) + + +REDUCED_HISTORY_LEN = 20 + + +def stream_history(request, name): + user = request.user + person = get_person_for_user(user) + draft = get_object_or_404(InternetDraft, filename=name) + streamed = get_streamed_draft(draft) + stream = get_stream_from_draft(draft) + workflow = get_workflow_for_draft(draft) + tags = [] + if workflow: + tags_setted = [i.annotation_tag.pk for i in get_annotation_tags_for_draft(draft)] + for tag in workflow.get_tags(): + tag.setted = tag.pk in tags_setted + tags.append(tag) + state = get_state_for_draft(draft) + history = get_workflow_history_for_draft(draft) + show_more = False + if history.count > REDUCED_HISTORY_LEN: + show_more = True + return render_to_response('ietfworkflows/stream_history.html', + {'stream': stream, + 'streamed': streamed, + 'draft': draft, + 'tags': tags, + 'state': state, + 'workflow': workflow, + 'show_more': show_more, + 'history': history[:REDUCED_HISTORY_LEN], + }, + context_instance=RequestContext(request)) diff --git a/ietf/templates/idrfc/status_columns.html b/ietf/templates/idrfc/status_columns.html index 973db5844..00491018e 100644 --- a/ietf/templates/idrfc/status_columns.html +++ b/ietf/templates/idrfc/status_columns.html @@ -31,7 +31,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% endcomment %} -{% load ietf_filters %}{% load ballot_icon %} +{% load ietf_filters ietf_streams %}{% load ballot_icon %} {{ doc.friendly_state|safe }} {% if not doc.rfc %}{{ doc.id|state_age_colored|safe }}{% endif %} {% if not hide_telechat_date %}{% if doc.telechat_date %}
IESG Telechat: {{ doc.telechat_date }}{% endif %}{% endif %} @@ -43,6 +43,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% if doc.rfc.has_errata %}
Errata{% endif %} {% else %}{# not rfc #} {% if doc.id.rfc_editor_state %}
RFC Editor State: {{ doc.id.rfc_editor_state|escape }}{% endif %} +{% stream_state doc %} {% endif %} diff --git a/ietf/templates/ietfworkflows/stream_history.html b/ietf/templates/ietfworkflows/stream_history.html new file mode 100644 index 000000000..ae4fb7b19 --- /dev/null +++ b/ietf/templates/ietfworkflows/stream_history.html @@ -0,0 +1,44 @@ +{% load ietf_streams %} + + + + + +
+ {% if workflow %} +
    + {% for tag in tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} +
+
+ {% if stream %} +

+ {{ stream|default:"Without stream" }}{% if stream.with_groups and streamed.group %} :: {{ streamed.group|default:"" }}{% endif %} +

+

Current state: {{ state|default:"None" }}

+ {% else %} +

Without stream

+ {% endif %} +
+ + {% if history %} +
+ {% if show_more %} +

Viewing the last 20 entries. Show full log.

+ {% endif %} + {% for entry in history %} + {% workflow_history_entry entry %} + {% endfor %} + {% if show_more %} +

Viewing the last 20 entries. Show full log.

+ {% endif %} +
+ {% else %} +

+ There is no stream history for this document. +

+ {% endif %} +
diff --git a/ietf/templates/ietfworkflows/stream_state.html b/ietf/templates/ietfworkflows/stream_state.html new file mode 100644 index 000000000..f4ffbf4b3 --- /dev/null +++ b/ietf/templates/ietfworkflows/stream_state.html @@ -0,0 +1,10 @@ +{% if draft %} +
+ +{% if stream %} + {{ stream }} {% if state %}:: {{ state }}{% endif %} +{% else %} + No stream asigned +{% endif %} +
+{% endif %} diff --git a/ietf/templates/ietfworkflows/workflow_history_entry.html b/ietf/templates/ietfworkflows/workflow_history_entry.html new file mode 100644 index 000000000..a18302c44 --- /dev/null +++ b/ietf/templates/ietfworkflows/workflow_history_entry.html @@ -0,0 +1,12 @@ +
+
+ + {{ entry.person }} +
+
+ {{ entry.describe_change|safe }} +
+
+ {{ entry.comment }} +
+ diff --git a/ietf/urls.py b/ietf/urls.py index 41ce897bc..c9b75c256 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -55,6 +55,7 @@ urlpatterns = patterns('', (r'^accounts/', include('ietf.ietfauth.urls')), (r'^doc/', include('ietf.idrfc.urls')), (r'^wg/', include('ietf.wginfo.urls')), + (r'^streams/', include('ietf.ietfworkflows.urls')), (r'^$', 'ietf.idrfc.views.main'), ('^admin/', include(admin.site.urls)), diff --git a/ietf/wgchairs/forms.py b/ietf/wgchairs/forms.py index 2d12e1bd1..9132a9c63 100644 --- a/ietf/wgchairs/forms.py +++ b/ietf/wgchairs/forms.py @@ -363,7 +363,10 @@ class WriteUpEditForm(RelatedWGForm): if self.data.get('modify_tag', False): followup = self.cleaned_data.get('followup', False) comment = self.cleaned_data.get('comment', False) - shepherd = self.doc.shepherd + try: + shepherd = self.doc.shepherd + except PersonOrOrgInfo.DoesNotExist: + shepherd = None if shepherd: extra_notify = ['%s <%s>' % shepherd.email()] else: diff --git a/static/css/base2.css b/static/css/base2.css index 96cecb3c7..0e719bbce 100644 --- a/static/css/base2.css +++ b/static/css/base2.css @@ -71,6 +71,17 @@ body { margin: 0; } .ietf-ballot .square { border:1px solid black; display: block; float:left;width: 10px; height:10px;font-size:1px;margin-right:4px; margin-top:1px;} .ietf-ballot .was { padding-left: 10px; font-size:85%; } +.ietf-stream tr { vertical-align: top; } +.ietf-stream ul.ietf-stream-tag-list { padding: 10px; margin: 0px; list-style-type: none; font-size: 10px; } +.ietf-stream ul.ietf-stream-tag-list li { margin-bottom: 1em; list-style-type: circle; color: #999999; } +.ietf-stream ul.ietf-stream-tag-list li.tag_set { font-weight: bold; list-style-type: disc; color: black; } +.ietf-stream td.right { padding-top: 1em; } +.ietf-stream .ietf-stream-head h2, .ietf-stream .ietf-stream-head h3 { margin: 0px; } +.ietf-stream .ietf-stream-head { margin-bottom: 2em; } +.ietf-stream .entry-title { background: #2647A0; color:white; padding: 2px 4px; font-size: 108%; margin-top: 0;} +.ietf-stream .entry-title .entry-date { float: right; } +.ietf-stream .entry-comment { background: #eeeeee; margin: 1em 0px; padding: 1em; } + .search_form_box {width: 99.5%; margin-top:8px; padding:4px; margin-bottom:1em; padding-left:8px;} form#search_form { padding-top: 4px; padding-bottom: 4px; } #search_form input { padding: 0; padding-left: 2px; border: 1px solid #89d;} diff --git a/static/js/base.js b/static/js/base.js index a24ac900b..08cb5b853 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -74,3 +74,53 @@ function showBallot(draftName, editPositionUrl) { function editBallot(editPositionUrl) { window.open(editPositionUrl); } +function showStream(dialogTitle, infoStreamUrl) { + var handleClose = function() { + IETF.streamDialog.hide(); + }; + var el; + + if (!IETF.streamDialog) { + el = document.createElement("div"); + el.innerHTML = ''; + document.getElementById("ietf-extras").appendChild(el); + + var buttons = [{text:"Close", handler:handleClose, isDefault:true}]; + var kl = [new YAHOO.util.KeyListener(document, {keys:27}, handleClose)] + IETF.streamDialog = new YAHOO.widget.Dialog("stream_dialog", { + visible:false, draggable:false, close:true, modal:true, + width:"860px", fixedcenter:true, constraintoviewport:true, + buttons: buttons, keylisteners:kl}); + IETF.streamDialog.render(); + } + document.getElementById("stream_title").innerHTML = dialogTitle; + IETF.streamDialog.show(); + + el = document.getElementById("stream_dialog_body"); + el.innerHTML = "Loading..."; + YAHOO.util.Connect.asyncRequest('GET', + infoStreamUrl, + { success: function(o) { el.innerHTML = (o.responseText !== undefined) ? o.responseText : "?"; }, + failure: function(o) { el.innerHTML = "Error: "+o.status+" "+o.statusText; }, + argument: null + }, null); +} +(function ($) { + + $.fn.StreamInfo = function() { + return this.each(function () { + var infoStreamUrl = $(this).attr('href'); + var title = $(this).attr('title'); + + $(this).click(function() { + showStream(title, infoStreamUrl); + return false; + }); + }); + }; + + $(document).ready(function () { + $('a.show_stream_info').StreamInfo(); + }); + +})(jQuery);