Repository: carpentries/amy

Issue:
Title not set in template parameters when editing person
1.  Display all persons.
2.  Pick a person.
3.  Edit (e.g., go to http://127.0.0.1:8000/workshops/person/100/edit).
4.  View source.

If you have #140, you will see XXX-unset-variable-XXX as the page title, because `title` isn't included in the context used when the page is expanded.  How should `title` be set when editing?  The editing form is auto-generated from `PersonUpdate(UpdateView)`.



Discussion:
Hey, @gvwilson, I don't have much experience with class-based views, but it seems like `ContextMixin` is a solution:
https://docs.djangoproject.com/en/1.7/topics/class-based-views/mixins/#context-and-template-responses

Here's sample usage:
https://docs.djangoproject.com/en/1.7/ref/class-based-views/mixins-simple/#django.views.generic.base.ContextMixin.get_context_data

We can basically inherit `ContextMixin` as, for example, `UpdateViewContext` that provides proper title:

``` python
class UpdateViewContext(UpdateView, ContextMixin):
    def get_context_data(self, **kwargs):
        context = super(ContextMixin, self).get_context_data(**kwargs)
        context['title'] = str(self.object)  # from UpdateView
        return context
```

Then we can use `UpdateViewContext` instead of `UpdateView`, I think.

Hi Piotr,
Doesn't work for me.  Grab #143 (which isolates this change), and look at:

```
class PersonUpdate(UpdateView, ContextMixin):
     model = Person
     fields = '__all__'
     pk_url_kwarg = 'person_id'

     def get_context_data(self, **kwargs):
         context = super(UpdateView, self).get_context_data(**kwargs)
         context['title'] = str(self.object)
         return context
```

The title is still 'XXX-unset-variable-XXX.  If I change the `super` 
call to `super(ContextMixin, self)`, Python complains that there's no 
such method.

Greg, it's definitely working for me:
![screenshot from 2015-01-16 15 02 15](https://cloud.githubusercontent.com/assets/72821/5777234/b4a8b222-9d90-11e4-9e46-2622d0a9ca0c.png)

What are your Python and Django versions? I'm using:

```
Python 2.7.9 :: Continuum Analytics, Inc.
Django 1.7.1
```

I believe you should invoke `super()` in this way:

``` python
    def get_context_data(self, **kwargs):
        context = super(PersonUpdate, self).get_context_data(**kwargs)
        context['title'] = str(self.object)  # from UpdateView
        return context
```

The change is that in your code you had `super(UpdateView, self)`, here it's `super(PersonUpdate, self)`.

I'm still advocating for using a new class-based view that I wrote in previous post (there's an error linked to using `super()` that I fixed in code below):

``` python
class UpdateViewContext(UpdateView, ContextMixin):
    def get_context_data(self, **kwargs):
        context = super(UpdateViewContext, self).get_context_data(**kwargs)
        context['title'] = str(self.object)
        return context
```

I tried this and it worked. Cheers!

Hi Piotr,
Can you submit a PR with your version and I'll try it here?
Thanks,
Greg

Python 2.7.8 |Anaconda 2.1.0 and django.VERSION = (1, 7, 1, 'final', 0)

And I tried both the `super(PersonUpdate, self)` and the 
UpdateViewContext that you advocated - failed the same way.

Greg, I sent you https://github.com/gvwilson/amy/pull/11.

Yup, I replied - fixes the title, but breadcrumbs are still not set.  
Separate bug?

Greg,
yeah there are some minor bugs with breadcrumbs. I'd go ahead and merge this PR and let me squash these bugs.
Cheers

Thanks - I fixed some things in the adding-tests branch (in particular, 
escaping characters in breadcrumb text), so you might want to start from 
there.  I'll go ahead and merge in your title-fixing PR.
Thanks,
Greg

p.s. should UpdateViewContext be used as a parent for all other editing 
views?  And is something equivalent needed for the creation views?

1) Yes, unless we have to use some specific template variables other than `title`. In that case, we should directly inherit `ContextMixin`
2) Yes, I can see some `XXX-unset-variable-XXX` in title when creating a new person.

Thanks - can you please update the ones that need updating? I think 
`title` will be enough, and I'm building tests to catch other cases that 
might slip through.
Thanks,
Greg


Resolving patch:
diff --git a/workshops/templatetags/breadcrumbs.py b/workshops/templatetags/breadcrumbs.py
--- a/workshops/templatetags/breadcrumbs.py
+++ b/workshops/templatetags/breadcrumbs.py
@@ -1,132 +1,118 @@
 import logging
 
 from django import template
-from django.template import loader, Node, Variable
-from django.utils.encoding import smart_text
-from django.template.defaulttags import url
-from django.template import VariableDoesNotExist
+from django.utils.encoding import force_text
+from django.core.urlresolvers import reverse
 
 register = template.Library()
 _LOG = logging.getLogger(__name__)
 
 
-@register.tag
-def breadcrumb(parser, token):
+@register.simple_tag
+def breadcrumb(title, url):
     """
-    Renders the breadcrumb.
-    Examples:
-        {% breadcrumb "Title of breadcrumb" url_var %}
-        {% breadcrumb context_var  url_var %}
-        {% breadcrumb "Just the title" %}
-        {% breadcrumb just_context_var %}
-
-    Parameters:
-    -First parameter is the title of the crumb,
-    -Second (optional) parameter is the url variable to link to, produced by
-     url tag, i.e.:
-        {% url person_detail object.id as person_url %}
-        then:
-        {% breadcrumb person.name person_url %}
-
-    @author Andriy Drozdyuk
+    Create a simple anchor with provided text and already-resolved URL.
+    Example usage:
+        {% breadcrumb "Title of breadcrumb" resolved_url %}
     """
-    return BreadcrumbNode(token.split_contents()[1:])
+    return create_crumb(title, url)
 
 
-@register.tag
-def breadcrumb_url(parser, token):
+@register.simple_tag
+def breadcrumb_url(title, url_name):
     """
-    Same as breadcrumb
-    but instead of url context variable takes in all the
-    arguments URL tag takes.
-        {% breadcrumb "Title of breadcrumb" person_detail person.id %}
-        {% breadcrumb person.name person_detail person.id %}
+    Add non-active breadcrumb with specified title.  Second argument should be
+    a string name of URL that needs to be resolved.
+    Example usage:
+        {% breadcrumb_url "Title of breadcrumb" url_name %}
     """
-
-    bits = token.split_contents()
-    if len(bits) == 2:
-        return breadcrumb(parser, token)
-
-    # Extract our extra title parameter
-    title = bits.pop(1)
-    token.contents = ' '.join(bits)
-
-    url_node = url(parser, token)
-
-    return UrlBreadcrumbNode(title, url_node)
+    url = reverse(url_name)
+    return create_crumb(title, url)
 
 
 @register.simple_tag
 def breadcrumb_active(title):
     """
-    Same as breadcrumb, but adds "active" CSS class.  Only breadcrumb title is
-    supported.
+    Add active breadcrumb, but not in an anchor.
+    Example usage:
         {% breadcrumb_active "Title of breadcrumb" %}
     """
-    return create_crumb(title, active=True)
-
-
-class BreadcrumbNode(Node):
-    def __init__(self, vars):
-        """
-        First var is title, second var is url context variable
-        """
-        self.vars = list(map(Variable, vars))
-
-    def render(self, context):
-        title = self.vars[0].var
+    return create_crumb(title, url=None, active=True)
 
-        if title.find("'") == -1 and title.find('"') == -1:
-            try:
-                val = self.vars[0]
-                title = val.resolve(context)
-            except:
-                title = ''
 
-        else:
-            title = title.strip("'").strip('"')
-            title = smart_text(title)
+@register.simple_tag
+def breadcrumb_index_all_objects(model):
+    """
+    Add breadcrumb linking to the listing of all objects of specific type.
+    This tag accepts both models or model instances as an argument.
+    Example usage:
+        {% breadcrumb_index_all_objects model %}
+        {% breadcrumb_index_all_objects person %}
+    """
+    plural = force_text(model._meta.verbose_name_plural)
+    title = 'All {}'.format(plural)
+    url_name = 'all_{}'.format(plural)
+    url = reverse(url_name)
+    return create_crumb(title, url)
 
-        url = None
 
-        if len(self.vars) > 1:
-            val = self.vars[1]
-            try:
-                url = val.resolve(context)
-            except VariableDoesNotExist:
-                _LOG.warning('URL does not exist {}'.format(val))
-                url = None
+@register.simple_tag
+def breadcrumb_edit_object(obj):
+    """
+    Add an active breadcrumb with the title "Edit MODEL_NAME".
+    This tag accepts model instance as an argument.
+    Example usage:
+        {% breadcrumb_edit_object person %}
+    """
+    singular = force_text(obj._meta.verbose_name)
+    title = 'Edit {}'.format(singular)
+    return create_crumb(title, url=None, active=True)
 
-        return create_crumb(title, url)
 
+@register.simple_tag
+def breadcrumb_new_object(model):
+    """
+    Add an active breadcrumb with the title "Add new MODEL_NAME".
+    This tag accepts model class as an argument.
+    Example usage:
+        {% breadcrumb_new_object person %}
+    """
+    singular = force_text(model._meta.verbose_name)
+    title = 'Add new {}'.format(singular)
+    return create_crumb(title, url=None, active=True)
 
-class UrlBreadcrumbNode(Node):
-    def __init__(self, title, url_node):
-        self.title = Variable(title)
-        self.url_node = url_node
 
-    def render(self, context):
-        title = self.title.var
+@register.simple_tag
+def breadcrumb_object(obj):
+    """
+    Add non-active breadcrumb with the title "Add new MODEL_NAME".
+    This tag accepts model instance as an argument.
+    Example usage:
+        {% breadcrumb_object person %}
+    """
+    title = str(obj)
+    url = obj.get_absolute_url()
+    return create_crumb(title, url, active=False)
 
-        if title.find("'") == -1 and title.find('"') == -1:
-            try:
-                val = self.title
-                title = val.resolve(context)
-            except:
-                title = ''
-        else:
-            title = title.strip("'").strip('"')
-            title = smart_text(title)
 
-        url = self.url_node.render(context)
-        return create_crumb(title, url)
+@register.simple_tag
+def breadcrumb_main_page():
+    """
+    Special case of ``breadcrumb_url``.  In all templates there's always a link
+    to the main page so I wanted to save everyone thinking & writing by
+    introducing this helper tag.
+    Example usage:
+        {% breadcrumb_main_page %}
+    """
+    title = 'Amy'
+    url = reverse('index')
+    return create_crumb(title, url)
 
 
 def create_crumb(title, url=None, active=False):
     """
-    Helper function
+    Helper function that creates breadcrumb.
     """
-
     active_str = ""
     if active:
         active_str = ' class="active"'
@@ -135,6 +121,6 @@ def create_crumb(title, url=None, active=False):
     if url:
         inner_str = '<a href="%s">%s</a>' % (url, title)
 
-    crumb = "<li %s>%s</li>" % (active_str, inner_str)
+    crumb = "<li%s>%s</li>" % (active_str, inner_str)
 
     return crumb
diff --git a/workshops/urls.py b/workshops/urls.py
--- a/workshops/urls.py
+++ b/workshops/urls.py
@@ -5,8 +5,8 @@
     url(r'^$', views.index, name='index'),
 
     url(r'^sites/?$', views.all_sites, name='all_sites'),
-    url(r'^site/(?P<site_domain>[\w\./-]+)/?$', views.site_details, name='site_details'),
-    url(r'^site/(?P<site_domain>[\w\./-]+)/edit$', views.SiteUpdate.as_view(), name='site_edit'),
+    url(r'^site/(?P<site_domain>[\w\.-]+)/?$', views.site_details, name='site_details'),
+    url(r'^site/(?P<site_domain>[\w\.-]+)/edit$', views.SiteUpdate.as_view(), name='site_edit'),
     url(r'^sites/add/$', views.SiteCreate.as_view(), name='site_add'),
 
     url(r'^airports/?$', views.all_airports, name='all_airports'),
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -6,7 +6,7 @@
 from django.contrib import messages
 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
 from django.core.urlresolvers import reverse
-from django.db.models import Count, Q
+from django.db.models import Count, Q, Model
 from django.shortcuts import redirect, render, get_object_or_404
 from django.views.generic.base import ContextMixin
 from django.views.generic.edit import CreateView, UpdateView
@@ -34,16 +34,40 @@
 #------------------------------------------------------------
 
 
-class UpdateViewContext(UpdateView, ContextMixin):
+class CreateViewContext(CreateView):
     """
-    Class-based view inheriting ``UpdateView`` and ``ContextMixin``.  This
-    makes it possible to specify variables used in all AMY's update views,
-    for example: title.
+    Class-based view for creating objects that extends default template context
+    by adding model class used in objects creation.
+    """
+
+    def get_context_data(self, **kwargs):
+        context = super(CreateViewContext, self).get_context_data(**kwargs)
+
+        # self.model is available in CreateView as the model class being
+        # used to create new model instance
+        context['model'] = self.model
+
+        if self.model and issubclass(self.model, Model):
+            context['title'] = 'New {}'.format(self.model._meta.verbose_name)
+        else:
+            context['title'] = 'New object'
+
+        return context
+
+
+class UpdateViewContext(UpdateView):
+    """
+    Class-based view for updating objects that extends default template context
+    by adding proper page title.
     """
 
     def get_context_data(self, **kwargs):
         context = super(UpdateViewContext, self).get_context_data(**kwargs)
 
+        # self.model is available in UpdateView as the model class being
+        # used to update model instance
+        context['model'] = self.model
+
         # self.object is available in UpdateView as the object being currently
         # edited
         context['title'] = str(self.object)
@@ -88,12 +112,12 @@ def site_details(request, site_domain):
     return render(request, 'workshops/site.html', context)
 
 
-class SiteCreate(CreateView):
+class SiteCreate(CreateViewContext):
     model = Site
     fields = SITE_FIELDS
 
 
-class SiteUpdate(UpdateView):
+class SiteUpdate(UpdateViewContext):
     model = Site
     fields = SITE_FIELDS
     slug_field = 'domain'
@@ -122,12 +146,12 @@ def airport_details(request, airport_iata):
     return render(request, 'workshops/airport.html', context)
 
 
-class AirportCreate(CreateView):
+class AirportCreate(CreateViewContext):
     model = Airport
     fields = AIRPORT_FIELDS
 
 
-class AirportUpdate(UpdateView):
+class AirportUpdate(UpdateViewContext):
     model = Airport
     fields = AIRPORT_FIELDS
     slug_field = 'iata'
@@ -212,7 +236,7 @@ def _upload_person_task_csv(request, uploaded_file):
     return persons_tasks
 
 
-class PersonCreate(CreateView):
+class PersonCreate(CreateViewContext):
     model = Person
     fields = '__all__'
 
@@ -268,12 +292,12 @@ def validate_event(request, event_ident):
     return render(request, 'workshops/validate_event.html', context)
 
 
-class EventCreate(CreateView):
+class EventCreate(CreateViewContext):
     model = Event
     fields = '__all__'
 
 
-class EventUpdate(UpdateView):
+class EventUpdate(UpdateViewContext):
     model = Event
     fields = '__all__'
     pk_url_kwarg = 'event_ident'
@@ -303,12 +327,12 @@ def task_details(request, event_slug, person_id, role_name):
     return render(request, 'workshops/task.html', context)
 
 
-class TaskCreate(CreateView):
+class TaskCreate(CreateViewContext):
     model = Task
     fields = TASK_FIELDS
 
 
-class TaskUpdate(UpdateView):
+class TaskUpdate(UpdateViewContext):
     model = Task
     fields = TASK_FIELDS
     pk_url_kwarg = 'task_id'
@@ -349,12 +373,12 @@ def cohort_details(request, cohort_name):
     return render(request, 'workshops/cohort.html', context)
 
 
-class CohortCreate(CreateView):
+class CohortCreate(CreateViewContext):
     model = Cohort
     fields = COHORT_FIELDS
 
 
-class CohortUpdate(UpdateView):
+class CohortUpdate(UpdateViewContext):
     model = Cohort
     fields = COHORT_FIELDS
     slug_field = 'name'


Test patch: