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 3 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
57 changes: 57 additions & 0 deletions piplmesh/account/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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 @@ -164,3 +165,59 @@ 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

def panel_form_factory():
"""
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?

Function which generates the form for selecting homepage panels.
"""
def __init__(self, *args, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)

# Add information about dependencies to display in template
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this now? Not anymore?

for name in self.fields:
Copy link
Member

Choose a reason for hiding this comment

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

for name, field in self.fields.items(): does not work? Then you would not have to access dict again bellow. And it might be more readable code.

panel = panels.panels_pool.get_panel(name)
self.fields[name].dependencies = panel.get_dependencies()
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 find this ugly. Attaching some external information to the field. This is OK if there is no other way, but in this case there is: you can retrieve this information ad-hoc in clean method. And in template you could simply provide additional data, like dict, which would map between field name and dependencies.

But OK, let it be like this for now. Let see other things.


def clean(self):
cleaned_data = super(forms.Form, self).clean()
add = []

while True:
Copy link
Member

Choose a reason for hiding this comment

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

I would not display errors for missing dependencies for automatically selected dependencies under the later, but they should be listed under initial panel. So if panel A requires panel B which in turn requires panel C, error message should be "Panel A depends on panels B and C, which have been selected."

Copy link
Member

Choose a reason for hiding this comment

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

(And not B nor C have been selected. If one is, then message mentions only the other one.)

if add:
# User has not selected some of the dependencies, so we select them
for val in add:
Copy link
Member

Choose a reason for hiding this comment

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

Replace val with something meaningful. What is meaning of val? Value of what? What does it contain?

cleaned_data[val] = True
self.data[val] = 'on'
add = []

for name, val in cleaned_data.iteritems():
if not val:
continue

panel = panels.panels_pool.get_panel(name)
errors = []
for dep in panel.get_dependencies():
if not cleaned_data[dep]:
errors.append(_("Panel '%s' depends on panel '%s', it has been selected." % (name, dep)))
Copy link
Member

Choose a reason for hiding this comment

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

OK. This is misuse of the API. Here you should or fix dependencies or display an error. You cannot do both.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, but it works nicely. :-) OK.

Copy link
Member

Choose a reason for hiding this comment

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

But, hm. It is not clear to the user if he has to submit form again or not, when dependent panels are automatically selected. With current code, because you are misusing errors, it is not saved until she submits again.

So I see two options:

  • you display and error but don't fix the value automatically
  • you fix the value automatically but use Django messages framework to display an error (or warning), not form errors

For later you will have to have access to request, which you can pass as a constructor to the form (see UserCurrentPasswordForm for example).

add.append(dep)

if errors:
self._errors[name] = self.error_class(errors)

# All requirements satisfied
if not add:
break

return cleaned_data

panel_list = panels.panels_pool.get_all_panels()

properties = {}
for panel in panel_list:
properties[panel.get_name()] = forms.BooleanField(required=False, initial=True)

form = type('PanelForm', (forms.Form,), properties)
form.__init__ = __init__
form.clean = clean

return form
27 changes: 19 additions & 8 deletions piplmesh/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class TwitterAccessToken(mongoengine.EmbeddedDocument):
key = mongoengine.StringField(max_length=150)
secret = mongoengine.StringField(max_length=150)

class UserPanels(mongoengine.EmbeddedDocument):
Copy link
Member

Choose a reason for hiding this comment

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

Can you make model like it is defined here.

So the idea is that you have a list/map of enabled plugins. So anything which is in user object is enabled. Not that you have a flag. There is no reason to have a flag. User adds a panel or removes it. This is it.

panels_collapsed = mongoengine.DictField()
panels_order = mongoengine.DictField()
panels_disabled = mongoengine.ListField()

def get_panels(self):
return [panel for panel \
in panels.panels_pool.get_all_panels() \
if panel not in map(panels.panels_pool.get_panel, self.panels_disabled)]

class User(auth.User):
username = mongoengine.StringField(
max_length=30,
Expand Down Expand Up @@ -83,10 +93,15 @@ 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()

user_panels = mongoengine.EmbeddedDocumentField(UserPanels)

def __init__(self, *args, **kwargs):
super(User, self).__init__(*args, **kwargs)

# If the user has not previously saved any panel data, make sure we have an object to query
if self.user_panels == None:
self.user_panels = UserPanels()

@models.permalink
def get_absolute_url(self):
Expand All @@ -95,10 +110,6 @@ def get_absolute_url(self):
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
39 changes: 39 additions & 0 deletions piplmesh/account/templates/form/panels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{% load i18n %}
Copy link
Member

Choose a reason for hiding this comment

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

OK. This will have to be improved. It is mostly duplication of form.html. This is not good. It should be done in extendable way. But I will do that.

Copy link
Member

Choose a reason for hiding this comment

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

(After I pull this in.)


<form action="{{ form_action }}" method="post" id="panel_form">
{{ form.non_field_errors }}
{% csrf_token %}
{% with next|default:request_get_next|default:"" as next_url %}
<div style="display: none;"><input type="hidden" name="{{ REDIRECT_FIELD_NAME }}" value="{{ next_url }}" /></div>
{% endwith %}
{% if form.fields %}
<ul class="form_fields">
{% for field in form %}
{% if field.errors %}
<li class="field_error">
{{ field.errors }}
</li>
{% endif %}
<li class="field">
<div class="text">
<label for="{{ field.id_for_label }}">
{{ field.label }}
</label>
{% if field.field.dependencies %}
<div class="dependencies">
{% trans "Required panels" %}:
<ul>
{% for dep in field.field.dependencies %}
<li>{{ dep }}</li>
Copy link
Member

Choose a reason for hiding this comment

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

Indentation.

{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="input">{{ field }}</div>
</li>
{% endfor %}
</ul>
{% endif %}
<div class="buttons"><input type="submit" value="{{ form_submit }}" /></div>
</form>
1 change: 1 addition & 0 deletions piplmesh/account/templates/user/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<div id="menu_list">
<p><a href="{% url "profile" username=user.username %}">{% trans "Profile" %}</a></p>
<p id="current_item"><a href="{% url "account" %}">{% trans "Account" %}</a></p>
<p><a href="{% url "user_panels" %}">{% trans "Panels" %}</a></p>
</div>
<div class="registration">
{% with form_submit=_("Submit") %}
Expand Down
18 changes: 18 additions & 0 deletions piplmesh/account/templates/user/panels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "plain.html" %}

{% load future i18n %}

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

{% block content %}
<div id="menu_list">
<p><a href="{% url "profile" username=user.username %}">{% trans "Profile" %}</a></p>
<p><a href="{% url "account" %}">{% trans "Account" %}</a></p>
<p id="current_item"><a href="{% url "user_panels" %}">{% trans "Panels" %}</a></p>
</div>
<div class="panels">
{% with form_submit=_("Submit") %}
{% include "form/panels.html" %}
{% endwith %}
</div>
{% endblock %}
28 changes: 28 additions & 0 deletions piplmesh/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,34 @@ 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.panel_form_factory()
success_url = urlresolvers.reverse_lazy('user_panels')

def form_valid(self, form):
user = self.request.user
user.user_panels.panels_disabled = [key for key, val in form.cleaned_data.iteritems() if not val]
user.save()
messages.success(self.request, _("You have successfully set your panel defaults."))
Copy link
Member

Choose a reason for hiding this comment

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

No need for the message because you are returning to the same page. You would need this if you would be redirecting to some other page and would need to give the user feedback.

return super(PanelView, self).form_valid(form)

def get_initial(self):
user = self.request.user
return dict(zip(user.user_panels.panels_disabled, [False] * len(user.user_panels.panels_disabled)))

def get_form_kwargs(self):
"""
Returns the keyword arguments for instanciating the form,
copying request.POST so we can change users' selection on validation.
"""
kwargs = {'initial': self.get_initial()}
if self.request.method in ('POST', 'PUT'):
Copy link
Member

Choose a reason for hiding this comment

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

Ehm, forms have only POST.

Copy link
Member

Choose a reason for hiding this comment

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

This is not REST API.

kwargs.update({
'data': dict(self.request.POST),
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand how this helps?

Copy link
Member

Choose a reason for hiding this comment

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

Hm, the only thing you are doing here is making data mutable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's so we can change which panels the user ticked if they didn't select all of the prerequisites. If we're not changing the selection anymore (not using form errors) then it'll probably not be neccessary anymore.

Copy link
Member

Choose a reason for hiding this comment

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

There is difference between displaying error messages and selecting dependencies automatically. So this is needed to be able to select dependencies automatically. But I am just wondering here why it is necessary for it to be here and not for example to copy this in view or form constructor.

Copy link
Member

Choose a reason for hiding this comment

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

Anyway, I think we could do this better. Dependencies should be resolved in JavaScript on the client. So user should get feedback there, dynamically.

On the server I would just test if everything is correct and display an error. Error should be possible in practice only if somebody is doing something nasty. In reality JavaScript would take care that everything is correct. But we just check for every case.

So this your approach to require user to submit the form to verify it is so 90'.

})
return kwargs

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

/* ------------------ PANELS ---------------------------- */
#panel_form .field {
overflow: auto;
}

#panel_form .field .text {
float: left;
}

#panel_form .field .text .dependencies {
color: #006600;
font-size: 10px;
text-align: right;
}

#panel_form .field .input {
float: left;
}

#panel_form .field .input input {
width: 25px;
}

/* ------------------ FOOTER ---------------------------- */
#footer {
text-align: center;
Expand Down
2 changes: 1 addition & 1 deletion piplmesh/frontend/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</div>
<div id="panels">
<div class="panels_column">
{% for panel in user.get_panels %}
{% for panel in user.user_panels.get_panels %}
{% render_panel panel %}
{% endfor %}
</div>
Expand Down
1 change: 1 addition & 0 deletions piplmesh/frontend/templates/user/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{% if user.username == object.username %}
<p id="current_item">{% trans "Profile" %}</p>
<p><a href="{% url "account" %}">{% trans "Account" %}</a></p>
<p><a href="{% url "user_panels" %}">{% trans "Panels" %}</a></p>
{% endif %}
</div>
<div id="picture" >
Expand Down
10 changes: 5 additions & 5 deletions piplmesh/frontend/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,27 @@ def send_update_on_new_post(sender, post, request, bundle, **kwargs):

def panels_collapse(request):
if request.method == 'POST':
request.user.panels_collapsed[request.POST['name']] = True if request.POST['collapsed'] == 'true' else False
request.user.user_panels.panels_collapsed[request.POST['name']] = True if request.POST['collapsed'] == 'true' else False
request.user.save()
return http.HttpResponse()
else:
return http.HttpResponse(simplejson.dumps(request.user.panels_collapsed), mimetype='application/json')
return http.HttpResponse(simplejson.dumps(request.user.user_panels.panels_collapsed), mimetype='application/json')

def panels_order(request):
if request.method == 'POST':
panels = []

for name, column in zip(request.POST.getlist('names'), request.POST.getlist('columns')):
column = int(column)
if column == len(panels):
while column >= len(panels):
panels.append([])
panels[column].append(name)

request.user.panels_order[request.POST['number_of_columns']] = panels
request.user.user_panels.panels_order[request.POST['number_of_columns']] = panels
request.user.save()

return http.HttpResponse()
else:
number_of_columns = request.GET['number_of_columns']
panels = request.user.panels_order.get(number_of_columns, [])
panels = request.user.user_panels.panels_order.get(number_of_columns, [])
return http.HttpResponse(simplejson.dumps(panels), mimetype='application/json')
4 changes: 2 additions & 2 deletions piplmesh/panels/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ def __init__(self):
from . import panels_pool

for dependency in self.get_dependencies():
if not panels_pool.panels_pool.has_panel(dependency):
raise exceptions.PanelDependencyNotRegistered("Panel '%s' depends on panel '%s', but later is not registered" % (self.get_name(), dependency))
if not panels_pool.has_panel(dependency):
Copy link
Member

Choose a reason for hiding this comment

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

Are you sure it should be panels_pool.has_panel and not panels_pool.panels_pool.has_panel? This is quite tricky, but here we import first panels_pool module, which has panels_pool attribute. I think it should be panels_pool.panels_pool. Have you tested this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I get

'PanelsPool' object has no property 'panels_pool'

if I try it.

raise exceptions.PanelDependencyNotRegistered("Panel '%s' depends on panel '%s', but the latter is not registered" % (self.get_name(), dependency))
Copy link
Member

Choose a reason for hiding this comment

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

Vau, I didn't know that later is different than latter.


@classmethod
def get_name(cls):
Expand Down
Empty file.
22 changes: 22 additions & 0 deletions piplmesh/panels/dummy_test/panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import random
Copy link
Member

Choose a reason for hiding this comment

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

Why additional panel? You could just in original panel file extend another panel class and register it. To reduce duplication. So, use inheritance.


from django.conf import settings
from django.contrib.webdesign import lorem_ipsum
from django.utils.translation import ugettext_lazy as _

from piplmesh import panels

class DummyTestPanel(panels.BasePanel):
dependencies = ('dummy',)

def get_context(self, context):
context = super(DummyTestPanel, self).get_context(context)

context.update({
'header': _("Dummy test panel"),
'content': '\n\n'.join(lorem_ipsum.paragraphs(random.randint(1, 1))),
})
return context

if settings.DEBUG:
panels.panels_pool.register(DummyTestPanel)
5 changes: 5 additions & 0 deletions piplmesh/panels/dummy_test/templates/panel/dummy/panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "panel/panel.html" %}

{% block content %}
{{ content|linebreaks }}
{% endblock %}
1 change: 1 addition & 0 deletions piplmesh/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
url(r'^account/confirmation/$', account_views.EmailConfirmationSendToken.as_view(), name='email_confirmation_send_token'),
url(r'^account/confirmation/token/(?:(?P<confirmation_token>\w+)/)?$', account_views.EmailConfirmationProcessToken.as_view(), name='email_confirmaton_process_token'),
url(r'^account/setlanguage/$', account_views.set_language, name='set_language'),
url(r'^account/panels/$', account_views.PanelView.as_view(), name='user_panels'),

# RESTful API
url(r'^api/', include(v1_api.urls)),
Expand Down