Commit 99365e88 authored by Kostas Papadimitriou's avatar Kostas Papadimitriou Committed by Giorgos Korfiatis

astakos: A bit of refactoring of projects views

- Update project views to work with the updated projects logic
- Use api.projects methods when applicable
- Common project view decorator
- Common view for app/project details
parent 0cdb544f
......@@ -30,6 +30,9 @@
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import re
import synnefo.util.date as date_util
from random import random
from datetime import datetime
......@@ -731,7 +734,7 @@ class ProjectApplicationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
self.precursor_application = instance
super(ProjectApplicationForm, self).__init__(*args, **kwargs)
# in case of new application remove closed join policy
if not instance:
......@@ -741,7 +744,7 @@ class ProjectApplicationForm(forms.ModelForm):
def clean_start_date(self):
start_date = self.cleaned_data.get('start_date')
if not self.precursor_application:
if not self.instance:
today = datetime.now()
today = datetime(today.year, today.month, today.day)
if start_date and (start_date - today).days < 0:
......@@ -780,21 +783,33 @@ class ProjectApplicationForm(forms.ModelForm):
def resource_policies(self):
policies = []
append = policies.append
resource_indexes = {}
include_diffs = False
existing_policies = []
if self.instance and self.instance.pk:
include_diffs = True
existing_policies = self.instance.resource_set
for name, value in self.data.iteritems():
if not value:
continue
uplimit = value
if name.endswith('_uplimit'):
subs = name.split('_uplimit')
prefix, suffix = subs
is_project_limit = name.endswith('_p_uplimit')
suffix = '_p_uplimit' if is_project_limit else '_m_uplimit'
uplimit = value
prefix, _suffix = name.split(suffix)
try:
resource = Resource.objects.get(name=prefix)
except Resource.DoesNotExist:
raise forms.ValidationError("Resource %s does not exist" %
resource.name)
# keep only resource limits for selected resource groups
if self.data.get('is_selected_%s' %
resource.group, "0") == "1":
if self.data.get('is_selected_%s' % \
resource.group, "0") == "1":
if not resource.ui_visible:
raise forms.ValidationError("Invalid resource %s" %
resource.name)
......@@ -804,10 +819,66 @@ class ProjectApplicationForm(forms.ModelForm):
except ValueError:
m = "Limit should be an integer"
raise forms.ValidationError(m)
display = units.show(uplimit, resource.unit)
d.update(dict(resource=prefix, uplimit=uplimit,
display_uplimit=display))
append(d)
existing = resource_indexes.get(prefix)
diff_data = None
if include_diffs:
try:
policy = existing_policies.get(resource=resource)
if is_project_limit:
pval = policy.project_capacity
else:
pval = policy.member_capacity
if pval != uplimit:
diff = pval - uplimit
diff_data = {
'prev': pval,
'prev_display': units.show(pval,
resource.unit),
'diff': diff,
'diff_display': units.show(abs(diff),
resource.unit),
'increased': diff < 0,
'operator': '+' if diff < 0 else '-'
}
except:
pass
if is_project_limit:
d.update(dict(resource=prefix,
p_uplimit=uplimit,
display_p_uplimit=display))
if diff_data:
d.update(dict(resource=prefix, p_diff=diff_data))
if not existing:
d.update(dict(resource=prefix, m_uplimit=0,
display_m_uplimit=units.show(0,
resource.unit)))
else:
d.update(dict(resource=prefix, m_uplimit=uplimit,
display_m_uplimit=display))
if diff_data:
d.update(dict(resource=prefix, m_diff=diff_data))
if not existing:
d.update(dict(resource=prefix, p_uplimit=0,
display_p_uplimit=units.show(0,
resource.unit)))
if resource_indexes.get(prefix, None) is not None:
# already included in policies
existing.update(d)
else:
# keep track of resource dicts
append(d)
resource_indexes[prefix] = d
ordered_keys = presentation.RESOURCES['resources_order']
......@@ -823,22 +894,55 @@ class ProjectApplicationForm(forms.ModelForm):
def cleaned_resource_policies(self):
policies = {}
for d in self.resource_policies:
if self.instance.pk:
if not d.get('p_diff', None) and not d.get('m_diff', None):
continue
policies[d["name"]] = {
### TEMPORARY HACK !!!
"project_capacity": d["uplimit"],
"member_capacity": d["uplimit"]
"project_capacity": d.get("p_uplimit", 0),
"member_capacity": d.get("m_uplimit", 0)
}
if len(policies.keys()) == 0:
return {}
return policies
def save(self, commit=True, **kwargs):
def fill_api_data(self):
data = dict(self.cleaned_data)
is_new = self.instance.id is None
data['project_id'] = self.instance.chain.id if not is_new else None
data['owner'] = self.user if is_new else self.instance.owner
if isinstance(self.instance, Project):
data['project_id'] = self.instance.id
else:
data['project_id'] = self.instance.chain.id if not is_new else None
data['owner'] = self.user.uuid if is_new else self.instance.owner.uuid
data['resources'] = self.cleaned_resource_policies()
data['request_user'] = self.user
submit_application(**data)
if data.get('start_date', None):
data['start_date'] = date_util.isoformat(data.get('start_date'))
data['end_date'] = date_util.isoformat(data.get('end_date'))
data['max_members'] = data.get('limit_on_members_number')
return data
def save(self, commit=True, **kwargs):
from astakos.api import projects as api
data = self.fill_api_data()
return api.submit_new_project(data, self.user)
class ProjectModificationForm(ProjectApplicationForm):
class Meta:
model = Project
fields = ('name', 'homepage', 'description',
'end_date', 'comments', 'member_join_policy',
'member_leave_policy', 'limit_on_members_number')
def save(self, commit=True, **kwargs):
from astakos.api import projects as api
data = self.fill_api_data()
return api.submit_modification(data, self.user, self.instance.uuid)
class ProjectSortForm(forms.Form):
......@@ -880,9 +984,9 @@ class AddProjectMembersForm(forms.Form):
required=True,)
def __init__(self, *args, **kwargs):
chain_id = kwargs.pop('chain_id', None)
if chain_id:
self.project = Project.objects.get(id=chain_id)
project_id = kwargs.pop('project_id', None)
if project_id:
self.project = Project.objects.get(id=project_id)
self.request_user = kwargs.pop('request_user', None)
super(AddProjectMembersForm, self).__init__(*args, **kwargs)
......@@ -893,7 +997,7 @@ class AddProjectMembersForm(forms.Form):
raise forms.ValidationError(e)
q = self.cleaned_data.get('q') or ''
users = q.split(',')
users = re.split("\r\n|\n|,", q)
users = list(u.strip() for u in users if u)
db_entries = AstakosUser.objects.accepted().filter(email__in=users)
unknown = list(set(users) - set(u.email for u in db_entries))
......@@ -905,10 +1009,7 @@ class AddProjectMembersForm(forms.Form):
def get_valid_users(self):
"""Should be called after form cleaning"""
try:
return self.valid_users
except:
return ()
return self.valid_users
class ProjectMembersSortForm(forms.Form):
......
......@@ -575,6 +575,13 @@ def leave_project_checks(membership, request_user):
raise ProjectConflict(m)
def can_cancel_join_request(project, user):
m = user.get_membership(project)
if m is None:
return False
return m.state in [m.REQUESTED]
def can_leave_request(project, user):
m = user.get_membership(project)
if m is None:
......
......@@ -271,6 +271,9 @@ APPLICATION_CANNOT_DENY = "Cannot deny application %s in state '%s'"
APPLICATION_CANNOT_DISMISS = "Cannot dismiss application %s in state '%s'"
APPLICATION_CANNOT_CANCEL = "Cannot cancel application %s in state '%s'"
APPLICATION_CANCELLED = "Your project application has been cancelled."
APPLICATION_APPROVED = "Project application has been approved."
APPLICATION_DENIED = "Project application has been denied."
APPLICATION_DISMISSED = "Project application has been dismissed."
REACHED_PENDING_APPLICATION_LIMIT = ("You have reached the maximum number "
"of pending project applications: %s.")
UNINITIALIZED_NO_MODIFY = "Cannot modify: project %s is not initialized."
......
......@@ -1368,9 +1368,8 @@ class ProjectApplication(models.Model):
return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
@property
def grants(self):
return self.projectresourcegrant_set.values('member_capacity',
'resource__name')
def resource_set(self):
return self.projectresourcegrant_set.order_by('resource__name')
@property
def resource_policies(self):
......@@ -1496,6 +1495,31 @@ class ProjectResourceGrant(models.Model):
def display_member_capacity(self):
return units.show(self.member_capacity, self.resource.unit)
def display_project_capacity(self):
return units.show(self.project_capacity, self.resource.unit)
def project_diffs(self):
project = self.project_application.chain
try:
project_resource = project.resource_set.get(resource=self.resource)
except ProjectResourceQuota.DoesNotExist:
return [self.project_capacity, self.member_capacity]
project_diff = \
self.project_capacity - project_resource.project_capacity
member_diff = self.member_capacity - project_resource.member_capacity
return [project_diff, member_diff]
def display_project_diff(self):
proj, member = self.project_diffs()
proj_abs, member_abs = abs(proj), abs(member)
unit = self.resource.unit
def disp(v):
sign = u'+' if v >= 0 else u'-'
return sign + unicode(units.show(v, unit))
return map(disp, [proj_abs, member_abs])
def __str__(self):
return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
self.display_member_capacity())
......@@ -1747,8 +1771,10 @@ class Project(models.Model):
def set_deleted(self, actor=None, reason=None):
self.set_state(self.DELETED, actor=actor, reason=reason)
### Logical checks
def can_modify(self):
return self.state not in [self.UNINITIALIZED, self.DELETED]
### Logical checks
@property
def is_alive(self):
return self.state in [self.NORMAL, self.SUSPENDED]
......@@ -1792,6 +1818,10 @@ class Project(models.Model):
policy = self.member_leave_policy
return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
@property
def resource_set(self):
return self.projectresourcequota_set.order_by('resource__name')
def create_project(**kwargs):
if "uuid" not in kwargs:
......@@ -1819,6 +1849,12 @@ class ProjectResourceQuota(models.Model):
class Meta:
unique_together = ("resource", "project")
def display_member_capacity(self):
return units.show(self.member_capacity, self.resource.unit)
def display_project_capacity(self):
return units.show(self.project_capacity, self.resource.unit)
class ProjectLogManager(models.Manager):
def last_deactivations(self, projects):
......
......@@ -341,6 +341,5 @@ def qh_sync_new_resource(resource):
resource=resource,
project_capacity=limit,
member_capacity=limit))
ProjectResourceQuota.objects.bulk_create(entries)
qh_sync_projects(projects, resource=resource.name)
......@@ -211,4 +211,10 @@ h2 .header-actions { float: right; font-size: 0.8em;}
display: block;
color:#F24E53;
font-family: monospace;
}
\ No newline at end of file
}
.policy-diff.green {
color: green;
}
.policy-diff.red {
color: red;
}
......@@ -273,7 +273,10 @@ dl.alt-style dd { overflow:hidden; position:relative;}
.projects h3 .rt-action { float:right;}
.projects .submit-rt { margin:0; text-align:right; }
.projects +.buttons-list.fixpos { left:0; right:auto; }
.project-actions a { font-size: 0.7em }
.project-actions .msg-wrap::after { content:'-'; }
.project-actions .msg-wrap:last-child::after { content:''; }
.back-to-action { display: block; position: absolute; top: -20px; font-size: 0.8em }
/*.project-actions a { font-size: 0.7em }*/
.project-actions a.inactive { color:#ccc; cursor: default}
.project-actions a.inactive:hover { text-decoration: none;}
.projects dl.alt-style .faint { color:#ccc; font-style: italic}
......
......@@ -43,6 +43,7 @@ function group_form_toggle_resources(el){
function bytesToSize2(bytes) {
var sizes = [ 'n/a', 'bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = +Math.floor(Math.log(bytes) / Math.log(1024));
if (!isFinite(i)) { return 0 + 'KB'}
return (bytes / Math.pow(1024, i)).toFixed( 0 ) + sizes[ isNaN( bytes ) ? 0 : i+1 ];
}
......@@ -270,34 +271,27 @@ $(document).ready(function() {
$('.group input[name$="_uplimit_proxy"]').each(function() {
$('.group input[name$="_m_uplimit_proxy"], .group input[name$="_p_uplimit_proxy"]').each(function() {
if ($(this).val()){
// get value from input
var value = $(this).val();
// get hidden input name
hidden_name = $(this).attr('name');
hidden_field_name = hidden_name.replace("_proxy","");
hidden_field_name = hidden_name.replace("_proxy", "");
$("input[name='"+hidden_field_name+"']").val(value);
var field = $(this);
if ( (field.hasClass('dehumanize')) && !($(this).parents('.form-row').hasClass('with-errors'))) {
// for dehumanize fields transform bytes to KB, MB, etc
// unless there is an error
field.val(bytesToSize2(value))
field.val(bytesToSize2(value) || 0)
} else {
// else just return the value
field.val(value);
}
var group_class = field.parents('div[class^="group"]').attr('class').replace('group ', '');
// select group icon
$('.quotas-form ul li a').each(function() {
......@@ -311,18 +305,13 @@ $(document).ready(function() {
$("input[name='"+hidden_name+"']").val("1");
group_form_show_resources($(this));
}
});
// if the field has class error, transfer error to the proxy fields
if ( $(this).parents('.form-row').hasClass('with-errors') ) {
field.parents('.form-row').addClass('with-errors');
}
}
});
......@@ -375,4 +364,4 @@ $(document).ready(function() {
});
});
\ No newline at end of file
});
......@@ -40,7 +40,7 @@ from django_tables2 import A
import django_tables2 as tables
from astakos.im.models import *
from astakos.im.templatetags.filters import truncatename
from astakos.im.util import truncatename
from astakos.im.functions import can_join_request, membership_allowed_actions
......@@ -187,19 +187,19 @@ def action_extra_context(project, table, self):
allowed = membership_allowed_actions(membership, user)
if 'leave' in allowed:
url = reverse('astakos.im.views.project_leave',
args=(membership.id,))
args=(membership.project.uuid,))
action = _('Leave')
confirm = True
prompt = _('Are you sure you want to leave from the project?')
elif 'cancel' in allowed:
url = reverse('astakos.im.views.project_cancel_member',
args=(membership.id,))
url = reverse('project_cancel_join',
args=(project.uuid,))
action = _('Cancel')
confirm = True
prompt = _('Are you sure you want to cancel the join request?')
if can_join_request(project, user, membership):
url = reverse('astakos.im.views.project_join', args=(project.id,))
url = reverse('project_join', args=(project.uuid,))
action = _('Join')
confirm = True
prompt = _('Are you sure you want to join this project?')
......@@ -245,10 +245,10 @@ class UserProjectsTable(UserTable):
caption = _('My projects')
name = LinkColumn('astakos.im.views.project_detail',
name = LinkColumn('project_detail',
coerce=lambda x: truncatename(x, 25),
append=project_name_append,
args=(A('id'),),
args=(A('uuid'),),
orderable=False,
accessor='realname')
......@@ -289,7 +289,7 @@ class UserProjectsTable(UserTable):
if c > 0:
pending_members_url = reverse(
'project_pending_members',
kwargs={'chain_id': record.id})
kwargs={'project_uuid': record.uuid})
pending_members = "<i class='tiny'> - %d %s</i>" % (
c, _('pending'))
......@@ -302,7 +302,7 @@ class UserProjectsTable(UserTable):
(pending_members_url, c, _('pending')))
append = mark_safe(pending_members)
members_url = reverse('project_approved_members',
kwargs={'chain_id': record.id})
kwargs={'project_uuid': record.uuid})
members_count = len(self.accepted.get(project.id, []))
if self.user.owns_project(record) or self.user.is_project_admin():
members_count = '<a href="%s">%d</a>' % (members_url,
......@@ -326,21 +326,22 @@ def member_action_extra_context(membership, table, col):
return context
if membership.state == ProjectMembership.REQUESTED:
urls = ['astakos.im.views.project_reject_member',
'astakos.im.views.project_accept_member']
urls = ['project_reject_member',
'project_accept_member']
actions = [_('Reject'), _('Accept')]
prompts = [_('Are you sure you want to reject this member?'),
_('Are you sure you want to accept this member?')]
confirms = [True, True]
if membership.state in ProjectMembership.ACCEPTED_STATES:
urls = ['astakos.im.views.project_remove_member']
urls = ['project_remove_member']
actions = [_('Remove')]
prompts = [_('Are you sure you want to remove this member?')]
confirms = [True, True]
for i, url in enumerate(urls):
context.append(dict(url=reverse(url, args=(membership.pk,)),
context.append(dict(url=reverse(url, args=(membership.project.uuid,
membership.pk,)),
action=actions[i], prompt=prompts[i],
confirm=confirms[i]))
return context
......
{% if field.name in filter_fields %}
<div class="form-row
{% if field.errors|length %}with-errors{% endif %}
{% if field.is_hidden %}with-hidden{% endif %}">
{{ field.errors }}
<p class="clearfix {% if field.blank %}required{% endif %}">
{{ field.label_tag }}
{{ field|safe }}
<span class="extra-img">&nbsp;</span>
{% if field.help_text %}
<span class="info">
<em>more info</em>
<span>{{ field.help_text|safe }}</span>
</span>
{% endif %}
</p>
</div>
{% endif %}
{% load astakos_tags filters %}
<fieldset class="quota">
<legend>
{% if rinfo.is_abbreviation %}
{{ rinfo.verbose_name|upper }}
{% else %}
{{ rinfo.verbose_name|capfirst }}
{% endif %}
<span class="info">
<em>more info</em>
<span>{{ rinfo.help_text }}</span>
</span>
</legend>
<div class="form-row">
<p class="clearfix">
<label for="id_{{resource}}_p_uplimit_proxy" >
Total {{rinfo.pluralized_display_name}}
</label>
<input type="text"
id="id_{{resource}}_p_uplimit_proxy"
name="{{ resource }}_p_uplimit_proxy"
placeholder="{{ rinfo.placeholder }}"
{% if rinfo.unit == 'bytes' %}
class="dehumanize"
{% endif %}
{% if request.POST %}
{% with resource|add:'_p_uplimit' as input_value %}
value = "{{ request.POST|lookup:input_value }}"
{% endwith %}
{% else %}
{% with value=object|get_project_resource_grant_value:resource %}
{% if value %}
value = "{{ value }}"
{% else %}
value = ""
{% endif %}
{% endwith %}
{% endif %}
autocomplete="off">
<span class="extra-img">&nbsp;</span>
<span class="info"><em>more info</em>
<span>{{ rinfo.help_text_input_each }}</span></span>
</p>
<p class="clearfix">
<label for="id_{{resource}}_m_uplimit_proxy" >
Per user {{rdata.pluralized_display_name}}
</label>
<input type="text"
id="id_{{resource}}_m_uplimit_proxy"
name="{{ resource }}_m_uplimit_proxy"
placeholder="{{ rinfo.placeholder }}"
{% if rinfo.unit == 'bytes' %}
class="dehumanize"
{% endif %}
{% if request.POST %}
{% with resource|add:'_m_uplimit' as input_value %}
value = "{{ request.POST|lookup:input_value }}"
{% endwith %}
{% else %}
{% with value=object|get_member_resource_grant_value:resource %}
{% if value %}
value = "{{ value }}"
{% endif %}
{% endwith %}
{% endif %}
autocomplete="off">
<span class="extra-img">&nbsp;</span>
<span class="info"><em>more info</em>
<span>{{ rinfo.help_text_input_each }}</span></span>
</p>
<p class="error-msg">Invalid format</p>
<p class="msg"></p>
</div>
</fieldset>
{% load astakos_tags filters %}
<ul class="clearfix">
{% with object|selected_resource_groups as selected_groups %}
{% for group, info in resource_groups_dict.items %}
<li>
<a href="#{{group}}" id="group_{{group}}"
{% if group in selected_groups %}class="selected"{% endif %}>
<img src="{{ IM_STATIC_URL }}images/create-{{group}}.png"
alt="{{ group }}" />
</a>
<input type="hidden" name="is_selected_{{group}}"
id="proxy_id_is_selected_{{group}}"
{% if group in selected_groups %}value="1"{% else %}value="0"{% endif %}/>
<p class="msg">{{ info.help_text }}</p>
</li>
{% endfor %}
{% endwith %}
</ul>
{% load astakos_tags filters %}
{% for group, resources in resource_catalog_dict.items %}
{% for resource, rinfo in resources.items %}
{% with type="hidden" %}
{% with value=object|get_member_resource_grant_value:resource %}
<input type="{{ type }}"
id="id_{{resource}}_m_uplimit"
name="{{resource}}_m_uplimit"
{% if value %}value="{{value}}"{% endif %} />
{% endwith %}
{% with value=object|get_project_resource_grant_value:resource %}
<input type="{{ type }}"
id="id_{{resource}}_p_uplimit"
name="{{resource}}_p_uplimit"
{% if value %}value="{{value}}"{% endif %} />
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
<div class="visible">&nbsp;</div>
<div class="not-visible">
{% for group, resources in resource_catalog_dict.items %}
<div class="group group_{{group}}" id="{{ group }}">
<a href="#icons" class="delete">X remove resource</a>
{% for resource, rinfo in resources.items %}
{% include "im/projects/_form_resource_field.html" %}
{% endfor %}
</div>
{% endfor %}
</div>
{% load astakos_tags i18n %}
<!-- make room for buttons -->
{% if owner_mode or admin_mode %}
<br />
{% endif %}
<div class="project-actions">
{% if owner_mode %}
{% if project.is_initialized %}
{% if object.can_approve %}