-
Notifications
You must be signed in to change notification settings - Fork 19
Issue #265 - user panel control #288
base: master
Are you sure you want to change the base?
Changes from 22 commits
1589264
b72a369
053e7ff
4b07a40
c9bcc0f
9ca619a
5acfcbd
f26f03c
ffcc520
cccd9ce
86e5a0e
4f69062
9b76a6a
5b281bf
101b62e
a9d117c
6a06c73
30f2295
88cc1c0
117fa2f
5a3650c
c439d20
b8352d1
39053de
87213c6
659b606
444243d
9b257c9
6ae8f90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.@+-]+' | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = mongoengine.MapField(mongoengine.EmbeddedDocumentField(PanelState)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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()}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
$(document).ready(function () { | ||
var panels_requiredby = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
$('#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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
}); |
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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 %} |
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 %} |
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) |
There was a problem hiding this comment.
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?