Skip to content
This repository has been archived by the owner on Jan 8, 2021. It is now read-only.

Issue #265 - user panel control #288

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1589264
Basic start for issue #265 - added form to enable/disable homepage pa…
Sep 10, 2012
b72a369
Upstream merge.
Sep 10, 2012
053e7ff
Panel ordering.
Sep 10, 2012
4b07a40
Remade form model with metaclass, changed Panels model more similiar …
Sep 13, 2012
c9bcc0f
Remove debugging.
Sep 13, 2012
9ca619a
Merge remote-tracking branch 'upstream/master'
Sep 13, 2012
5acfcbd
Improved consistency with Issue #278.
Sep 16, 2012
f26f03c
Merge remote-tracking branch 'upstream/master'
Sep 16, 2012
ffcc520
Added number_of_columns support to /panels/collapse/
Sep 16, 2012
cccd9ce
Moved dependency checking to client side.
Sep 23, 2012
86e5a0e
Dependency checking completely client side.
Sep 24, 2012
4f69062
Upstream merge, json filter.
Sep 25, 2012
9b76a6a
Validation JavaScript.
Sep 25, 2012
5b281bf
Merge remote-tracking branch 'upstream/master'
Sep 25, 2012
101b62e
Consistent panel model, improvements.
Sep 26, 2012
a9d117c
Quality improvements, panel form dependency feedback.
Sep 29, 2012
6a06c73
Upstream merge.
Sep 29, 2012
30f2295
Improved panels control.
Oct 5, 2012
88cc1c0
Upstream merge.
Oct 5, 2012
117fa2f
Code clarity.
Oct 13, 2012
5a3650c
Merge remote-tracking branch 'upstream/master'
Oct 13, 2012
c439d20
Panels administration.
Oct 13, 2012
b8352d1
Ghost panel inputs.
Oct 22, 2012
39053de
Merge remote-tracking branch 'upstream/master'
Oct 22, 2012
87213c6
Unneccesary.
Oct 22, 2012
659b606
BooleanField, improved input detection.
Oct 27, 2012
444243d
No debug.
Oct 27, 2012
9b257c9
No debug.
Oct 27, 2012
6ae8f90
Revert.
Oct 27, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions piplmesh/account/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django import forms
from django.core import exceptions
from django.forms.extras import widgets
from django.utils.translation import ugettext_lazy as _

from piplmesh import panels
from piplmesh.account import fields, form_fields, models

class UserUsernameForm(forms.Form):
Expand Down Expand Up @@ -165,3 +167,31 @@ def clean_confirmation_token(self):
if not self.user.email_confirmation_token.check_token(confirmation_token):
raise forms.ValidationError(_("The confirmation token is invalid or has expired. Please retry."), code='confirmation_token_incorrect')
return confirmation_token

class PanelFormMetaclass(forms.Form.__metaclass__):
def __new__(cls, name, bases, attrs):
for panel in panels.panels_pool.get_all_panels():
attrs[panel.get_name()] = forms.BooleanField(required=False)

return super(PanelFormMetaclass, cls).__new__(cls, name, bases, attrs)

class PanelForm(forms.Form):
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting approach, but it is a bit hackish. ;-) I much more prefer metaclass approach. Here is done for models, but it is very similar. So you define a function which adds class attributes at creation time. Can you convert your code into it?

Form for selecting homepage panels.
"""

__metaclass__ = PanelFormMetaclass

def clean(self):
cleaned_data = super(PanelForm, self).clean()

for panel_name, panel_enabled in cleaned_data.items():
if not panel_enabled:
continue

panel = panels.panels_pool.get_panel(panel_name)
for dependency in panel.get_dependencies():
if not cleaned_data.get(dependency, False):
raise exceptions.ValidationError(_("Dependencies not satisfied."))

return cleaned_data
76 changes: 66 additions & 10 deletions piplmesh/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from missing import timezone as timezone_missing

from . import fields, utils
from .. import panels
from .. import panels as piplmesh_panels

LOWER_DATE_LIMIT = 366 * 120 # days
USERNAME_REGEX = r'[\w.@+-]+'
Expand Down Expand Up @@ -53,6 +53,24 @@ class TwitterAccessToken(mongoengine.EmbeddedDocument):
key = mongoengine.StringField(max_length=150)
secret = mongoengine.StringField(max_length=150)

class PanelState(mongoengine.EmbeddedDocument):
collapsed = mongoengine.BooleanField(default=False)
column = mongoengine.IntField()
order = mongoengine.IntField()

class Panel(mongoengine.EmbeddedDocument):
"""
This class holds panel instances for a user, their properties and layouts.

:param dict layout: mapping of count of displayed columns (`string`) to `PanelState` objects for that count
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I noticed. This is not really a param. You use this when commenting functions and which parameters they take. In this case this is a class. and even class constructor does not take this parameter.

You might simply move this line above layout definition bellow and just leave it as a in-code-only comment.

"""

layout = mongoengine.MapField(mongoengine.EmbeddedDocumentField(PanelState))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a comment about this field would be useful, to tell that we are mapping between number of columns to PanelState. Do we store number of columns as number or as string?


def get_layout(self, columns_count):
# Transparently provide either existing or default values
return self.layout.get(columns_count, PanelState())

class User(auth.User):
username = mongoengine.StringField(
max_length=30,
Expand Down Expand Up @@ -89,22 +107,16 @@ class User(auth.User):

email_confirmed = mongoengine.BooleanField(default=False)
email_confirmation_token = mongoengine.EmbeddedDocumentField(EmailConfirmationToken)

# TODO: Model for panel settings should be more semantic.
panels_collapsed = mongoengine.DictField()
panels_order = mongoengine.DictField()


panels = mongoengine.MapField(mongoengine.EmbeddedDocumentField(Panel), default=lambda: {panel.get_name(): Panel() for panel in piplmesh_panels.panels_pool.get_all_panels()})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also comment here above, between what this maps.


@models.permalink
def get_absolute_url(self):
return ('profile', (), {'username': self.username})

def get_profile_url(self):
return self.get_absolute_url()

def get_panels(self):
# TODO: Should return only panels user has enabled (should make sure users can enable panels only in the way that dependencies are satisfied)
return panels.panels_pool.get_all_panels()

def is_anonymous(self):
return not self.is_authenticated()

Expand Down Expand Up @@ -187,3 +199,47 @@ def create_user(cls, username, email=None, password=None):
user.set_password(password)
user.save()
return user

def get_layouts(self, columns_count):
return {name: panel.get_layout(columns_count) for name, panel in self.panels.items()}

def get_panels(self):
return map(piplmesh_panels.panels_pool.get_panel, self.panels.keys())

def get_collapsed(self, columns_count):
return {panel: layout.collapsed for panel, layout in self.get_layouts(columns_count).items()}

def set_collapsed(self, columns_count, name, collapsed):
layout = self.panels[name].get_layout(columns_count)
layout.collapsed = collapsed
self.panels[name].layout[columns_count] = layout

def get_columns(self, columns_count):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must say that I don't understand this method. :-)

panels = []
for column, order, name in sorted([(panel.column, panel.order, name) for name, panel in self.get_layouts(columns_count).items() if panel.column is not None]):
assert column >= 0
while len(panels) <= column:
panels.append([])
panels[column].append(name)

return panels

def has_panel(self, name):
return name in self.panels

def set_panels(self, panels):
# Preserve prior settings for kept panels
self.panels = {name: self.panels.get(name, Panel()) for name in panels}

def reorder_panels(self, columns_count, column_ordering):
"""
This method reorders panels for a user.

:param int columns_count: count of columns for which the layout is changed
:param dict column_ordering: mapping of panel names to a (column (`int`), order (`int`)) `tuple`
"""

for panel, layout in self.get_layouts(columns_count).items():
if panel in column_ordering:
layout.column, layout.order = column_ordering[panel]
self.panels[panel].layout[columns_count] = layout
62 changes: 62 additions & 0 deletions piplmesh/account/static/piplmesh/js/panels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
$(document).ready(function () {
var panels_requiredby = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

; missing at the end.


$('#content .panels form').on('submit', function (event) {
$('input[type="checkbox"]:disabled', this).prop('disabled', false);
}).find('input[type="checkbox"]').on('change', function (event) {
validateCheckbox(this);
}).andSelf().find(':checked').change();

function validateCheckbox(checkbox) {
var checkbox = $(checkbox);
var panel = checkbox.attr('name');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I would use prop also here? No?

var checked = checkbox.prop('checked');

$.each(panels_with_dependencies[panel] || [], function (index, dependency) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation.

if (!(dependency in panels_requiredby)) {
panels_requiredby[dependency] = {};
}

if (checked) {
panels_requiredby[dependency][panel] = true;
}
else if (!checked && panel in panels_requiredby[dependency]) {
delete panels_requiredby[dependency][panel];
}

lockCheckbox(dependency);
});
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary empty line.

function lockCheckbox(panel) {
var panel_checkbox = $('#content .panels form input[name="' + panel + '"]');
var panel_locktext = $('.locked', panel_checkbox.parents('li'));
var changed = false;

if (!$.isEmptyObject(panels_requiredby[panel])) {
panel_locktext.text(gettext("Panel is required by:"));
var list = $('<ul/>');
$.each(panels_requiredby[panel], function (panel, depends) {
$('<li/>').text(panel).appendTo(list);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation.

})
list.appendTo(panel_locktext);

panel_checkbox.data('previous_state', panel_checkbox.prop('checked'));
panel_checkbox.prop('checked', true).prop('disabled', true);
changed = true;
}
else {
if (panel_checkbox.prop('disabled')) {
panel_checkbox.prop('disabled', false).prop('checked', panel_checkbox.data('previous_state'));
panel_locktext.text('');
changed = true;
}
}

// Only if it changed, otherwise there's a chance of an infinite loop with mutually dependent panels.
if (changed) {
// Trigger event to recursively check dependencies throughout hierarchy.
panel_checkbox.change();
}
}
});
23 changes: 23 additions & 0 deletions piplmesh/account/templates/form/panels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "form/form.html" %}

{% load i18n frontend %}

{% block form_after_field %}
<div class="text">
<div class="locked">
</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to the end of previous line.

{% with panels_with_dependencies|get:field.name as panel_dependencies %}
<div class="dependencies">
{% if panel_dependencies %}
{% trans "Required panels:" %}
<ul>
{% for panel_dependency in panel_dependencies %}
<li>{{ panel_dependency }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endwith %}
</div>
</li>
{% endblock %}
2 changes: 2 additions & 0 deletions piplmesh/account/templates/user/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
<li{% if request.path|is_active:url_path %} class="current_item"{% endif %}><a href="{{ url_path }}">{% trans "Profile" %}</a></li>
{% url "account" as url_path %}
<li{% if request.path|is_active:url_path %} class="current_item"{% endif %}><a href="{{ url_path }}">{% trans "Account" %}</a></li>
{% url "user_panels" as url_path %}
<li{% if request.path|is_active:url_path %} class="current_item"{% endif %}><a href="{{ url_path }}">{% trans "Panels" %}</a></li>
</ul>
24 changes: 24 additions & 0 deletions piplmesh/account/templates/user/panels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "plain.html" %}

{% load i18n staticfiles frontend %}

{% block title %}{% trans "Panels" %}{% endblock %}

{% block js %}
{{ block.super }}
<script src="{% static "piplmesh/js/panels.js" %}" type="text/javascript"></script>
<script type="text/javascript">
/* <![CDATA[ */
var panels_with_dependencies = {{ panels_with_dependencies|json|safe }};
/* ]]> */
</script>
{% endblock %}

{% block content %}
{% include "user/menu.html" %}
<div class="panels">
{% with form_submit=_("Submit") %}
{% include "form/panels.html" %}
{% endwith %}
</div>
{% endblock %}
23 changes: 23 additions & 0 deletions piplmesh/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import tweepy

from piplmesh.account import forms, models
from piplmesh import panels

import django_browserid
from django_browserid import views as browserid_views
Expand Down Expand Up @@ -402,6 +403,28 @@ def dispatch(self, request, *args, **kwargs):
def get_form(self, form_class):
return form_class(self.request.user, **self.get_form_kwargs())

class PanelView(generic_views.FormView):
template_name = 'user/panels.html'
form_class = forms.PanelForm
success_url = urlresolvers.reverse_lazy('user_panels')

def form_valid(self, form):
user = self.request.user
user.set_panels([panel for panel, enabled in form.cleaned_data.items() if enabled])
user.save()
return super(PanelView, self).form_valid(form)

def get_initial(self):
return {panel: True for panel in self.request.user.panels.keys()}

def get_context_data(self, **kwargs):
context = super(PanelView, self).get_context_data(**kwargs)
context.update({
'panels_with_dependencies': {panel.get_name(): panel.dependencies for panel in panels.panels_pool.get_all_panels()}
})

return context

def logout(request):
"""
After user logouts, redirect her back to the page she came from.
Expand Down
29 changes: 28 additions & 1 deletion piplmesh/frontend/static/piplmesh/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,32 @@ button.browserid-signin:hover {
padding-right: 4px;
}

/* ------------------ PANELS ---------------------------- */
#content .panels form .field {
overflow: auto;
}

#content .panels form .field .text {
font-size: 10px;
text-align: center;
}

#content .panels form .field .text .locked {
color: red;
}

#content .panels form .field .text .dependencies {
color: #006600;
}

#content .panels form .field .input {
float: left;
}

#content .panels form .field .input input {
width: 25px;
}

/* ------------------ FOOTER ---------------------------- */
#footer {
text-align: center;
Expand Down Expand Up @@ -612,7 +638,8 @@ button.browserid-signin:hover {
.account,
#menu_list,
#picture,
.profile {
.profile,
.panels {
display: inline-block;
vertical-align: top;
}
Expand Down
7 changes: 5 additions & 2 deletions piplmesh/frontend/static/piplmesh/js/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ function orderPanels() {
}

function collapsePanels() {
$.getJSON(URLS.panels_collapse, function (data, textStatus, jqXHR) {
$.getJSON(URLS.panels_collapse, {
'number_of_columns': howManyColumns()
}, function (data, textStatus, jqXHR) {
$.each(data, function (name, collapsed) {
if (collapsed) {
$('#panel-' + name + ' .content').css('display', 'none');
Expand Down Expand Up @@ -428,7 +430,8 @@ $(document).ready(function () {

$.post(URLS.panels_collapse, {
'name': name,
'collapsed': collapsed
'collapsed': collapsed,
'number_of_columns': howManyColumns()
});
});

Expand Down
13 changes: 13 additions & 0 deletions piplmesh/frontend/templatetags/frontend.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
from django import template
from django.core import serializers
from django.db.models import query
from django.utils import simplejson

register = template.Library()

@register.filter()
def is_active(current_path, url_path):
return current_path.startswith(url_path)

@register.filter
def json(object):
if isinstance(object, query.QuerySet):
return serializers.serialize('json', object)
return simplejson.dumps(object)

@register.filter
def get(dict, key):
return dict.get(key)
Loading