Initial patt implementation
105
__init__.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
from django.urls.base import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
|
||||||
|
def back_url(request, addition):
|
||||||
|
return request.path + addition
|
||||||
|
|
||||||
|
|
||||||
|
def url_current(request):
|
||||||
|
return request.GET.get('next', request.get_full_path())
|
||||||
|
|
||||||
|
|
||||||
|
def url_tasklist(request, user_filter_id=None, search_txt=None, common_filter_id=None):
|
||||||
|
if user_filter_id is not None:
|
||||||
|
return reverse('patt-userfilter', kwargs={'user_filter_id': user_filter_id})
|
||||||
|
elif search_txt is not None:
|
||||||
|
return reverse('search') + '?q=%s' % search_txt
|
||||||
|
elif common_filter_id is not None:
|
||||||
|
return reverse('patt-commonfilter', kwargs={'common_filter_id': common_filter_id})
|
||||||
|
else:
|
||||||
|
return reverse('patt-tasklist')
|
||||||
|
|
||||||
|
|
||||||
|
def url_projectlist(request):
|
||||||
|
return reverse('patt-projectlist')
|
||||||
|
|
||||||
|
|
||||||
|
def url_helpview(request, page):
|
||||||
|
return reverse('patt-helpview', kwargs={'page': page})
|
||||||
|
|
||||||
|
|
||||||
|
def url_tasknew(request, project_id):
|
||||||
|
nxt = url_current(request)
|
||||||
|
return reverse('patt-tasknew') + '?next=%s' % nxt + ('&project_id=%s' % project_id if project_id is not None else '')
|
||||||
|
|
||||||
|
|
||||||
|
def url_projectnew(request):
|
||||||
|
nxt = url_current(request)
|
||||||
|
return reverse('patt-projectnew') + '?next=%s' % nxt
|
||||||
|
|
||||||
|
|
||||||
|
def url_taskedit(request, task_id):
|
||||||
|
nxt = url_current(request)
|
||||||
|
return reverse('patt-taskedit', kwargs={'task_id': task_id}) + '?do=edit&next=%s' % nxt
|
||||||
|
|
||||||
|
|
||||||
|
def url_projectedit(request, project_id):
|
||||||
|
nxt = url_current(request)
|
||||||
|
return reverse('patt-projectedit', kwargs={'project_id': project_id}) + '?next=%s' % nxt
|
||||||
|
|
||||||
|
|
||||||
|
def url_commentnew(request, task_id):
|
||||||
|
nxt = url_current(request)
|
||||||
|
return reverse('patt-commentnew', kwargs={'task_id': task_id}) + '?next=%s' % nxt
|
||||||
|
|
||||||
|
|
||||||
|
def url_easysearch(request):
|
||||||
|
return reverse('patt-easysearch')
|
||||||
|
|
||||||
|
|
||||||
|
def url_taskset(request, task_id, **kwargs):
|
||||||
|
nxt = url_current(request)
|
||||||
|
if kwargs.get('priority') is not None:
|
||||||
|
return reverse('patt-taskedit', kwargs={'task_id': task_id}) + '?do=set_priority&priority=%s&next=%s' % (kwargs.get('priority'), nxt)
|
||||||
|
elif kwargs.get('state') is not None:
|
||||||
|
return reverse('patt-taskedit', kwargs={'task_id': task_id}) + '?do=set_state&state=%s&next=%s' % (kwargs.get('state'), nxt)
|
||||||
|
else:
|
||||||
|
raise Exception('Required keyword missing. One of "priority", "state" is not in %s.' % repr(kwargs.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
def url_filteredit(request, search_id=None):
|
||||||
|
if search_id is None:
|
||||||
|
if get_search_query(request) is None:
|
||||||
|
return reverse('patt-filternew')
|
||||||
|
else:
|
||||||
|
return reverse('patt-filternew') + '?q=%s' % get_search_query(request)
|
||||||
|
else:
|
||||||
|
return reverse('patt-filteredit', kwargs={'search_id': search_id})
|
||||||
|
|
||||||
|
|
||||||
|
def url_printview(request):
|
||||||
|
if not is_printview(request):
|
||||||
|
return '?printview'
|
||||||
|
else:
|
||||||
|
return request.path
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_query(request):
|
||||||
|
return request.GET.get('q')
|
||||||
|
|
||||||
|
|
||||||
|
def is_printview(request):
|
||||||
|
return 'printview' in request.GET
|
||||||
|
|
||||||
|
|
||||||
|
def is_projectlistview(request):
|
||||||
|
return request.META['PATH_INFO'].startswith(reverse('patt-projectlist'))
|
||||||
|
|
||||||
|
|
||||||
|
def is_tasklistview(request, search_id=None):
|
||||||
|
if request.META['PATH_INFO'].startswith(url_tasklist('patt-tasklist', search_id)):
|
||||||
|
return True
|
||||||
|
if search_id is None and get_search_query(request) is not None:
|
||||||
|
return True
|
||||||
|
return False
|
208
access.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import logging
|
||||||
|
from .models import Task, Project, Comment, TASKSTATE_CHOICES, TASKS_IN_WORK, PROJECTS_IN_WORK, PRIO_CHOICES
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('ACC')
|
||||||
|
|
||||||
|
|
||||||
|
def read_attachment(request, rel_path):
|
||||||
|
item_type, item_id = rel_path.split('/')[1:3]
|
||||||
|
try:
|
||||||
|
item_id = int(item_id)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if item_type == 'task':
|
||||||
|
acc = acc_task(Task.objects.get(id=item_id), request.user)
|
||||||
|
return acc.read
|
||||||
|
elif item_type == 'comment':
|
||||||
|
acc = acc_task(Comment.objects.get(id=item_id).task, request.user)
|
||||||
|
return acc.read_comments
|
||||||
|
elif item_type == 'project':
|
||||||
|
acc = acc_project(Project.objects.get(id=item_id), request.user)
|
||||||
|
return acc.read
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def modify_attachment(request, rel_path):
|
||||||
|
item_type, item_id = rel_path.split('/')[1:3]
|
||||||
|
try:
|
||||||
|
item_id = int(item_id)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if item_type == 'task':
|
||||||
|
acc = acc_task(Task.objects.get(id=item_id), request.user)
|
||||||
|
return acc.modify or acc.modify_limited
|
||||||
|
elif item_type == 'comment':
|
||||||
|
comment = Comment.objects.get(id=item_id)
|
||||||
|
acc = acc_task(comment.task, request.user)
|
||||||
|
return request.user == comment.user or acc.modify_comment
|
||||||
|
elif item_type == 'project':
|
||||||
|
acc = acc_project(Project.objects.get(id=item_id), request.user)
|
||||||
|
return acc.modify or acc.modify_limited
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class acc_task(object):
|
||||||
|
def __init__(self, task, user):
|
||||||
|
self.task = task
|
||||||
|
self.user = user
|
||||||
|
self.__read__ = None
|
||||||
|
self.__modify__ = None
|
||||||
|
self.__modify_limited__ = None
|
||||||
|
self.__add_comment__ = None
|
||||||
|
self.__modify_comment__ = None
|
||||||
|
self.user_has_leader_rights = user in task.project.role_leader.all() and user.is_staff
|
||||||
|
self.user_has_memeber_rights = user in task.project.role_member.all() and user.is_staff
|
||||||
|
self.user_has_visitor_rights = user in task.project.role_visitor.all() and user.is_staff
|
||||||
|
self.user_has_role_rights = self.user_has_leader_rights or self.user_has_memeber_rights or self.user_has_visitor_rights
|
||||||
|
self.user_is_assigned_user = user == task.assigned_user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def read(self):
|
||||||
|
if self.__read__ is None:
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_task.read: Access granted (Task #%d). User is Superuser.', self.task.id)
|
||||||
|
self.__read__ = True
|
||||||
|
elif self.user_is_assigned_user and self.task.state in TASKS_IN_WORK:
|
||||||
|
logger.debug('acc_task.read: Access granted (Task #%d). User is Taskowner and taskstate is open or finished.', self.task.id)
|
||||||
|
self.__read__ = True
|
||||||
|
elif self.user_has_role_rights:
|
||||||
|
logger.debug('acc_task.read: Access granted (Task #%d). User has a role and is Staff.', self.task.id)
|
||||||
|
self.__read__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_task.read: Access denied (Task #%d).', self.task.id)
|
||||||
|
self.__read__ = False
|
||||||
|
return self.__read__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def read_comments(self):
|
||||||
|
return self.read
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modify_limited(self):
|
||||||
|
if self.__modify_limited__ is None:
|
||||||
|
if self.user_is_assigned_user and self.user.is_staff and self.task.state in TASKS_IN_WORK:
|
||||||
|
logger.debug('acc_task.modify_limited: Access granted (Task #%d). User is Taskowner and taskstate is open or finished.', self.task.id)
|
||||||
|
self.__modify_limited__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_task.modify_limited: Access denied (Task #%d).', self.task.id)
|
||||||
|
self.__modify_limited__ = False
|
||||||
|
return self.__modify_limited__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modify(self):
|
||||||
|
if self.__modify__ is None:
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_task.modify: Access granted (Task #%d). User is Superuser.', self.task.id)
|
||||||
|
self.__modify__ = True
|
||||||
|
elif self.user_has_leader_rights:
|
||||||
|
logger.debug('acc_task.modify: Access granted (Task #%d). User is Projectleader and staff.', self.task.id)
|
||||||
|
self.__modify__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_task.modify: Access denied (Task #%d).', self.task.id)
|
||||||
|
self.__modify__ = False
|
||||||
|
return self.__modify__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def add_comments(self):
|
||||||
|
if self.__add_comment__ is None:
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_task.add_comments: Access granted (Task #%d). User is Superuser.', self.task.id)
|
||||||
|
self.__add_comment__ = True
|
||||||
|
elif (self.user_has_leader_rights or self.user_has_memeber_rights) and self.task.state in TASKS_IN_WORK:
|
||||||
|
logger.debug('acc_task.add_comments: Access granted (Task #%d). User is Staff, has role in the project and the task state is open or finished.', self.task.id)
|
||||||
|
self.__add_comment__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_task.add_comments: Access denied (Task #%d).', self.task.id)
|
||||||
|
self.__add_comment__ = False
|
||||||
|
return self.__add_comment__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modify_comment(self):
|
||||||
|
if self.__modify_comment__ is None:
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_task.modify_comment: Access granted (Task #%d). User is Superuser.', self.task.id)
|
||||||
|
self.__modify_comment__ = True
|
||||||
|
elif self.user_has_leader_rights:
|
||||||
|
logger.debug('acc_task.modify_comment: Access granted (Task #%d). User is Projectleader.', self.task.id)
|
||||||
|
self.__modify_comment__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_task.modify_comment: Access denied (Task #%d).', self.task.id)
|
||||||
|
self.__modify_comment__ = False
|
||||||
|
return self.__modify_comment__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_targetstates(self):
|
||||||
|
if self.modify:
|
||||||
|
rv = [state[0] for state in TASKSTATE_CHOICES]
|
||||||
|
elif self.modify_limited:
|
||||||
|
rv = list(TASKS_IN_WORK)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
rv.pop(rv.index(self.task.state))
|
||||||
|
rv.sort()
|
||||||
|
rv.reverse()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_targetpriority(self):
|
||||||
|
if self.modify:
|
||||||
|
rv = [prio[0] for prio in PRIO_CHOICES]
|
||||||
|
rv.pop(rv.index(self.task.priority))
|
||||||
|
rv.sort()
|
||||||
|
rv.reverse()
|
||||||
|
return rv
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class acc_project(object):
|
||||||
|
def __init__(self, project, user):
|
||||||
|
self.project = project
|
||||||
|
self.user = user
|
||||||
|
self.__modify__ = None
|
||||||
|
self.user_has_leader_rights = user in project.role_leader.all() and user.is_staff
|
||||||
|
self.user_has_memeber_rights = user in project.role_member.all() and user.is_staff
|
||||||
|
self.user_has_visitor_rights = user in project.role_visitor.all() and user.is_staff
|
||||||
|
self.user_has_role_rights = self.user_has_leader_rights or self.user_has_memeber_rights or self.user_has_visitor_rights
|
||||||
|
|
||||||
|
@property
|
||||||
|
def read(self):
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_project.read: Access granted (Project #%d). User is Superuser.', self.project.id)
|
||||||
|
return True
|
||||||
|
elif self.user_has_leader_rights:
|
||||||
|
logger.debug('acc_project.read: Access granted (Project #%d). User is projectleader.', self.project.id)
|
||||||
|
return True
|
||||||
|
elif self.user_has_role_rights and self.project.state in PROJECTS_IN_WORK:
|
||||||
|
logger.debug('acc_project.read: Access granted (Project #%d). User has a role and project is in work.', self.project.id)
|
||||||
|
return True
|
||||||
|
elif len(self.project.task_set.filter(assigned_user=self.user, state__in=TASKS_IN_WORK)) > 0:
|
||||||
|
logger.debug('acc_project.read: Access granted (Project #%d). User has open tasks.', self.project.id)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_project.read: Access denied (Project #%d). User is not authenticated.', self.project.id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modify(self):
|
||||||
|
if self.__modify__ is None:
|
||||||
|
if self.user.is_superuser:
|
||||||
|
logger.debug('acc_project.modify: Access granted (Project #%d). User is Superuser.', self.project.id)
|
||||||
|
self.__modify__ = True
|
||||||
|
elif self.user in self.project.role_leader.all() and self.user.is_staff:
|
||||||
|
logger.debug('acc_project.modify: Access granted (Project #%d). User is Projectleader.', self.project.id)
|
||||||
|
self.__modify__ = True
|
||||||
|
else:
|
||||||
|
logger.debug('acc_project.modify: Access denied (Project #%d).', self.project.id)
|
||||||
|
self.__modify__ = False
|
||||||
|
return self.__modify__
|
||||||
|
|
||||||
|
|
||||||
|
def create_task_possible(user):
|
||||||
|
return len(Project.objects.filter(role_leader__in=[user])) + len(Project.objects.filter(role_member__in=[user])) > 0 and user.is_staff
|
||||||
|
|
||||||
|
|
||||||
|
def create_project_possible(user):
|
||||||
|
return user.is_superuser
|
65
admin.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Project, Task, Comment, Search
|
||||||
|
from simple_history.admin import SimpleHistoryAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'description', 'id', )
|
||||||
|
search_fields = ('name', 'description', 'id', )
|
||||||
|
list_filter = (
|
||||||
|
('state', admin.ChoicesFieldListFilter),
|
||||||
|
('role_leader', admin.RelatedFieldListFilter),
|
||||||
|
('role_member', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAdmin(SimpleHistoryAdmin):
|
||||||
|
list_display = ('name', 'description', 'id', )
|
||||||
|
history_list_display = ('name', 'description', 'state', )
|
||||||
|
search_fields = ('name', 'description', 'id', )
|
||||||
|
list_filter = (
|
||||||
|
('state', admin.ChoicesFieldListFilter),
|
||||||
|
('priority', admin.ChoicesFieldListFilter),
|
||||||
|
('assigned_user', admin.RelatedFieldListFilter),
|
||||||
|
('project', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentAdmin(SimpleHistoryAdmin):
|
||||||
|
list_display = ('task', 'user', 'comment', )
|
||||||
|
history_list_display = ('comment', 'type', )
|
||||||
|
search_fields = ('comment', )
|
||||||
|
list_filter = (
|
||||||
|
('type', admin.ChoicesFieldListFilter),
|
||||||
|
('user', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSettingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('profile', 'view', )
|
||||||
|
search_fields = ('profile', 'view', )
|
||||||
|
list_filter = (
|
||||||
|
('profile', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PattProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', )
|
||||||
|
search_fields = ('user', )
|
||||||
|
list_filter = (
|
||||||
|
('user', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'name', )
|
||||||
|
search_fields = ('user', 'name', )
|
||||||
|
list_filter = (
|
||||||
|
('user', admin.RelatedFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Project, ProjectAdmin)
|
||||||
|
admin.site.register(Task, TaskAdmin)
|
||||||
|
admin.site.register(Comment, CommentAdmin)
|
||||||
|
admin.site.register(Search, SearchAdmin)
|
8
apps.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PattConfig(AppConfig):
|
||||||
|
name = 'patt'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import patt.signals
|
247
context.py
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
from .access import create_project_possible, create_task_possible, acc_task
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from .help import actionbar as actionbar_add_help
|
||||||
|
import inspect
|
||||||
|
import mycreole
|
||||||
|
import patt
|
||||||
|
from .search import common_searches
|
||||||
|
from themes import empty_entry_parameters, color_icon_url, gray_icon_url
|
||||||
|
from users.context import menubar as menubar_users
|
||||||
|
|
||||||
|
ATTACHMENT_UID = 'attachment'
|
||||||
|
BACK_UID = 'back'
|
||||||
|
COMMENTNEW_UID = 'commentnew'
|
||||||
|
CREATE_PROJECT_UID = 'create-project'
|
||||||
|
CREATE_TASK_UID = 'create-task'
|
||||||
|
HELP_UID = 'help'
|
||||||
|
PRINTVIEW_UID = 'printview'
|
||||||
|
TASKEDIT_UID = 'taskedit'
|
||||||
|
VIEW_PROJECTLIST_UID = 'view-projectlist'
|
||||||
|
VIEW_TASKLIST_UID = 'view-tasklist'
|
||||||
|
|
||||||
|
|
||||||
|
def context_adaption(context, request, **kwargs):
|
||||||
|
caller_name = inspect.currentframe().f_back.f_code.co_name
|
||||||
|
try:
|
||||||
|
context.set_additional_title(kwargs.pop('title'))
|
||||||
|
except KeyError:
|
||||||
|
pass # no title in kwargs
|
||||||
|
menubar_users(context[context.MENUBAR], request)
|
||||||
|
menubar(context, request, caller_name, **kwargs)
|
||||||
|
actionbar(context, request, caller_name, **kwargs)
|
||||||
|
navigationbar(context, request)
|
||||||
|
for key in kwargs:
|
||||||
|
context[key] = kwargs[key]
|
||||||
|
|
||||||
|
|
||||||
|
def menubar(context, request, caller_name, **kwargs):
|
||||||
|
bar = context[context.MENUBAR]
|
||||||
|
add_help_menu(request, bar)
|
||||||
|
add_tasklist_menu(request, bar)
|
||||||
|
add_filter_submenu(request, bar, VIEW_TASKLIST_UID)
|
||||||
|
add_projectlist_menu(request, bar)
|
||||||
|
add_printview_menu(request, bar)
|
||||||
|
finalise_bar(request, bar)
|
||||||
|
|
||||||
|
|
||||||
|
def navigationbar(context, request):
|
||||||
|
bar = context[context.NAVIGATIONBAR]
|
||||||
|
add_back_menu(request, bar)
|
||||||
|
finalise_bar(request, bar)
|
||||||
|
|
||||||
|
|
||||||
|
def actionbar(context, request, caller_name, **kwargs):
|
||||||
|
bar = context[context.ACTIONBAR]
|
||||||
|
if caller_name == 'patt_tasklist':
|
||||||
|
if create_task_possible(request.user):
|
||||||
|
add_newtask_menu(request, bar, kwargs.get('project_id'))
|
||||||
|
elif caller_name == 'patt_projectlist':
|
||||||
|
if create_project_possible(request.user):
|
||||||
|
add_newproject_menu(request, bar)
|
||||||
|
elif caller_name == 'patt_taskview':
|
||||||
|
acc = acc_task(kwargs['task'], request.user)
|
||||||
|
if acc.modify or acc.modify_limited:
|
||||||
|
add_edittask_menu(request, bar, kwargs['task'].id)
|
||||||
|
if acc.add_comments:
|
||||||
|
add_newcomment_menu(request, bar, kwargs['task'].id)
|
||||||
|
add_manageupload_menu(request, bar, kwargs['task'])
|
||||||
|
elif caller_name == 'patt_helpview':
|
||||||
|
actionbar_add_help(context, request, **kwargs)
|
||||||
|
finalise_bar(request, bar)
|
||||||
|
|
||||||
|
|
||||||
|
def finalise_bar(request, bar):
|
||||||
|
if len(bar) == 0:
|
||||||
|
bar.append_entry(*empty_entry_parameters(request))
|
||||||
|
|
||||||
|
|
||||||
|
def add_help_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
HELP_UID, # uid
|
||||||
|
_('Help'), # name
|
||||||
|
color_icon_url(request, 'help.png'), # icon
|
||||||
|
patt.url_helpview(request, 'main'), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_tasklist_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
VIEW_TASKLIST_UID, # uid
|
||||||
|
_('Tasklist'), # name
|
||||||
|
color_icon_url(request, 'task.png'), # icon
|
||||||
|
patt.url_tasklist(request), # url
|
||||||
|
True, # left
|
||||||
|
patt.is_tasklistview(request) # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_projectlist_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
VIEW_PROJECTLIST_UID, # uid
|
||||||
|
_('Projectlist'), # name
|
||||||
|
color_icon_url(request, 'folder.png'), # icon
|
||||||
|
patt.url_projectlist(request), # url
|
||||||
|
True, # left
|
||||||
|
patt.is_projectlistview(request) # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_printview_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
PRINTVIEW_UID, # uid
|
||||||
|
_('Printview'), # name
|
||||||
|
color_icon_url(request, 'print.png'), # icon
|
||||||
|
patt.url_printview(request), # url
|
||||||
|
True, # left
|
||||||
|
patt.is_printview(request) # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_newtask_menu(request, bar, project_id):
|
||||||
|
bar.append_entry(
|
||||||
|
CREATE_TASK_UID, # uid
|
||||||
|
_('New Task'), # name
|
||||||
|
color_icon_url(request, 'plus.png'), # icon
|
||||||
|
patt.url_tasknew(request, project_id), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_edittask_menu(request, bar, task_id):
|
||||||
|
bar.append_entry(
|
||||||
|
TASKEDIT_UID, # uid
|
||||||
|
_('Edit'), # name
|
||||||
|
color_icon_url(request, 'edit.png'), # icon
|
||||||
|
patt.url_taskedit(request, task_id), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_newcomment_menu(request, bar, task_id):
|
||||||
|
bar.append_entry(
|
||||||
|
COMMENTNEW_UID, # uid
|
||||||
|
_('Add Comment'), # name
|
||||||
|
color_icon_url(request, 'edit2.png'), # icon
|
||||||
|
patt.url_commentnew(request, task_id), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_newproject_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
CREATE_PROJECT_UID, # uid
|
||||||
|
_('New Project'), # name
|
||||||
|
color_icon_url(request, 'plus.png'), # icon
|
||||||
|
patt.url_projectnew(request), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_manageupload_menu(request, bar, task):
|
||||||
|
bar.append_entry(
|
||||||
|
ATTACHMENT_UID, # uid
|
||||||
|
_("Attachments"), # name
|
||||||
|
color_icon_url(request, 'upload.png'), # icon
|
||||||
|
mycreole.url_manage_uploads(request, task.attachment_target_path), # url
|
||||||
|
True, # left
|
||||||
|
False, # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_back_menu(request, bar):
|
||||||
|
bar.append_entry(
|
||||||
|
BACK_UID, # uid
|
||||||
|
_('Back'), # name
|
||||||
|
gray_icon_url(request, 'back.png'), # icon
|
||||||
|
'javascript:history.back()', # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_filter_submenu(request, bar, menu_uid):
|
||||||
|
bar.append_entry_to_entry(
|
||||||
|
menu_uid,
|
||||||
|
menu_uid + '-easysearch', # uid
|
||||||
|
_('Easysearch'), # name
|
||||||
|
gray_icon_url(request, 'search.png'), # icon
|
||||||
|
patt.url_easysearch(request), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
if patt.get_search_query(request) is not None:
|
||||||
|
bar.append_entry_to_entry(
|
||||||
|
menu_uid,
|
||||||
|
menu_uid + '-save', # uid
|
||||||
|
_('Save Search as Filter'), # name
|
||||||
|
gray_icon_url(request, 'save.png'), # icon
|
||||||
|
patt.url_filteredit(request), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
bar.append_entry_to_entry(
|
||||||
|
menu_uid,
|
||||||
|
menu_uid + '-all', # uid
|
||||||
|
_('All Tasks'), # name
|
||||||
|
gray_icon_url(request, 'task.png'), # icon
|
||||||
|
patt.url_tasklist(request), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
cs = common_searches(request)
|
||||||
|
for common_filter_id in cs:
|
||||||
|
bar.append_entry_to_entry(
|
||||||
|
menu_uid,
|
||||||
|
menu_uid + '-common', # uid
|
||||||
|
_(cs[common_filter_id][0]), # name
|
||||||
|
gray_icon_url(request, 'filter.png'), # icon
|
||||||
|
patt.url_tasklist(request, common_filter_id=common_filter_id), # url
|
||||||
|
True, # left
|
||||||
|
False # active
|
||||||
|
)
|
||||||
|
for s in request.user.search_set.order_by(Lower('name')):
|
||||||
|
active = patt.is_tasklistview(request, s.id)
|
||||||
|
if active is True:
|
||||||
|
url = patt.url_filteredit(request, s.id)
|
||||||
|
else:
|
||||||
|
url = patt.url_tasklist(request, user_filter_id=s.id)
|
||||||
|
if active:
|
||||||
|
icon = 'settings.png'
|
||||||
|
else:
|
||||||
|
icon = 'favourite.png'
|
||||||
|
bar.append_entry_to_entry(
|
||||||
|
menu_uid,
|
||||||
|
menu_uid + '-sub', # uid
|
||||||
|
s.name, # name
|
||||||
|
gray_icon_url(request, icon), # icon
|
||||||
|
url, # url
|
||||||
|
True, # left
|
||||||
|
active # active
|
||||||
|
)
|
45
creole.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.urls.base import reverse
|
||||||
|
from .models import Project
|
||||||
|
|
||||||
|
|
||||||
|
def task_link_filter(text):
|
||||||
|
render_txt = ''
|
||||||
|
while len(text) > 0:
|
||||||
|
try:
|
||||||
|
pos = text.index('[[task:')
|
||||||
|
except ValueError:
|
||||||
|
pos = len(text)
|
||||||
|
render_txt += text[:pos]
|
||||||
|
text = text[pos + 7:]
|
||||||
|
if len(text):
|
||||||
|
pos = text.index(']]')
|
||||||
|
try:
|
||||||
|
task_id = int(text[:pos])
|
||||||
|
except ValueError:
|
||||||
|
render_txt += "[[task:" + text[:pos + 2]
|
||||||
|
else:
|
||||||
|
render_txt += '[[%s|#%d]]' % (reverse('patt-taskview', kwargs={'task_id': task_id}), task_id)
|
||||||
|
text = text[pos + 2:]
|
||||||
|
return render_txt
|
||||||
|
|
||||||
|
|
||||||
|
def tasklist_link_filter(text):
|
||||||
|
render_txt = ''
|
||||||
|
while len(text) > 0:
|
||||||
|
try:
|
||||||
|
pos = text.index('[[tasklist:')
|
||||||
|
except ValueError:
|
||||||
|
pos = len(text)
|
||||||
|
render_txt += text[:pos]
|
||||||
|
text = text[pos + 11:]
|
||||||
|
if len(text):
|
||||||
|
pos = text.index(']]')
|
||||||
|
try:
|
||||||
|
project_id = int(text[:pos])
|
||||||
|
except ValueError:
|
||||||
|
render_txt += "[[tasklist:" + text[:pos + 2]
|
||||||
|
else:
|
||||||
|
p = Project.objects.get(id=project_id)
|
||||||
|
render_txt += '[[%s|%s]]' % (reverse('patt-tasklist-prj', kwargs={'project_id': project_id}), p.name)
|
||||||
|
text = text[pos + 2:]
|
||||||
|
return render_txt
|
137
forms.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from .models import Task, Project, Comment, Search, TASKSTATE_CHOICES, PROJECTSTATE_OPEN, ModelList
|
||||||
|
from .search import INDEX_STATES
|
||||||
|
|
||||||
|
|
||||||
|
class TaskForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# get request from kwargs
|
||||||
|
try:
|
||||||
|
self.request = kwargs.pop('request')
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError("needed request object is missing in kwargs")
|
||||||
|
plist = self.__projectlist_taskedit__(self.request.user)
|
||||||
|
if self.request.POST:
|
||||||
|
project = Project.objects.get(id=self.request.POST.get('project'))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
project = kwargs.get('instance').project
|
||||||
|
except Task.project.RelatedObjectDoesNotExist:
|
||||||
|
if len(plist) > 0:
|
||||||
|
project = plist[0]
|
||||||
|
# init TaskForm
|
||||||
|
super(TaskForm, self).__init__(*args, **kwargs)
|
||||||
|
# set projectchoice for project
|
||||||
|
self.fields['project'].queryset = plist
|
||||||
|
self.fields['project'].empty_label = None
|
||||||
|
# set userlist (projectdepending)
|
||||||
|
self.fields['assigned_user'].empty_label = _('----- choose a user -----')
|
||||||
|
if project is not None:
|
||||||
|
self.fields['assigned_user'].queryset = self.__userlist_taskedit__(self.request.user, project)
|
||||||
|
|
||||||
|
def __projectlist_taskedit__(self, user):
|
||||||
|
if user.is_superuser:
|
||||||
|
rv = Project.objects.all()
|
||||||
|
else:
|
||||||
|
rv = Project.objects.filter(
|
||||||
|
Q(role_leader__id__exact=user.id) | Q(role_member__id__exact=user.id),
|
||||||
|
state=PROJECTSTATE_OPEN,
|
||||||
|
).distinct()
|
||||||
|
rv = ModelList(rv)
|
||||||
|
rv.sort()
|
||||||
|
return Project.objects.filter(pk__in=[p.pk for p in rv])
|
||||||
|
|
||||||
|
def __userlist_taskedit__(self, user, project=None):
|
||||||
|
if user.is_superuser:
|
||||||
|
return User.objects.all().order_by('username')
|
||||||
|
elif user in project.role_leader.all():
|
||||||
|
uids = [u.id for u in project.role_leader.all() | project.role_member.all()]
|
||||||
|
else:
|
||||||
|
uids = [user.id]
|
||||||
|
return User.objects.filter(id__in=set(uids)).order_by('username')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Task
|
||||||
|
fields = ['project', 'assigned_user', 'name', 'state', 'priority', 'targetdate', 'progress', 'description']
|
||||||
|
widgets = {
|
||||||
|
'assigned_user': forms.Select(attrs={'required': True}),
|
||||||
|
'project': forms.Select(attrs={'onchange': 'submit()'}),
|
||||||
|
'state': forms.Select(choices=TASKSTATE_CHOICES),
|
||||||
|
'targetdate': forms.DateInput(format="%Y-%m-%d"),
|
||||||
|
'description': forms.Textarea(attrs={'rows': 5}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskFormLimited(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# remove request from kwargs
|
||||||
|
kwargs.pop('request')
|
||||||
|
super(TaskFormLimited, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Task
|
||||||
|
fields = ['state', 'progress', 'description']
|
||||||
|
widgets = {
|
||||||
|
'state': forms.Select(choices=TASKSTATE_CHOICES[:2]),
|
||||||
|
'description': forms.Textarea(attrs={'rows': 5}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = ['name', 'state', 'role_leader', 'role_member', 'role_visitor', 'description', 'days_late', 'days_very_soon', 'days_soon']
|
||||||
|
widgets = {
|
||||||
|
'description': forms.Textarea(attrs={'rows': 5}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'days_late': _('Days to deadline (late)'),
|
||||||
|
'days_very_soon': _('Days to deadline (very soon)'),
|
||||||
|
'days_soon': _('Days to deadline (soon)'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ProjectForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['role_leader'].queryset = User.objects.order_by('username')
|
||||||
|
self.fields['role_member'].queryset = User.objects.order_by('username')
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['comment']
|
||||||
|
|
||||||
|
|
||||||
|
class TaskCommentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['comment']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(forms.ModelForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['comment'].required = False
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Search
|
||||||
|
fields = ['name', 'search_txt']
|
||||||
|
labels = {
|
||||||
|
'search_txt': _('Search Text'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EasySearchForm(forms.Form):
|
||||||
|
user_ids = forms.MultipleChoiceField(required=False, label=_('Assigned User(s)'), widget=forms.widgets.SelectMultiple(attrs={'size': 6}))
|
||||||
|
states = forms.MultipleChoiceField(required=False, label=_('State(s)'))
|
||||||
|
prj_ids = forms.MultipleChoiceField(required=False, label=_('Project(s)'), widget=forms.widgets.SelectMultiple(attrs={'size': 10}))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EasySearchForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['user_ids'].choices = [(u.id, u.username) for u in User.objects.order_by('username')]
|
||||||
|
self.fields['states'].choices = [(INDEX_STATES.get(task_num), task_name) for task_num, task_name in TASKSTATE_CHOICES]
|
||||||
|
self.fields['prj_ids'].choices = [(p.id, p.name) for p in Project.objects.order_by('state', 'name')]
|
153
help.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
import mycreole
|
||||||
|
import patt
|
||||||
|
from themes import color_icon_url
|
||||||
|
|
||||||
|
# TODO: Search: Describe search fields
|
||||||
|
# TODO: Search: Describe logic operator order and brackets if possible
|
||||||
|
# TODO: Search: Extend Examples with useful features.
|
||||||
|
# TODO: Search for specific content: Describe search possibilities (also in pygal)
|
||||||
|
|
||||||
|
|
||||||
|
HELP_UID = 'help'
|
||||||
|
|
||||||
|
MAIN = mycreole.render_simple(_("""
|
||||||
|
= PaTT
|
||||||
|
|
||||||
|
**PaTT** is a **P**roject **a**nd **T**eamorganisation **T**ool.
|
||||||
|
|
||||||
|
It is designed to store Tasks in relation to Projects and Users.
|
||||||
|
|
||||||
|
== Help
|
||||||
|
* [[creole|Creole Markup Language]]
|
||||||
|
* [[access|Access Control for the site content]]
|
||||||
|
* [[search|Help on Search]]
|
||||||
|
|
||||||
|
== Items
|
||||||
|
|
||||||
|
=== Task properties:
|
||||||
|
* State
|
||||||
|
* Targetdate
|
||||||
|
* Priority
|
||||||
|
* Progress
|
||||||
|
* Name
|
||||||
|
* Description
|
||||||
|
|
||||||
|
=== Project properties:
|
||||||
|
* Project Leaders
|
||||||
|
* Project Members
|
||||||
|
* State
|
||||||
|
* Name
|
||||||
|
* Description
|
||||||
|
|
||||||
|
"""))
|
||||||
|
|
||||||
|
CREOLE = mycreole.mycreole_help_pagecontent()
|
||||||
|
CREOLE += mycreole.render_simple("""
|
||||||
|
= PaTT Markup
|
||||||
|
{{{[[task:number]]}}} will result in a Link to the given tasknumber.
|
||||||
|
|
||||||
|
{{{[[tasklist:number]]}}} will result in a Link to the tasklist of the given projectnumber.
|
||||||
|
""")
|
||||||
|
|
||||||
|
ACCESS = mycreole.render_simple(_("""
|
||||||
|
= Superuser(s)
|
||||||
|
* Are able to view, create and edit everything!
|
||||||
|
* Only a Superuser is able to create a project.
|
||||||
|
= Non-Staff-Users
|
||||||
|
* Are able to read their own tasks, which are in the state "Open" or "Finished" and the related project(s) to these tasks.
|
||||||
|
* They don't get project role permissions (Projectleader, -member, ...), even if they have a role.
|
||||||
|
* They don't get permission to change any content.
|
||||||
|
= Projectleader(s)
|
||||||
|
* Are able to view and edit everything related to the project.
|
||||||
|
* They are able to create tasks for the project for any user with a projectrole.
|
||||||
|
= Projectmember(s)
|
||||||
|
* Are able to view everything related to the project.
|
||||||
|
* They are have limited modify permission to their own task related to that project.
|
||||||
|
* They are able to leave taskcomments at every task related to the project.
|
||||||
|
* They are able to create tasks related to the project for themselves.
|
||||||
|
= Projectvisitor(s)
|
||||||
|
* Are able to view everything related to the project.
|
||||||
|
* They are have limited modify permission to their own task related to that project.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
SEARCH = mycreole.render_simple(_("""
|
||||||
|
= Search
|
||||||
|
The search looks up full words in //Tasknames (name)// and //Taskdescriptions (description)// without giving \
|
||||||
|
special search commands in the search string. The search will result in a tasklist.
|
||||||
|
|
||||||
|
|
||||||
|
=== Task search fields
|
||||||
|
* task_id (NUMERIC):
|
||||||
|
* assigned_user (TEXT):
|
||||||
|
* assigned_user_missing (BOOLEAN):
|
||||||
|
* name (TEXT):
|
||||||
|
* description (TEXT):
|
||||||
|
* state (TEXT):
|
||||||
|
** The state of a Task. It is one of the following states: Open, Finished, Closed, Cancelled
|
||||||
|
* targetdate (DATETIME):
|
||||||
|
|
||||||
|
=== Project related fields
|
||||||
|
* project_id (NUMERIC):
|
||||||
|
* project_name (TEXT):
|
||||||
|
* project_description (TEXT):
|
||||||
|
|
||||||
|
=== Comment related field
|
||||||
|
* comment (TEXT):
|
||||||
|
|
||||||
|
|
||||||
|
== Search syntax (Whoosh)
|
||||||
|
=== Logic operators
|
||||||
|
* AND
|
||||||
|
** **Example:** "foo AND bar" - Search will find all items with foo and bar.
|
||||||
|
* OR
|
||||||
|
** **Example:** "foo OR bar" - Search will find all items with foo, bar or with foo and bar.
|
||||||
|
* NOT
|
||||||
|
** **Example:** "foo NOT bar" - Search will find all items with foo and no bar.
|
||||||
|
=== Search in specific fields
|
||||||
|
A search pattern like //foo:bar// does look for //bar// in the field named //foo//.
|
||||||
|
|
||||||
|
This search pattern can also be combined with other search text via logical operators.
|
||||||
|
=== Search for specific content
|
||||||
|
* **Wildcards:**
|
||||||
|
* **Range:**
|
||||||
|
** From To:
|
||||||
|
** Above:
|
||||||
|
** Below:
|
||||||
|
* **Named constants:**
|
||||||
|
** //now//: Current date
|
||||||
|
** //-[num]y//: Current date minus [num] years
|
||||||
|
** //+[num]mo//: Current date plus [num] months
|
||||||
|
** //-[num]d//: Current date minus [num] days
|
||||||
|
** ...
|
||||||
|
|
||||||
|
== Examples
|
||||||
|
* [[/patt/tasklist/search?q=project_id:1|project_id:1]] gives results with all tasks of project number #1.
|
||||||
|
* [[/patt/tasklist/search?q=project_id:1 AND assigned_user:dirk|project_id:1 AND assigned_user:dirk]] gives results with all tasks of project number #1 which are assigned to 'dirk'.
|
||||||
|
* [[/patt/tasklist/search?q=assigned_user:dirk+AND+targetdate:[2000+to+%2b5d]|assigned_user:dirk AND targetdate:[2000 to +5d] ]] gives results with tasks having a targetdate within the next 5 days and assigned to 'dirk'.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
help_pages = {
|
||||||
|
'main': MAIN,
|
||||||
|
'creole': CREOLE,
|
||||||
|
'access': ACCESS,
|
||||||
|
'search': SEARCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def actionbar(context, request, current_help_page=None, **kwargs):
|
||||||
|
actionbar_entries = (
|
||||||
|
('1', 'Main'),
|
||||||
|
('2', 'Creole'),
|
||||||
|
('3', 'Access'),
|
||||||
|
('4', 'Search'),
|
||||||
|
)
|
||||||
|
for num, name in actionbar_entries:
|
||||||
|
context[context.ACTIONBAR].append_entry(
|
||||||
|
HELP_UID + '-%s' % name.lower(), # uid
|
||||||
|
_(name), # name
|
||||||
|
color_icon_url(request, num + '.png'), # icon
|
||||||
|
patt.url_helpview(request, name.lower()), # url
|
||||||
|
True, # left
|
||||||
|
name.lower() == current_help_page, # active
|
||||||
|
)
|
0
management/__init__.py
Normal file
0
management/commands/__init__.py
Normal file
9
management/commands/rebuild_index.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from patt.search import create_index, rebuild_index
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
ix = create_index()
|
||||||
|
n = rebuild_index(ix)
|
||||||
|
self.stdout.write(self.style.SUCCESS('Search index for %d items created.') % n)
|
76
migrations/0001_initial.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-09-30 16:58
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Project',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('state', models.IntegerField(choices=[(0, 'Open'), (1, 'Closed')], default=0)),
|
||||||
|
('role_leader', models.ManyToManyField(related_name='role_leader', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('role_member', models.ManyToManyField(blank=True, related_name='role_member', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TaskListSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(default='All', max_length=50)),
|
||||||
|
('projectfilter', models.CharField(default='all', max_length=50)),
|
||||||
|
('userfilter', models.CharField(default='all', max_length=50)),
|
||||||
|
('statefilter', models.CharField(default='0,1,2,3', max_length=50)),
|
||||||
|
('displaytype', models.CharField(default='short', max_length=50)),
|
||||||
|
('sorttype', models.CharField(default='date', max_length=50)),
|
||||||
|
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('current_tasklistsetting', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='patt.TaskListSetting')),
|
||||||
|
('my_tasklistsettings', models.ManyToManyField(related_name='my_tasklistsettings', to='patt.TaskListSetting')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Task',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('state', models.IntegerField(choices=[(0, 'Open'), (1, 'Finished'), (2, 'Closed'), (3, 'Canceled')], default=0)),
|
||||||
|
('priority', models.IntegerField(choices=[(1, '1 - Highest'), (2, '2 - High'), (3, '3 - Above-Average'), (4, '4 - Average'), (5, '5 - Below-Average'), (6, '6 - Low'), (7, '7 - Lowest')], default=4)),
|
||||||
|
('targetdate', models.DateField(blank=True, null=True)),
|
||||||
|
('progress', models.IntegerField(choices=[(0, '0 %'), (10, '10 %'), (20, '20 %'), (30, '30 %'), (40, '40 %'), (50, '50 %'), (60, '60 %'), (70, '70 %'), (80, '80 %'), (90, '90 %'), (100, '100 %')], default=0)),
|
||||||
|
('name', models.CharField(default='', max_length=150)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('creation_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('assigned_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='patt.Project')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Comment',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.IntegerField(choices=[(1, 'Appraisal'), (0, 'Comment')], default=0)),
|
||||||
|
('creation_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patt.Task')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
29
migrations/0002_auto_20191003_1614.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-03 16:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='state',
|
||||||
|
field=models.IntegerField(choices=[(0, 'Open'), (2, 'Closed')], default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patt.Project'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='state',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
17
migrations/0003_remove_tasklistsetting_sorttype.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-03 18:36
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0002_auto_20191003_1614'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tasklistsetting',
|
||||||
|
name='sorttype',
|
||||||
|
),
|
||||||
|
]
|
45
migrations/0004_auto_20191004_1125.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-04 11:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0003_remove_tasklistsetting_sorttype'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ViewSetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('view', models.CharField(max_length=50)),
|
||||||
|
('userfilter', models.CharField(default='all', max_length=50)),
|
||||||
|
('statefilter', models.CharField(default='1,0', max_length=50)),
|
||||||
|
('displaytype', models.CharField(default='short', max_length=50)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='current_tasklistsetting',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='my_tasklistsettings',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='viewsettings',
|
||||||
|
field=models.TextField(default='{}'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='TaskListSetting',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='viewsetting',
|
||||||
|
name='profile',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patt.UserProfile'),
|
||||||
|
),
|
||||||
|
]
|
20
migrations/0005_project_creation_date.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-05 11:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0004_auto_20191004_1125'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='creation_date',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
18
migrations/0006_auto_20191006_1824.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-06 18:24
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0005_project_creation_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='comment',
|
||||||
|
old_name='description',
|
||||||
|
new_name='comment',
|
||||||
|
),
|
||||||
|
]
|
65
migrations/0007_historicalcomment_historicaltask.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-09 19:38
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import simple_history.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('patt', '0006_auto_20191006_1824'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HistoricalTask',
|
||||||
|
fields=[
|
||||||
|
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||||
|
('state', models.IntegerField(default=0)),
|
||||||
|
('priority', models.IntegerField(choices=[(1, '1 - Highest'), (2, '2 - High'), (3, '3 - Above-Average'), (4, '4 - Average'), (5, '5 - Below-Average'), (6, '6 - Low'), (7, '7 - Lowest')], default=4)),
|
||||||
|
('targetdate', models.DateField(blank=True, null=True)),
|
||||||
|
('progress', models.IntegerField(choices=[(0, '0 %'), (10, '10 %'), (20, '20 %'), (30, '30 %'), (40, '40 %'), (50, '50 %'), (60, '60 %'), (70, '70 %'), (80, '80 %'), (90, '90 %'), (100, '100 %')], default=0)),
|
||||||
|
('name', models.CharField(default='', max_length=150)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('creation_date', models.DateTimeField(blank=True, editable=False)),
|
||||||
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('history_date', models.DateTimeField()),
|
||||||
|
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||||
|
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||||
|
('assigned_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='patt.Project')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'historical task',
|
||||||
|
'ordering': ('-history_date', '-history_id'),
|
||||||
|
'get_latest_by': 'history_date',
|
||||||
|
},
|
||||||
|
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HistoricalComment',
|
||||||
|
fields=[
|
||||||
|
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||||
|
('type', models.IntegerField(choices=[(1, 'Appraisal'), (0, 'Comment')], default=0)),
|
||||||
|
('creation_date', models.DateTimeField(blank=True, editable=False)),
|
||||||
|
('comment', models.TextField()),
|
||||||
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('history_date', models.DateTimeField()),
|
||||||
|
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||||
|
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||||
|
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('task', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='patt.Task')),
|
||||||
|
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'historical comment',
|
||||||
|
'ordering': ('-history_date', '-history_id'),
|
||||||
|
'get_latest_by': 'history_date',
|
||||||
|
},
|
||||||
|
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||||
|
),
|
||||||
|
]
|
17
migrations/0008_remove_viewsetting_displaytype.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-10-10 18:33
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0007_historicalcomment_historicaltask'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='viewsetting',
|
||||||
|
name='displaytype',
|
||||||
|
),
|
||||||
|
]
|
41
migrations/0009_auto_20191102_1925.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-11-02 19:25
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('patt', '0008_remove_viewsetting_displaytype'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PattProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_highlight',
|
||||||
|
field=models.IntegerField(choices=[(0, 'Off'), (1, 1), (2, 2), (3, 3), (5, 5), (7, 7), (10, 10), (14, 14)], default=7),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_warn',
|
||||||
|
field=models.IntegerField(choices=[(0, 'Off'), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 7), (9, 9)], default=3),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='viewsetting',
|
||||||
|
name='profile',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patt.PattProfile'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='UserProfile',
|
||||||
|
),
|
||||||
|
]
|
36
migrations/0010_auto_20191117_1157.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 2.2.5 on 2019-11-17 11:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('patt', '0009_auto_20191102_1925'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_highlight',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_warn',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_late',
|
||||||
|
field=models.IntegerField(choices=[(-1, 'Off'), (0, 0)], default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_soon',
|
||||||
|
field=models.IntegerField(choices=[(-1, 'Off'), (1, 1), (2, 2), (3, 3), (5, 5), (7, 7), (10, 10), (14, 14)], default=7),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='days_very_soon',
|
||||||
|
field=models.IntegerField(choices=[(-1, 'Off'), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 7), (9, 9)], default=3),
|
||||||
|
),
|
||||||
|
]
|
20
migrations/0011_project_role_visitor.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-01-09 10:53
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('patt', '0010_auto_20191117_1157'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='role_visitor',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='role_visitor', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
35
migrations/0012_auto_20200114_1035.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 2.2.9 on 2020-01-14 10:35
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('patt', '0011_project_role_visitor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Search',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=48)),
|
||||||
|
('search_txt', models.TextField(blank=True, default='')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='viewsetting',
|
||||||
|
name='profile',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='PattProfile',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='ViewSetting',
|
||||||
|
),
|
||||||
|
]
|
0
migrations/__init__.py
Normal file
281
models.py
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import datetime
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.dateformat import format
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
|
# PROJECTSTATE (REQ-??)
|
||||||
|
#
|
||||||
|
PROJECTSTATE_OPEN = 0
|
||||||
|
PROJECTSTATE_CLOSED = 2
|
||||||
|
PROJECTS_IN_WORK = (PROJECTSTATE_OPEN, )
|
||||||
|
#
|
||||||
|
PROJECTSTATE_CHOICES = (
|
||||||
|
(PROJECTSTATE_OPEN, _('Open')),
|
||||||
|
(PROJECTSTATE_CLOSED, _('Closed')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TASKSTATE (REQ-14)
|
||||||
|
#
|
||||||
|
TASKSTATE_OPEN = 0
|
||||||
|
TASKSTATE_FINISHED = 1
|
||||||
|
TASKSTATE_CLOSED = 2
|
||||||
|
TASKSTATE_CANCELED = 3
|
||||||
|
TASKS_IN_WORK = (TASKSTATE_OPEN, TASKSTATE_FINISHED, )
|
||||||
|
#
|
||||||
|
TASKSTATE_CHOICES = (
|
||||||
|
(TASKSTATE_OPEN, _('Open')),
|
||||||
|
(TASKSTATE_FINISHED, _('Finished')),
|
||||||
|
(TASKSTATE_CLOSED, _('Closed')),
|
||||||
|
(TASKSTATE_CANCELED, _('Cancelled')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TASKPRIORITY (REQ-??)
|
||||||
|
#
|
||||||
|
PRIO_CHOICES = (
|
||||||
|
(1, _('1 - Highest')),
|
||||||
|
(2, _('2 - High')),
|
||||||
|
(3, _('3 - Above-Average')),
|
||||||
|
(4, _('4 - Average')),
|
||||||
|
(5, _('5 - Below-Average')),
|
||||||
|
(6, _('6 - Low')),
|
||||||
|
(7, _('7 - Lowest')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TASKPROGRESS (REQ-??)
|
||||||
|
#
|
||||||
|
PRORGRESS_CHOICES = (
|
||||||
|
(0, '0 %'),
|
||||||
|
(10, '10 %'),
|
||||||
|
(20, '20 %'),
|
||||||
|
(30, '30 %'),
|
||||||
|
(40, '40 %'),
|
||||||
|
(50, '50 %'),
|
||||||
|
(60, '60 %'),
|
||||||
|
(70, '70 %'),
|
||||||
|
(80, '80 %'),
|
||||||
|
(90, '90 %'),
|
||||||
|
(100, '100 %'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# COMMENTTYPE (REQ-??)
|
||||||
|
#
|
||||||
|
COMMENTTYPE_COMMENT = 0
|
||||||
|
COMMENTTYPE_APPRAISAL = 1
|
||||||
|
#
|
||||||
|
COMMENTTYPE_CHOICES = (
|
||||||
|
(COMMENTTYPE_APPRAISAL, _('Appraisal')),
|
||||||
|
(COMMENTTYPE_COMMENT, _('Comment')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# GENERAL Methods and Classes
|
||||||
|
#
|
||||||
|
class ModelList(list):
|
||||||
|
def __init__(self, lst, chk=None, user=None):
|
||||||
|
if chk is None:
|
||||||
|
list.__init__(self, lst)
|
||||||
|
else:
|
||||||
|
list.__init__(self)
|
||||||
|
for e in lst:
|
||||||
|
acc = chk(e, user)
|
||||||
|
if acc.read:
|
||||||
|
self.append(e)
|
||||||
|
|
||||||
|
def sort(self, **kwargs):
|
||||||
|
list.sort(self, key=lambda entry: entry.sort_string(), reverse=kwargs.get('reverse', False))
|
||||||
|
|
||||||
|
|
||||||
|
# PROCECT Model
|
||||||
|
#
|
||||||
|
class Project(models.Model):
|
||||||
|
name = models.CharField(max_length=128) # REQ-7
|
||||||
|
description = models.TextField(default='', blank=True) # REQ-8
|
||||||
|
state = models.IntegerField(choices=PROJECTSTATE_CHOICES, default=PROJECTSTATE_OPEN) # REQ-26
|
||||||
|
role_leader = models.ManyToManyField(User, related_name='role_leader') # REQ-9
|
||||||
|
role_member = models.ManyToManyField(User, related_name='role_member', blank=True) # REQ-12
|
||||||
|
role_visitor = models.ManyToManyField(User, related_name='role_visitor', blank=True) # REQ-33
|
||||||
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
days_late = models.IntegerField(default=0, choices=((-1, _('Off')), (0, 0), ))
|
||||||
|
days_very_soon = models.IntegerField(default=3, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 7), (9, 9), ))
|
||||||
|
days_soon = models.IntegerField(default=7, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (5, 5), (7, 7), (10, 10), (14, 14), ))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attachment_target_path(self):
|
||||||
|
if self.id:
|
||||||
|
return 'patt/project/%d' % self.id
|
||||||
|
else:
|
||||||
|
return 'patt/project/new'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatted_leaders(self):
|
||||||
|
if self.id: # in preview (before saving), the ManyToManyFields are not available
|
||||||
|
return ', '.join([user.username for user in self.role_leader.all()]) or '-'
|
||||||
|
else:
|
||||||
|
return '...'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatted_members(self):
|
||||||
|
if self.id: # in preview (before saving), the ManyToManyFields are not available
|
||||||
|
return ', '.join([user.username for user in self.role_member.all()]) or '-'
|
||||||
|
else:
|
||||||
|
return '...'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def formatted_visitors(self):
|
||||||
|
if self.id: # in preview (before saving), the ManyToManyFields are not available
|
||||||
|
return ', '.join([user.username for user in self.role_visitor.all()]) or '-'
|
||||||
|
else:
|
||||||
|
return '...'
|
||||||
|
|
||||||
|
def sort_string(self):
|
||||||
|
return (self.state, self.name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Project: %s' % self.name
|
||||||
|
|
||||||
|
|
||||||
|
# TASK Model
|
||||||
|
#
|
||||||
|
class Task(models.Model):
|
||||||
|
FUSION_STATE_FINISHED = 0
|
||||||
|
FUSION_STATE_NORMAL = 1
|
||||||
|
FUSION_STATE_SOON = 2
|
||||||
|
FUSION_STATE_VERY_SOON = 3
|
||||||
|
FUSION_STATE_LATE = 4
|
||||||
|
#
|
||||||
|
SAVE_ON_CHANGE_FIELDS = ['state', 'priority', 'targetdate', 'progress', 'name', 'description', 'assigned_user', 'project']
|
||||||
|
#
|
||||||
|
state = models.IntegerField(default=TASKSTATE_OPEN) # REQ-14
|
||||||
|
priority = models.IntegerField(choices=PRIO_CHOICES, default=4) # REQ-15
|
||||||
|
targetdate = models.DateField(null=True, blank=True) # REQ-16
|
||||||
|
progress = models.IntegerField(default=0, choices=PRORGRESS_CHOICES) # REQ-17
|
||||||
|
name = models.CharField(max_length=150, default='') # REQ-?? (18)
|
||||||
|
description = models.TextField(default='', blank=True) # REQ-??
|
||||||
|
assigned_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) # REQ-31
|
||||||
|
project = models.ForeignKey(Project, on_delete=models.CASCADE) # REQ-??
|
||||||
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
#
|
||||||
|
history = HistoricalRecords()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def taskname_prefix(self):
|
||||||
|
if self.assigned_user:
|
||||||
|
return '**#%d //(%s)//:** ' % (self.id, self.assigned_user.username)
|
||||||
|
else:
|
||||||
|
return '**#%d:** ' % self.id
|
||||||
|
|
||||||
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
|
if self.id and not force_update:
|
||||||
|
orig = Task.objects.get(id=self.id)
|
||||||
|
for key in self.SAVE_ON_CHANGE_FIELDS:
|
||||||
|
if getattr(self, key) != getattr(orig, key):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.save_needed = False
|
||||||
|
return False
|
||||||
|
self.save_needed = True
|
||||||
|
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attachment_target_path(self):
|
||||||
|
if self.id:
|
||||||
|
return 'patt/task/%d' % self.id
|
||||||
|
else:
|
||||||
|
return 'patt/task/new'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self):
|
||||||
|
rv = ModelList(self.comment_set.all())
|
||||||
|
rv.sort(reverse=True)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def datafusion_state(self):
|
||||||
|
if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
|
||||||
|
return self.FUSION_STATE_FINISHED
|
||||||
|
else:
|
||||||
|
if self.targetdate is not None:
|
||||||
|
if type(self.targetdate) == datetime.date:
|
||||||
|
targetdate = self.targetdate
|
||||||
|
else:
|
||||||
|
targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
|
||||||
|
if targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_late) and self.project.days_late >= 0:
|
||||||
|
return self.FUSION_STATE_LATE
|
||||||
|
elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_very_soon) and self.project.days_very_soon >= 0:
|
||||||
|
return self.FUSION_STATE_VERY_SOON
|
||||||
|
elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_soon) and self.project.days_soon >= 0:
|
||||||
|
return self.FUSION_STATE_SOON
|
||||||
|
return self.FUSION_STATE_NORMAL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def class_by_state(self):
|
||||||
|
return {
|
||||||
|
self.FUSION_STATE_FINISHED: 'task-finished',
|
||||||
|
self.FUSION_STATE_NORMAL: 'task-normal',
|
||||||
|
self.FUSION_STATE_VERY_SOON: 'task-very-soon',
|
||||||
|
self.FUSION_STATE_SOON: 'task-soon',
|
||||||
|
self.FUSION_STATE_LATE: 'task-late',
|
||||||
|
}.get(self.datafusion_state())
|
||||||
|
|
||||||
|
def sort_string(self):
|
||||||
|
if self.targetdate:
|
||||||
|
td = int(format(self.targetdate, 'U'))
|
||||||
|
else:
|
||||||
|
td = 999999999999
|
||||||
|
return (100 - self.datafusion_state(), self.state, self.priority, td, self.progress, self.name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.project:
|
||||||
|
return 'Task: %s - %s' % (self.project.name, self.name)
|
||||||
|
else:
|
||||||
|
return 'Task: %s' % (self.name)
|
||||||
|
|
||||||
|
|
||||||
|
# COMMENT Model
|
||||||
|
#
|
||||||
|
class Comment(models.Model): # REQ-19 and REQ-20
|
||||||
|
SAVE_ON_CHANGE_FIELDS = ['task', 'user', 'type', 'comment']
|
||||||
|
task = models.ForeignKey(Task, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
type = models.IntegerField(choices=COMMENTTYPE_CHOICES, default=COMMENTTYPE_COMMENT)
|
||||||
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
comment = models.TextField()
|
||||||
|
#
|
||||||
|
history = HistoricalRecords()
|
||||||
|
|
||||||
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
|
if self.id and not force_update:
|
||||||
|
orig = Comment.objects.get(id=self.id)
|
||||||
|
for key in self.SAVE_ON_CHANGE_FIELDS:
|
||||||
|
if getattr(self, key) != getattr(orig, key):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.save_needed = False
|
||||||
|
return False
|
||||||
|
self.save_needed = True
|
||||||
|
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attachment_target_path(self):
|
||||||
|
if self.id:
|
||||||
|
return 'patt/comment/%d' % self.id
|
||||||
|
else:
|
||||||
|
return 'patt/comment/new'
|
||||||
|
|
||||||
|
def sort_string(self):
|
||||||
|
return (self.creation_date, self.type, self.comment)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Comment: %s - %d' % (self.task.name, self.id)
|
||||||
|
|
||||||
|
|
||||||
|
# SEARCH Model
|
||||||
|
#
|
||||||
|
class Search(models.Model):
|
||||||
|
name = models.CharField(max_length=48)
|
||||||
|
search_txt = models.TextField(default='', blank=True)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
167
search.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
import fstools
|
||||||
|
import logging
|
||||||
|
from .models import Task, TASKSTATE_OPEN, TASKSTATE_FINISHED, TASKSTATE_CLOSED, TASKSTATE_CANCELED
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from whoosh.fields import Schema, ID, TEXT, NUMERIC, DATETIME, BOOLEAN
|
||||||
|
from whoosh.qparser.dateparse import DateParserPlugin
|
||||||
|
from whoosh import index, qparser
|
||||||
|
|
||||||
|
logger = logging.getLogger("WHOOSH")
|
||||||
|
|
||||||
|
SEARCH_MY_OPEN_TASKS = 1
|
||||||
|
SEARCH_LOST_SOULS = 2
|
||||||
|
|
||||||
|
|
||||||
|
def common_searches(request):
|
||||||
|
cs = {}
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
cs[SEARCH_MY_OPEN_TASKS] = ('My Open Tasks', mk_search_pattern(user_ids=[request.user.id]))
|
||||||
|
cs[SEARCH_LOST_SOULS] = ('Lost Souls (no user)', 'assigned_user_missing:1')
|
||||||
|
return cs
|
||||||
|
|
||||||
|
|
||||||
|
# INDEX_STATES
|
||||||
|
#
|
||||||
|
INDEX_STATES = {
|
||||||
|
TASKSTATE_OPEN: 'Open',
|
||||||
|
TASKSTATE_FINISHED: 'Finished',
|
||||||
|
TASKSTATE_CLOSED: 'Closed',
|
||||||
|
TASKSTATE_CANCELED: 'Canselled'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA = Schema(
|
||||||
|
id=ID(unique=True, stored=True),
|
||||||
|
# Task
|
||||||
|
task_id=NUMERIC,
|
||||||
|
assigned_user=TEXT,
|
||||||
|
assigned_user_missing=BOOLEAN,
|
||||||
|
name=TEXT,
|
||||||
|
description=TEXT,
|
||||||
|
state=TEXT,
|
||||||
|
targetdate=DATETIME,
|
||||||
|
# Related Project
|
||||||
|
project_id=NUMERIC,
|
||||||
|
project_name=TEXT,
|
||||||
|
project_description=TEXT,
|
||||||
|
# Related Comments
|
||||||
|
comment=TEXT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mk_whooshpath_if_needed():
|
||||||
|
if not os.path.exists(settings.WHOOSH_PATH):
|
||||||
|
fstools.mkdir(settings.WHOOSH_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def create_index():
|
||||||
|
mk_whooshpath_if_needed()
|
||||||
|
logger.debug('Search Index created.')
|
||||||
|
return index.create_in(settings.WHOOSH_PATH, schema=SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def load_index():
|
||||||
|
mk_whooshpath_if_needed()
|
||||||
|
try:
|
||||||
|
ix = index.open_dir(settings.WHOOSH_PATH)
|
||||||
|
except index.EmptyIndexError:
|
||||||
|
ix = create_index()
|
||||||
|
else:
|
||||||
|
logger.debug('Search Index opened.')
|
||||||
|
return ix
|
||||||
|
|
||||||
|
|
||||||
|
def add_item(ix, item):
|
||||||
|
# Define Standard data
|
||||||
|
#
|
||||||
|
data = dict(
|
||||||
|
id='%d' % item.id,
|
||||||
|
# Task
|
||||||
|
task_id=item.id,
|
||||||
|
name=item.name,
|
||||||
|
description=item.description,
|
||||||
|
state=INDEX_STATES.get(item.state),
|
||||||
|
# Related Project
|
||||||
|
project_id=item.project.id,
|
||||||
|
project_name=item.project.name,
|
||||||
|
project_description=item.project.description,
|
||||||
|
# Related Comments
|
||||||
|
comment=' '.join([c.comment for c in item.comment_set.all()]),
|
||||||
|
)
|
||||||
|
# Add Optional data
|
||||||
|
#
|
||||||
|
if item.assigned_user is not None:
|
||||||
|
data['assigned_user'] = item.assigned_user.username
|
||||||
|
data['assigned_user_missing'] = False
|
||||||
|
else:
|
||||||
|
data['assigned_user_missing'] = True
|
||||||
|
if item.targetdate is not None:
|
||||||
|
data['targetdate'] = datetime.datetime.combine(item.targetdate, datetime.datetime.min.time())
|
||||||
|
# Write data to the index
|
||||||
|
#
|
||||||
|
with ix.writer() as w:
|
||||||
|
logger.info('Adding document with id=%d to the search index.', data.get('task_id'))
|
||||||
|
w.add_document(**data)
|
||||||
|
for key in data:
|
||||||
|
logger.debug(' - Adding %s=%s', key, repr(data[key]))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_item(ix, item):
|
||||||
|
with ix.writer() as w:
|
||||||
|
logger.info('Removing document with id=%d from the search index.', item.id)
|
||||||
|
w.delete_by_term("task_id", item.id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_item(ix, item):
|
||||||
|
delete_item(ix, item)
|
||||||
|
add_item(ix, item)
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_index(ix):
|
||||||
|
for t in Task.objects.all():
|
||||||
|
add_item(ix, t)
|
||||||
|
return len(Task.objects.all())
|
||||||
|
|
||||||
|
|
||||||
|
def search(ix, search_txt):
|
||||||
|
qp = qparser.MultifieldParser(['name', 'description'], ix.schema)
|
||||||
|
qp.add_plugin(DateParserPlugin(free=True))
|
||||||
|
try:
|
||||||
|
q = qp.parse(search_txt)
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
with ix.searcher() as s:
|
||||||
|
results = s.search(q, limit=None)
|
||||||
|
rpl = []
|
||||||
|
for hit in results:
|
||||||
|
rpl.append(hit['id'])
|
||||||
|
return Task.objects.filter(id__in=rpl)
|
||||||
|
|
||||||
|
|
||||||
|
def mk_search_pattern(**kwargs):
|
||||||
|
prj_ids = kwargs.get('prj_ids', [])
|
||||||
|
user_ids = kwargs.get('user_ids', [])
|
||||||
|
states = kwargs.get('states', [INDEX_STATES.get(TASKSTATE_OPEN), INDEX_STATES.get(TASKSTATE_FINISHED)])
|
||||||
|
rule_parts = []
|
||||||
|
if prj_ids is not None and len(prj_ids) > 0:
|
||||||
|
rule_parts.append(' OR '.join(['project_id:%s' % pid for pid in prj_ids]))
|
||||||
|
if user_ids is not None and len(user_ids) > 0:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
rule_parts.append(' OR '.join(['assigned_user:%s' % User.objects.get(id=uid).username for uid in user_ids]))
|
||||||
|
if states is not None and len(states) > 0:
|
||||||
|
rule_parts.append(' OR '.join(['state:%s' % state for state in states]))
|
||||||
|
return ' AND '.join('(%s)' % rule for rule in rule_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_ids_from_search_pattern(search_txt):
|
||||||
|
try:
|
||||||
|
return re.findall('project_id:(\d+)', search_txt)
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
except TypeError:
|
||||||
|
return None
|
31
signals.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from .models import Task, Project, Comment
|
||||||
|
from mycreole import delete_attachment_target_path
|
||||||
|
from .search import load_index, delete_item, update_item
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_delete, sender=Task)
|
||||||
|
@receiver(pre_delete, sender=Project)
|
||||||
|
@receiver(pre_delete, sender=Comment)
|
||||||
|
def item_cache_delete(instance, **kwargs):
|
||||||
|
delete_attachment_target_path(instance.attachment_target_path)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=Task)
|
||||||
|
def search_index_item_delete(instance, **kwargs):
|
||||||
|
# delete index entry
|
||||||
|
ix = load_index()
|
||||||
|
delete_item(ix, instance)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Task)
|
||||||
|
@receiver(post_save, sender=Comment)
|
||||||
|
@receiver(post_delete, sender=Comment)
|
||||||
|
def search_index_item_update(instance, **kwargs):
|
||||||
|
if type(instance) == Comment:
|
||||||
|
task = instance.task
|
||||||
|
else:
|
||||||
|
task = instance
|
||||||
|
ix = load_index()
|
||||||
|
update_item(ix, task)
|
9
static/patt/datepicker.min.css
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*!
|
||||||
|
* Datepicker v0.6.5
|
||||||
|
* https://github.com/fengyuanchen/datepicker
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-2018 Chen Fengyuan
|
||||||
|
* Released under the MIT license
|
||||||
|
*
|
||||||
|
* Date: 2018-03-31T06:16:43.444Z
|
||||||
|
*/.datepicker-container{background-color:#fff;direction:ltr;font-size:12px;left:0;line-height:30px;position:fixed;top:0;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:210px;z-index:-1;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}.datepicker-container:after,.datepicker-container:before{border:5px solid transparent;content:" ";display:block;height:0;position:absolute;width:0}.datepicker-dropdown{border:1px solid #ccc;box-shadow:0 3px 6px #ccc;box-sizing:content-box;position:absolute;z-index:1}.datepicker-inline{position:static}.datepicker-top-left,.datepicker-top-right{border-top-color:#39f}.datepicker-top-left:after,.datepicker-top-left:before,.datepicker-top-right:after,.datepicker-top-right:before{border-top:0;left:10px;top:-5px}.datepicker-top-left:before,.datepicker-top-right:before{border-bottom-color:#39f}.datepicker-top-left:after,.datepicker-top-right:after{border-bottom-color:#fff;top:-4px}.datepicker-bottom-left,.datepicker-bottom-right{border-bottom-color:#39f}.datepicker-bottom-left:after,.datepicker-bottom-left:before,.datepicker-bottom-right:after,.datepicker-bottom-right:before{border-bottom:0;bottom:-5px;left:10px}.datepicker-bottom-left:before,.datepicker-bottom-right:before{border-top-color:#39f}.datepicker-bottom-left:after,.datepicker-bottom-right:after{border-top-color:#fff;bottom:-4px}.datepicker-bottom-right:after,.datepicker-bottom-right:before,.datepicker-top-right:after,.datepicker-top-right:before{left:auto;right:10px}.datepicker-panel>ul{margin:0;padding:0;width:102%}.datepicker-panel>ul:after,.datepicker-panel>ul:before{content:" ";display:table}.datepicker-panel>ul:after{clear:both}.datepicker-panel>ul>li{background-color:#fff;cursor:pointer;float:left;height:30px;list-style:none;margin:0;padding:0;text-align:center;width:30px}.datepicker-panel>ul>li:hover{background-color:#e5f2ff}.datepicker-panel>ul>li.muted,.datepicker-panel>ul>li.muted:hover{color:#999}.datepicker-panel>ul>li.highlighted{background-color:#e5f2ff}.datepicker-panel>ul>li.highlighted:hover{background-color:#cce5ff}.datepicker-panel>ul>li.picked,.datepicker-panel>ul>li.picked:hover{color:#39f}.datepicker-panel>ul>li.disabled,.datepicker-panel>ul>li.disabled:hover{background-color:#fff;color:#ccc;cursor:default}.datepicker-panel>ul>li.disabled.highlighted,.datepicker-panel>ul>li.disabled:hover.highlighted{background-color:#e5f2ff}.datepicker-panel>ul>li[data-view="month next"],.datepicker-panel>ul>li[data-view="month prev"],.datepicker-panel>ul>li[data-view="year next"],.datepicker-panel>ul>li[data-view="year prev"],.datepicker-panel>ul>li[data-view="years next"],.datepicker-panel>ul>li[data-view="years prev"],.datepicker-panel>ul>li[data-view=next]{font-size:18px}.datepicker-panel>ul>li[data-view="month current"],.datepicker-panel>ul>li[data-view="year current"],.datepicker-panel>ul>li[data-view="years current"]{width:150px}.datepicker-panel>ul[data-view=months]>li,.datepicker-panel>ul[data-view=years]>li{height:52.5px;line-height:52.5px;width:52.5px}.datepicker-panel>ul[data-view=week]>li,.datepicker-panel>ul[data-view=week]>li:hover{background-color:#fff;cursor:default}.datepicker-hide{display:none}
|
10
static/patt/datepicker.min.js
vendored
Normal file
BIN
static/patt/draft.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
static/patt/icons/collapse.png
Normal file
After Width: | Height: | Size: 264 B |
BIN
static/patt/icons/edit.png
Normal file
After Width: | Height: | Size: 749 B |
BIN
static/patt/icons/edit_comment.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
static/patt/icons/expand.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
static/patt/icons/pg_0.png
Normal file
After Width: | Height: | Size: 171 B |
BIN
static/patt/icons/pg_10.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_100.png
Normal file
After Width: | Height: | Size: 173 B |
BIN
static/patt/icons/pg_20.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_30.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_40.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_50.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_60.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_70.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_80.png
Normal file
After Width: | Height: | Size: 179 B |
BIN
static/patt/icons/pg_90.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
static/patt/icons/prio1.png
Normal file
After Width: | Height: | Size: 742 B |
BIN
static/patt/icons/prio2.png
Normal file
After Width: | Height: | Size: 906 B |
BIN
static/patt/icons/prio3.png
Normal file
After Width: | Height: | Size: 892 B |
BIN
static/patt/icons/prio4.png
Normal file
After Width: | Height: | Size: 859 B |
BIN
static/patt/icons/prio5.png
Normal file
After Width: | Height: | Size: 912 B |
BIN
static/patt/icons/prio6.png
Normal file
After Width: | Height: | Size: 942 B |
BIN
static/patt/icons/prio7.png
Normal file
After Width: | Height: | Size: 813 B |
BIN
static/patt/icons/spacer.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
static/patt/icons/state0.png
Normal file
After Width: | Height: | Size: 651 B |
BIN
static/patt/icons/state1.png
Normal file
After Width: | Height: | Size: 800 B |
BIN
static/patt/icons/state2.png
Normal file
After Width: | Height: | Size: 557 B |
BIN
static/patt/icons/state3.png
Normal file
After Width: | Height: | Size: 454 B |
4
static/patt/jquery.min.js
vendored
Normal file
12
templates/patt/help.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ help_content|safe }}
|
||||||
|
{% endblock content %}
|
131
templates/patt/patt.css
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
.tasklist, .projectlist {
|
||||||
|
padding-top: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasklist-first, .projectlist-first {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskheadline, .projectheadline {
|
||||||
|
margin: 0px;
|
||||||
|
display: block;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskname, .projectname, .projectusers, .projectuserlabel {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectname {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectuserlabel {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskdetails, .projectdetails {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskcomment {
|
||||||
|
margin: 16px;
|
||||||
|
border-left: 6px solid #323232;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskcomment-date {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskcomment-description {
|
||||||
|
padding: 0 50px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-finished {
|
||||||
|
background-color: #efffef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-normal {
|
||||||
|
background-color: #dfefff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-soon {
|
||||||
|
background-color: #ffffe4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-very-soon {
|
||||||
|
background-color: #ffe6cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-late {
|
||||||
|
background-color: #ffe4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-normal {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskicon, .projecticon {
|
||||||
|
padding-left: 1px;
|
||||||
|
padding-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projecticon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskicon:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the screen is less than 700px wide, reduce content to be shown */
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.prio_icons_hide {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the screen is less than 500px wide, reduce content to be shown */
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.target_date_hide {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the screen is less than 375px wide, reduce content to be shown */
|
||||||
|
@media screen and (max-width: 375px) {
|
||||||
|
.state_icons_hide {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
background-image: url("{% static 'patt/draft.png' %}");
|
||||||
|
padding: 40px;
|
||||||
|
padding-top: 75px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-spacer {
|
||||||
|
min-height: 35px;
|
||||||
|
}
|
3
templates/patt/project/details.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{% load mycreole %}
|
||||||
|
|
||||||
|
{% if project.description %}<div class="projectdetails">{% render_creole project.description project.attachment_target_path next_anchor %}</div>{% endif %}
|
28
templates/patt/project/head.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load mycreole %}
|
||||||
|
{% load access %}
|
||||||
|
{% load patt_urls %}
|
||||||
|
|
||||||
|
{% may_modify project as user_may_modify %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="projectheadline project-normal" ondblclick="location.href='{% url_tasklist_for_project project %}';" style="cursor:pointer;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="projectname">{% render_creole "**Project #"|add:project_id|add:":** "|add:project.name project.attachment_target_path next_anchor %}</div>
|
||||||
|
<img class="projecticon" src="{% static 'patt/icons/state'|add:state|add:'.png' %}">
|
||||||
|
{% if user_may_modify %}<a href="{% url_projectedit project %}"><img class="projecticon" style="left: unset; right: 0;" src="{% static 'patt/icons/edit.png' %}" style="float:right"></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<span class="projectuserlabel">{% trans 'Projectleader(s)' %}:</span>
|
||||||
|
<span class="projectusers">{{ project.formatted_leaders }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="projectuserlabel">{% trans 'Projectmember(s)' %}:</span>
|
||||||
|
<span class="projectusers">{{ project.formatted_members }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="projectuserlabel">{% trans 'Projectvisitor(s)' %}:</span>
|
||||||
|
<span class="projectusers">{{ project.formatted_visitors }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
4
templates/patt/project/project.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% with state=project.state|stringformat:"s" project_id=project.id|stringformat:"s" %}
|
||||||
|
{% include 'patt/project/head.html' %}
|
||||||
|
{% include 'patt/project/details.html' %}
|
||||||
|
{% endwith %}
|
20
templates/patt/projectlist.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with link=True %}
|
||||||
|
{% for project in projectlist %}
|
||||||
|
<div {% if forloop.counter != 1 %} id="project-{{ project.id }}"{% endif %}>
|
||||||
|
<div class="projectlist{% if forloop.counter == 1 %}-first{% endif %}">
|
||||||
|
{% include 'patt/project/project.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock content %}
|
31
templates/patt/raw_single_form.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
{{ form.media }}
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input type="hidden" name="item_id" value="{{ item_id }}">
|
||||||
|
{% if not disable_save %}<input type="submit" name="save" value="{% trans "Save" %}" class="button" />{% endif %}
|
||||||
|
{% if not disable_search %}<input type="submit" formaction="" name="search" value="{% trans "Search" %}" class="button" />{% endif %}
|
||||||
|
{% if not disable_preview %}<input type="submit" formaction="#preview" name="preview" value="{% trans "Preview" %}" class="button" />{% endif %}
|
||||||
|
{% if not disable_delete %}<input type="submit" formaction="" name="delete" value="{% trans "Delete" %}" class="button" />{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if template %}
|
||||||
|
<div class="preview-spacer" id="preview"></div>
|
||||||
|
<div class="preview">
|
||||||
|
{% include template %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
9
templates/patt/task/comment.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% load mycreole %}
|
||||||
|
{% load access %}
|
||||||
|
|
||||||
|
{% may_modify_comment comment as user_may_modify_comment %}
|
||||||
|
|
||||||
|
<div class="taskcomment"{% if printview != True and user_may_modify_comment %} onclick="location.href='{% url 'patt-commentedit' task_id=task.id comment_id=comment.id %}{{"?next="|add:request.path }}';" style="cursor:pointer;"{% endif %}>
|
||||||
|
<div class="taskcomment-date">{{ comment.creation_date }}{% if comment.user %} ({{ comment.user.username }}){% endif %}:</div>
|
||||||
|
<div class="taskcomment-description">{% render_creole comment.comment comment.attachment_target_path next_anchor %}</div>
|
||||||
|
</div>
|
19
templates/patt/task/details.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load mycreole %}
|
||||||
|
|
||||||
|
<div class="taskdetails">
|
||||||
|
<strong>{% trans 'Created' %}:</strong> {{ task.creation_date }}<br>
|
||||||
|
<strong>{% trans 'Project' %}:</strong> {{ task.project.name }}
|
||||||
|
</div>
|
||||||
|
{% if task.description %}<div class="taskdetails">{% render_creole task.description task.attachment_target_path next_anchor %}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if comment_new %}
|
||||||
|
{% if comment_new.comment != "" %}
|
||||||
|
{% with comment=comment_new %}
|
||||||
|
{% include 'patt/task/comment.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% for comment in task.comments %}
|
||||||
|
{% include 'patt/task/comment.html' %}
|
||||||
|
{% endfor %}
|
41
templates/patt/task/head.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% load mycreole %}
|
||||||
|
{% load access %}
|
||||||
|
{% load patt_urls %}
|
||||||
|
|
||||||
|
{% may_add_comments task as user_may_add_comments %}
|
||||||
|
{% may_modify task as user_may_modify %}
|
||||||
|
{% may_modify_limited task as user_may_modify_limited %}
|
||||||
|
{% targetstates task as possible_targetstates %}
|
||||||
|
{% targetpriority task as possible_targetpriority %}
|
||||||
|
|
||||||
|
<div class="taskheadline {{ task.class_by_state }}"{% if target_head %} ondblclick="location.href='{% url target_head task_id=task.id %}{% if target_head == 'patt-taskedit' %}{{"?do=edit&next="|add:request.path }}{% endif %}';" style="cursor:pointer;"{% endif %}>
|
||||||
|
<div class="taskname">{% render_creole task.taskname_prefix|add:task.name task.attachment_target_path next_anchor %}</div>
|
||||||
|
<p>
|
||||||
|
{% if printview != True and taskview != True %}
|
||||||
|
<span class="toggle-button" style="cursor:pointer; padding-right: 10px;">
|
||||||
|
<img class="minus" src="{% static 'patt/icons/collapse.png' %}">
|
||||||
|
<img class="plus" src="{% static 'patt/icons/expand.png' %}">
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<img class="taskicon" src="{% static 'patt/icons/prio'|add:priority|add:'.png' %}">
|
||||||
|
<img class="taskicon" src="{% static 'patt/icons/state'|add:state|add:'.png' %}">
|
||||||
|
<img class="taskicon" src="{% static 'patt/icons/pg_'|add:progress|add:'.png' %}">
|
||||||
|
{% if task.targetdate %}<span class="target_date_hide">{{ task.targetdate }}</span>{% endif %}
|
||||||
|
|
||||||
|
{% if printview != True %}
|
||||||
|
{% if user_may_add_comments %}<a href="{% url_commentnew task %}"><img src="{% static 'patt/icons/edit_comment.png' %}" style="float:right"></a>{% endif %}
|
||||||
|
{% if user_may_modify or user_may_modify_limited %}<a href="{% url_taskedit task %}"><img src="{% static 'patt/icons/edit.png' %}" style="float:right"></a>{% endif %}
|
||||||
|
{% if user_may_modify or user_may_add_comments or user_may_modify_limited %}<img class="spacer prio_icons_hide" src="{% static 'patt/icons/spacer.png' %}" style="float:right">{% endif %}
|
||||||
|
{% for i in possible_targetpriority %}
|
||||||
|
<a class="taskicon prio_icons_hide" href="{% url_taskset_priority task i %}"><img src="{% static 'patt/icons/prio'|add:i|add:'.png' %}" style="float:right"></a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if possible_targetpriority %}
|
||||||
|
<img class="spacer state_icons_hide" src="{% static 'patt/icons/spacer.png' %}" style="float:right">
|
||||||
|
{% endif %}
|
||||||
|
{% for i in possible_targetstates %}
|
||||||
|
<a class="taskicon state_icons_hide" href="{% url_taskset_state task i %}"><img src="{% static 'patt/icons/state'|add:i|add:'.png' %}" style="float:right"></a>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
6
templates/patt/task/task.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% with priority=task.priority|stringformat:"s" state=task.state|stringformat:"s" progress=task.progress|stringformat:"s" task_id=task.id|stringformat:"s" %}
|
||||||
|
{% include 'patt/task/head.html' %}
|
||||||
|
<div class="toggle">
|
||||||
|
{% include 'patt/task/details.html' %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
37
templates/patt/task_form.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
<script src="{% static 'patt/jquery.min.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'patt/datepicker.min.css' %}">
|
||||||
|
<script src="{% static 'patt/datepicker.min.js' %}"></script>
|
||||||
|
{{ form.media }}
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$("#id_targetdate").datepicker({format:'yyyy-mm-dd', });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<input type="submit" name="save" value="{% trans "Save" %}" class="button" />
|
||||||
|
{% if not disable_preview %}<input type="submit" formaction="#preview" name="preview" value="{% trans "Preview" %}" class="button" />{% endif %}
|
||||||
|
{{ form_comment.as_p }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if template %}
|
||||||
|
<div class="preview-spacer" id="preview"></div>
|
||||||
|
<div class="preview">
|
||||||
|
{% include template %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
36
templates/patt/tasklist.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
<script src="{% static 'patt/jquery.min.js' %}"></script>
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% for task in tasklist %}
|
||||||
|
{% with task_id=task.id|stringformat:"s" %}
|
||||||
|
{% with next_anchor='task-'|add:task_id %}
|
||||||
|
<div {% if forloop.counter != 1 %} id="task-{{ task.id }}"{% endif %}>
|
||||||
|
<div class="tasklist{% if forloop.counter == 1 %}-first{% endif %}">
|
||||||
|
{% include 'patt/task/task.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('div.toggle').hide();
|
||||||
|
$('img.minus').hide();
|
||||||
|
$('.toggle-button').click(function(){
|
||||||
|
$(this).children().first().toggle()
|
||||||
|
$(this).children().last().toggle()
|
||||||
|
$(this).closest('div').next('div.toggle').toggle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock content %}
|
23
templates/patt/tasklist_print.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
{% for task in tasklist %}
|
||||||
|
<div {% if forloop.counter != 1 %} id="task-{{ task.id }}"{% endif %}>
|
||||||
|
<div class="tasklist{% if forloop.counter == 1 %}-first{% endif %}">
|
||||||
|
{% include 'patt/task/task.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
12
templates/patt/taskview.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||||
|
|
||||||
|
{% block head_extensions %}
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
{% endblock head_extensions %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'patt/task/task.html' %}
|
||||||
|
{% endblock content %}
|
16
templates/patt/taskview_print.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
{% include 'patt/patt.css' %}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% include 'patt/task/task.html' %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
0
templatetags/__init__.py
Normal file
64
templatetags/access.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from ..access import acc_task, acc_project
|
||||||
|
from django import template
|
||||||
|
from ..models import Task, Project, Comment
|
||||||
|
from ..views import TaskLike, ProjectLike, CommentLike
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
class xlike_acc(object):
|
||||||
|
add_comments = False
|
||||||
|
modify = False
|
||||||
|
modify_comment = False
|
||||||
|
modify_limited = False
|
||||||
|
allowed_targetstates = []
|
||||||
|
allowed_targetpriority = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_acc(obj, user):
|
||||||
|
if type(obj) is Comment:
|
||||||
|
return acc_task(obj.task, user)
|
||||||
|
elif type(obj) is Task:
|
||||||
|
return acc_task(obj, user)
|
||||||
|
elif type(obj) is Project:
|
||||||
|
return acc_project(obj, user)
|
||||||
|
elif type(obj) in [TaskLike, ProjectLike, CommentLike]:
|
||||||
|
return xlike_acc()
|
||||||
|
else:
|
||||||
|
raise AttributeError("Unknown obj of type", type(obj))
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='may_add_comments', takes_context=True)
|
||||||
|
def may_add_comments(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return acc.add_comments
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='may_modify', takes_context=True)
|
||||||
|
def may_modify(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return acc.modify
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='may_modify_comment', takes_context=True)
|
||||||
|
def may_modify_comment(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return acc.modify_comment
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='may_modify_limited', takes_context=True)
|
||||||
|
def may_modify_limited(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return acc.modify_limited
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='targetstates', takes_context=True)
|
||||||
|
def targetstates(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return [str(i) for i in acc.allowed_targetstates]
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='targetpriority', takes_context=True)
|
||||||
|
def targetpriority(context, obj):
|
||||||
|
acc = get_acc(obj, context['request'].user)
|
||||||
|
return [str(i) for i in acc.allowed_targetpriority]
|
37
templatetags/patt_urls.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django import template
|
||||||
|
import patt
|
||||||
|
from ..search import mk_search_pattern
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_taskedit', takes_context=True)
|
||||||
|
def url_taskedit(context, task):
|
||||||
|
return patt.url_taskedit(context['request'], task.id)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_taskset_priority', takes_context=True)
|
||||||
|
def url_taskset_priority(context, task, target_priority):
|
||||||
|
return patt.url_taskset(context['request'], task.id, priority=target_priority)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_taskset_state', takes_context=True)
|
||||||
|
def url_taskset_state(context, task, target_state):
|
||||||
|
return patt.url_taskset(context['request'], task.id, state=target_state)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_commentnew', takes_context=True)
|
||||||
|
def url_commentnew(context, task):
|
||||||
|
return patt.url_commentnew(context['request'], task.id)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_projectedit', takes_context=True)
|
||||||
|
def url_projectedit(context, project):
|
||||||
|
return patt.url_projectedit(context['request'], project.id)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name='url_tasklist_for_project', takes_context=True)
|
||||||
|
def url_tasklist_for_project(context, project):
|
||||||
|
request = context['request']
|
||||||
|
return patt.url_tasklist(request, search_txt=mk_search_pattern(prj_ids=[project.id]))
|
3
tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
30
urls.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import path
|
||||||
|
from django.urls.base import reverse
|
||||||
|
from .search import SEARCH_MY_OPEN_TASKS
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', lambda request: redirect(reverse('patt-commonfilter', kwargs={'common_filter_id': SEARCH_MY_OPEN_TASKS}), permanent=False)),
|
||||||
|
#
|
||||||
|
# views and urls
|
||||||
|
#
|
||||||
|
path('tasklist/', views.patt_tasklist, name='patt-tasklist'),
|
||||||
|
path('projectlist/', views.patt_projectlist, name='patt-projectlist'),
|
||||||
|
path('taskview/<int:task_id>/', views.patt_taskview, name='patt-taskview'),
|
||||||
|
path('helpview/', views.patt_helpview, name='patt-helpview'),
|
||||||
|
path('helpview/<str:page>', views.patt_helpview, name='patt-helpview'),
|
||||||
|
path('tasknew/', views.patt_tasknew, name='patt-tasknew'),
|
||||||
|
path('projectnew/', views.patt_projectnew, name='patt-projectnew'),
|
||||||
|
path('taskedit/<int:task_id>/', views.patt_taskedit, name='patt-taskedit'),
|
||||||
|
path('projectedit/<int:project_id>/', views.patt_projectedit, name='patt-projectedit'),
|
||||||
|
path('commentnew/<int:task_id>/', views.patt_commentnew, name='patt-commentnew'),
|
||||||
|
path('commentedit/<int:task_id>/<int:comment_id>/', views.patt_commentedit, name='patt-commentedit'),
|
||||||
|
path('filteredit/', views.patt_filteredit, name='patt-filternew'),
|
||||||
|
path('filteredit/<int:search_id>/', views.patt_filteredit, name='patt-filteredit'),
|
||||||
|
path('easysearch/', views.patt_easysearch, name='patt-easysearch'),
|
||||||
|
path('tasklist/user_filter/<int:user_filter_id>/', views.patt_tasklist, name='patt-userfilter'),
|
||||||
|
path('tasklist/common_filter/<int:common_filter_id>', views.patt_tasklist, name='patt-commonfilter'),
|
||||||
|
#
|
||||||
|
path('tasklist/search/', views.patt_tasklist, name='search'),
|
||||||
|
]
|
690
views.py
Normal file
@ -0,0 +1,690 @@
|
|||||||
|
from .access import acc_task, acc_project, create_project_possible, create_task_possible
|
||||||
|
from .context import context_adaption
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.forms import User
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.urls.base import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from .forms import TaskForm, TaskFormLimited, ProjectForm, CommentForm, TaskCommentForm, SearchForm, EasySearchForm
|
||||||
|
from .help import help_pages
|
||||||
|
from .models import Task, ModelList, Comment, TASKSTATE_CHOICES, PRIO_CHOICES, TASKS_IN_WORK
|
||||||
|
from .models import Project, Search
|
||||||
|
import patt
|
||||||
|
from .search import load_index, search, mk_search_pattern, get_project_ids_from_search_pattern, common_searches
|
||||||
|
from themes import Context
|
||||||
|
|
||||||
|
|
||||||
|
class Like(object):
|
||||||
|
DATA = []
|
||||||
|
|
||||||
|
def __init__(self, request, obj):
|
||||||
|
for key in self.DATA:
|
||||||
|
if key in request.POST:
|
||||||
|
setattr(self, key, request.POST.get(key))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
setattr(self, key, getattr(obj, key))
|
||||||
|
except AttributeError:
|
||||||
|
setattr(self, key, None)
|
||||||
|
self.__init_additions__(request, obj)
|
||||||
|
if self.creation_date is None:
|
||||||
|
self.creation_date = timezone.now()
|
||||||
|
if self.id is None:
|
||||||
|
self.id = 'xxx'
|
||||||
|
|
||||||
|
def __init_additions__(self, request, obj):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TaskLike(Like):
|
||||||
|
DATA = ['id', 'state', 'priority', 'targetdate', 'progress', 'name', 'description', 'creation_date', 'comments']
|
||||||
|
|
||||||
|
def __init_additions__(self, request, obj):
|
||||||
|
if 'project' in request.POST:
|
||||||
|
self.project = Project.objects.get(id=request.POST.get('project'))
|
||||||
|
else:
|
||||||
|
self.project = getattr(obj, 'project')
|
||||||
|
if 'assigned_user' in request.POST:
|
||||||
|
try:
|
||||||
|
self.assigned_user = User.objects.get(id=request.POST.get('assigned_user'))
|
||||||
|
except ValueError:
|
||||||
|
self.assigned_user = None
|
||||||
|
else:
|
||||||
|
self.assigned_user = getattr(obj, 'assigned_user')
|
||||||
|
#
|
||||||
|
self.class_by_state = 'task-normal' if self.state == '0' else 'task-finished'
|
||||||
|
#
|
||||||
|
if obj is None:
|
||||||
|
self.attachment_target_path = Task().attachment_target_path
|
||||||
|
else:
|
||||||
|
self.attachment_target_path = obj.attachment_target_path
|
||||||
|
|
||||||
|
if self.assigned_user and self.id:
|
||||||
|
self.taskname_prefix = '**#%d //(%s)//:** ' % (self.id, self.assigned_user.username)
|
||||||
|
elif self.id:
|
||||||
|
self.taskname_prefix = '**#%d:** ' % self.id
|
||||||
|
elif self.assigned_user:
|
||||||
|
self.taskname_prefix = '**//(%s)//:** ' % self.assigned_user.username
|
||||||
|
else:
|
||||||
|
self.taskname_prefix = ''
|
||||||
|
|
||||||
|
|
||||||
|
class CommentLike(Like):
|
||||||
|
DATA = ['id', 'type', 'creation_date', 'comment']
|
||||||
|
|
||||||
|
def __init_additions__(self, request, obj):
|
||||||
|
if 'user' in request.POST:
|
||||||
|
self.user = User.objects.get(id=request.POST.get('user'))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.user = getattr(obj, 'user')
|
||||||
|
except AttributeError:
|
||||||
|
self.user = request.user
|
||||||
|
#
|
||||||
|
if obj is None:
|
||||||
|
self.attachment_target_path = Comment().attachment_target_path
|
||||||
|
else:
|
||||||
|
self.attachment_target_path = obj.attachment_target_path
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectLike(Like):
|
||||||
|
DATA = ['id', 'name', 'description', 'state', 'creation_date']
|
||||||
|
|
||||||
|
def __init_additions__(self, request, obj):
|
||||||
|
self.role_leader = [User.objects.get(id=user_id) for user_id in request.POST.getlist('role_leader', [])]
|
||||||
|
self.role_member = [User.objects.get(id=user_id) for user_id in request.POST.getlist('role_member', [])]
|
||||||
|
self.creation_date = '-'
|
||||||
|
self.formatted_leaders = ', '.join([user.username for user in self.role_leader])
|
||||||
|
self.formatted_members = ', '.join([user.username for user in self.role_member])
|
||||||
|
#
|
||||||
|
if obj is None:
|
||||||
|
self.attachment_target_path = Project().attachment_target_path
|
||||||
|
else:
|
||||||
|
self.attachment_target_path = obj.attachment_target_path
|
||||||
|
|
||||||
|
|
||||||
|
def permission_denied_msg_project(request, project_id):
|
||||||
|
if project_id is None:
|
||||||
|
messages.error(request, _('Permission denied to create a new Project.'))
|
||||||
|
else:
|
||||||
|
messages.error(request, _('Permission denied to Project #%(project_id)d') % {'project_id': project_id})
|
||||||
|
|
||||||
|
|
||||||
|
def permission_denied_msg_task(request, task_id):
|
||||||
|
if task_id is None:
|
||||||
|
messages.error(request, _('Permission denied to create a new Task'))
|
||||||
|
else:
|
||||||
|
messages.error(request, _('Permission denied to Task #%(task_id)d') % {'task_id': task_id})
|
||||||
|
|
||||||
|
|
||||||
|
def permission_denied_msg_comment(request, task_id):
|
||||||
|
messages.error(request, _('Permission denied to add or edit a comment of Task #%(task_id)d') % {'task_id': task_id})
|
||||||
|
|
||||||
|
|
||||||
|
def no_change_msg(request):
|
||||||
|
messages.info(request, _("Nothing changed, no storage needed."))
|
||||||
|
|
||||||
|
|
||||||
|
def does_not_exist_msg_task(request, task_id):
|
||||||
|
messages.error(request, _('Task #%(id)d does not exist!') % {'id': task_id})
|
||||||
|
|
||||||
|
|
||||||
|
def does_not_exist_msg_search(request, search_id):
|
||||||
|
messages.error(request, _('Search #%(id)d does not exist!') % {'id': search_id})
|
||||||
|
|
||||||
|
|
||||||
|
def get_next(request):
|
||||||
|
if not request.POST:
|
||||||
|
return request.GET.get('next', '/')
|
||||||
|
else:
|
||||||
|
return request.POST.get('next', '/')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_tasklist(request, user_filter_id=None, common_filter_id=None):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
is_printview = patt.is_printview(request)
|
||||||
|
search_txt = patt.get_search_query(request)
|
||||||
|
if user_filter_id is not None:
|
||||||
|
try:
|
||||||
|
s = Search.objects.get(id=user_filter_id)
|
||||||
|
except Search.DoesNotExist:
|
||||||
|
does_not_exist_msg_search(request, user_filter_id)
|
||||||
|
return redirect(patt.url_tasklist(request))
|
||||||
|
search_txt = s.search_txt
|
||||||
|
title = s.name
|
||||||
|
ix = load_index()
|
||||||
|
sr = search(ix, search_txt)
|
||||||
|
if sr is None:
|
||||||
|
messages.error(request, _('Invalid search pattern: %s') % repr(search_txt))
|
||||||
|
sr = []
|
||||||
|
tasklist = ModelList(sr, acc_task, request.user)
|
||||||
|
elif search_txt is not None:
|
||||||
|
ix = load_index()
|
||||||
|
sr = search(ix, search_txt)
|
||||||
|
if sr is None:
|
||||||
|
messages.error(request, _('Invalid search pattern: %s') % repr(search_txt))
|
||||||
|
sr = []
|
||||||
|
tasklist = ModelList(sr, acc_task, request.user)
|
||||||
|
max_len = 25
|
||||||
|
if len(search_txt) > max_len:
|
||||||
|
title = _('Searchresults for %s') % repr(search_txt[:max_len - 3] + '...')
|
||||||
|
else:
|
||||||
|
title = _('Searchresults for %s') % repr(search_txt)
|
||||||
|
elif common_filter_id is not None:
|
||||||
|
try:
|
||||||
|
title, search_txt = common_searches(request)[common_filter_id]
|
||||||
|
except KeyError:
|
||||||
|
messages.error(request, _('Invalid common search: %s') % repr(common_filter_id))
|
||||||
|
sr = []
|
||||||
|
title = _('Common Search Error')
|
||||||
|
else:
|
||||||
|
ix = load_index()
|
||||||
|
sr = search(ix, search_txt)
|
||||||
|
if sr is None:
|
||||||
|
messages.error(request, _('Invalid search pattern: %s') % repr(search_txt))
|
||||||
|
sr = []
|
||||||
|
tasklist = ModelList(sr, acc_task, request.user)
|
||||||
|
else:
|
||||||
|
tasklist = ModelList(Task.objects.filter(state__in=TASKS_IN_WORK), acc_task, request.user)
|
||||||
|
title = _("All Tasks in work")
|
||||||
|
try:
|
||||||
|
project_id = get_project_ids_from_search_pattern(search_txt)[0]
|
||||||
|
except IndexError:
|
||||||
|
project_id = None
|
||||||
|
except TypeError:
|
||||||
|
project_id = None
|
||||||
|
tasklist.sort()
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
tasklist=tasklist, # the tasklist to be shown (template)
|
||||||
|
printview=is_printview, # Show reduced view and functionality in printview
|
||||||
|
target_head=None if is_printview else 'patt-taskview', # target-link for head (template)
|
||||||
|
project_id=project_id, # as default Value for New Task entry
|
||||||
|
title=title, # the title for the page (template)
|
||||||
|
)
|
||||||
|
if patt.is_printview(request):
|
||||||
|
return render(request, 'patt/tasklist_print.html', context=context)
|
||||||
|
else:
|
||||||
|
return render(request, 'patt/tasklist.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_projectlist(request):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
projectlist = ModelList(Project.objects.all(), acc_project, request.user)
|
||||||
|
projectlist.sort()
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
projectlist=projectlist, # the projectlist to be shown (template)
|
||||||
|
target_head='patt-projectedit', # target-link for head (template)
|
||||||
|
title=_("Projectlist"), # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/projectlist.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_taskview(request, task_id):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
is_printview = patt.is_printview(request)
|
||||||
|
try:
|
||||||
|
task = Task.objects.get(id=task_id)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
does_not_exist_msg_task(request, task_id)
|
||||||
|
else:
|
||||||
|
acc = acc_task(task, request.user)
|
||||||
|
if acc.read:
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
task=task, # the task to be shown (template)
|
||||||
|
printview=is_printview, # Show reduced view and functionality in printview
|
||||||
|
taskview=True, # deactivate collapse and expand buttons
|
||||||
|
target_head=None if is_printview else 'patt-taskedit', # target-link for head (template)
|
||||||
|
project_id=task.project.id, # as default Value for New Task entry
|
||||||
|
title=_('Task #%d') % task.id # the title for the page (template)
|
||||||
|
)
|
||||||
|
if patt.is_printview(request):
|
||||||
|
return render(request, 'patt/taskview_print.html', context=context)
|
||||||
|
else:
|
||||||
|
return render(request, 'patt/taskview.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_task(request, task.id)
|
||||||
|
return redirect(reverse('patt-tasklist'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_tasknew(request):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
if create_task_possible(request.user):
|
||||||
|
if not request.POST: # Initial Form...
|
||||||
|
task = Task(project_id=request.GET.get('project_id'))
|
||||||
|
form = TaskForm(instance=task, request=request)
|
||||||
|
form_comment = TaskCommentForm()
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the task form (template)
|
||||||
|
form_comment=form_comment, # the form object to create the comment form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
title=_('New Task') # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/task_form.html', context=context)
|
||||||
|
else:
|
||||||
|
task = Task()
|
||||||
|
form = TaskForm(request.POST, instance=task, request=request)
|
||||||
|
form_comment = TaskCommentForm(request.POST)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the task form (template)
|
||||||
|
form_comment=form_comment, # the form object to create the comment form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
title=_('New Task') # the title for the page (template)
|
||||||
|
)
|
||||||
|
if request.POST.get('save'): # Save form content...
|
||||||
|
if form.is_valid() and form_comment.is_valid():
|
||||||
|
form.save()
|
||||||
|
if request.POST.get('comment'):
|
||||||
|
form_comment = TaskCommentForm(request.POST, instance=Comment(task_id=form.instance.id, user=request.user))
|
||||||
|
form_comment.save()
|
||||||
|
messages.success(request, _('Thanks for adding Task #%(task_id)d.') % {'task_id': form.instance.id})
|
||||||
|
return redirect(nxt + '#task-%d' % form.instance.id)
|
||||||
|
elif request.POST.get('preview'): # Create a preview
|
||||||
|
context['comment_new'] = CommentLike(request, None) # the new comment to be used in the preview
|
||||||
|
context['template'] = 'patt/task/task.html' # the template for preview (template)
|
||||||
|
context['task'] = TaskLike(request, task) # the object to be used in the preview template
|
||||||
|
context['printview'] = True # Show reduced view and functionality like in printview
|
||||||
|
return render(request, 'patt/task_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_task(request, None)
|
||||||
|
return redirect(nxt)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_projectnew(request):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
if create_project_possible(request.user):
|
||||||
|
project = Project()
|
||||||
|
if not request.POST:
|
||||||
|
form = ProjectForm(instance=project)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('New Project') # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
form = ProjectForm(request.POST, instance=project)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('New Project') # the title for the page (template)
|
||||||
|
)
|
||||||
|
#
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _('Thanks for adding Project #%(project_id)d.') % {'project_id': form.instance.id})
|
||||||
|
return redirect(nxt + '#project-%d' % form.instance.id)
|
||||||
|
elif request.POST.get('preview'):
|
||||||
|
context['template'] = 'patt/project/project.html' # the template for preview (template)
|
||||||
|
context['project'] = ProjectLike(request, None) # the object to be used in the preview template
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_project(request, None)
|
||||||
|
return redirect(nxt)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_taskedit(request, task_id, **kwargs):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
try:
|
||||||
|
task = Task.objects.get(id=task_id)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
does_not_exist_msg_task(request, task_id)
|
||||||
|
return redirect(nxt)
|
||||||
|
else:
|
||||||
|
acc = acc_task(task, request.user)
|
||||||
|
if acc.modify or acc.modify_limited:
|
||||||
|
if acc.modify:
|
||||||
|
ThisForm = TaskForm
|
||||||
|
else:
|
||||||
|
ThisForm = TaskFormLimited
|
||||||
|
if not request.POST:
|
||||||
|
# get request parameters
|
||||||
|
do = request.GET.get('do')
|
||||||
|
state = request.GET.get('state')
|
||||||
|
if state:
|
||||||
|
state = int(state)
|
||||||
|
priority = request.GET.get('priority')
|
||||||
|
if priority:
|
||||||
|
priority = int(priority)
|
||||||
|
#
|
||||||
|
if do == 'edit':
|
||||||
|
form = ThisForm(instance=task, request=request)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the task form (template)
|
||||||
|
form_comment=TaskCommentForm(), # the form object to create the comment form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task.id, # the anchor for redirection
|
||||||
|
title=_('Edit Task #%d') % task.id # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/task_form.html', context=context)
|
||||||
|
elif do == 'set_state':
|
||||||
|
if state in acc.allowed_targetstates:
|
||||||
|
task.state = state
|
||||||
|
task.save()
|
||||||
|
messages.success(request, _('State of Task #%(task_id)d set to %(name)s') % {'task_id': task.id, 'name': dict(TASKSTATE_CHOICES)[state]})
|
||||||
|
else:
|
||||||
|
permission_denied_msg_task(request, task_id)
|
||||||
|
elif do == 'set_priority':
|
||||||
|
if priority in acc.allowed_targetpriority:
|
||||||
|
task.priority = priority
|
||||||
|
task.save()
|
||||||
|
messages.success(request, _('Priority of Task #%(task_id)d set to %(name)s') % {'task_id': task.id, 'name': dict(PRIO_CHOICES)[priority]})
|
||||||
|
else:
|
||||||
|
permission_denied_msg_task(request, task_id)
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Edit with do="%s" not yet implemented!' % do)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
else:
|
||||||
|
comment = Comment(task_id=task_id, user=request.user)
|
||||||
|
form = ThisForm(request.POST, instance=task, request=request)
|
||||||
|
form_comment = TaskCommentForm(request.POST, instance=comment)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the task form (template)
|
||||||
|
form_comment=form_comment, # the form object to create the comment form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task.id, # the anchor for redirection
|
||||||
|
title=_('Edit Task #%d') % task.id # the title for the page (template)
|
||||||
|
)
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid() and form_comment.is_valid(): # TODO: Validator depending on modify and modify_limited (to ensure content is okay for this user)!
|
||||||
|
save_needed = form.save().save_needed
|
||||||
|
if request.POST.get('comment'):
|
||||||
|
save_needed |= form_comment.save().save_needed
|
||||||
|
if save_needed:
|
||||||
|
messages.success(request, _('Thanks for editing Task #%(task_id)d.') % {'task_id': task.id})
|
||||||
|
else:
|
||||||
|
no_change_msg(request)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
elif request.POST.get('preview'):
|
||||||
|
context['comment_new'] = CommentLike(request, comment) # the new comment to be used in the preview
|
||||||
|
context['template'] = 'patt/task/task.html' # the template for preview (template)
|
||||||
|
context['task'] = TaskLike(request, task) # the object to be used in the preview template
|
||||||
|
context['printview'] = True # Show reduced view and functionality like in printview
|
||||||
|
return render(request, 'patt/task_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_task(request, task_id)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_projectedit(request, project_id):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
try:
|
||||||
|
project = Project.objects.get(id=project_id)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
messages.error(request, _('Project with id %(project_id)d does not exist!') % {'project_id': project_id})
|
||||||
|
return redirect(nxt)
|
||||||
|
else:
|
||||||
|
acc = acc_project(project, request.user)
|
||||||
|
if acc.modify:
|
||||||
|
if not request.POST:
|
||||||
|
form = ProjectForm(instance=project)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='project-%d' % project_id, # the anchor for redirection
|
||||||
|
item_id=project.id, # project.id for Preview, ... (template)
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Project #%d') % project_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
form = ProjectForm(request.POST, instance=project)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='project-%d' % project_id, # the anchor for redirection
|
||||||
|
item_id=project.id, # project.id for Preview, ... (template)
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Project #%d') % project_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
#
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _('Thanks for editing Project #%(project_id)d.') % {'project_id': project.id})
|
||||||
|
return redirect(nxt + '#project-%d' % project_id)
|
||||||
|
elif request.POST.get('preview'):
|
||||||
|
context['template'] = 'patt/project/project.html' # the template for preview (template)
|
||||||
|
context['project'] = ProjectLike(request, project) # the object to be used in the preview template
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_project(request, project_id)
|
||||||
|
return redirect(nxt + '#project-%d' % project_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_commentnew(request, task_id):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
try:
|
||||||
|
task = Task.objects.get(id=task_id)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
does_not_exist_msg_task(request, task_id)
|
||||||
|
return redirect(nxt)
|
||||||
|
else:
|
||||||
|
acc = acc_task(task, request.user)
|
||||||
|
if acc.add_comments:
|
||||||
|
if not request.POST:
|
||||||
|
form = CommentForm(instance=Comment())
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task_id, # the anchor for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Add a Comment (Task #%d)') % task_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
comment = Comment(task_id=task_id, user=request.user)
|
||||||
|
#
|
||||||
|
form = CommentForm(request.POST, instance=comment)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task_id, # the anchor for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Add Comment to Task #%d') % task_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _('Thanks for adding a comment to Task #%(task_id)d.') % {'task_id': task_id})
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
elif request.POST.get('preview'):
|
||||||
|
context['template'] = 'patt/task/comment.html' # the template for preview (template)
|
||||||
|
context['comment'] = CommentLike(request, comment) # the object to be used in the preview template
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_comment(request, task_id)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def patt_commentedit(request, task_id, comment_id):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
nxt = get_next(request)
|
||||||
|
comment = Comment.objects.get(id=comment_id)
|
||||||
|
acc = acc_task(comment.task, request.user)
|
||||||
|
if acc.modify_comment:
|
||||||
|
if not request.POST:
|
||||||
|
form = CommentForm(instance=comment)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task_id, # the anchor for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Comment (Task #%d)') % task_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
form = CommentForm(request.POST, instance=comment)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
next=nxt, # the url for redirection
|
||||||
|
next_anchor='task-%d' % task_id, # the anchor for redirection
|
||||||
|
disable_delete=True, # disable delete button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Comment (Task #%d)') % task_id # the title for the page (template)
|
||||||
|
)
|
||||||
|
#
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid():
|
||||||
|
if form.save().save_needed:
|
||||||
|
messages.success(request, _('Thanks for editing a comment of Task #%(task_id)d.') % {'task_id': task_id})
|
||||||
|
else:
|
||||||
|
no_change_msg(request)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
elif request.POST.get('preview'):
|
||||||
|
context['template'] = 'patt/task/comment.html' # the template for preview (template)
|
||||||
|
context['comment'] = CommentLike(request, comment) # the object to be used in the preview template
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
permission_denied_msg_comment(request, task_id)
|
||||||
|
return redirect(nxt + '#task-%d' % task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def patt_filteredit(request, search_id=None):
|
||||||
|
def filter_does_not_exist_error(request, search_id):
|
||||||
|
messages.error(request, _('Filter #%d does not exist.') % search_id)
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
if not request.POST:
|
||||||
|
if search_id is not None:
|
||||||
|
try:
|
||||||
|
form = SearchForm(instance=Search.objects.get(id=search_id))
|
||||||
|
except Search.DoesNotExist:
|
||||||
|
filter_does_not_exist_error(request, search_id)
|
||||||
|
return redirect('/')
|
||||||
|
else:
|
||||||
|
if patt.get_search_query(request) is None:
|
||||||
|
form = SearchForm(initial={'user': request.user})
|
||||||
|
else:
|
||||||
|
form = SearchForm(initial={'search_txt': patt.get_search_query(request), 'user': request.user})
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
disable_preview=True, # disable the preview button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Filter') # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
if search_id is not None:
|
||||||
|
try:
|
||||||
|
s = Search.objects.get(id=search_id)
|
||||||
|
except Search.DoesNotExist:
|
||||||
|
filter_does_not_exist_error(request, search_id)
|
||||||
|
return redirect('/')
|
||||||
|
else:
|
||||||
|
s = Search(user=request.user)
|
||||||
|
if request.user == s.user:
|
||||||
|
form = SearchForm(request.POST, instance=s)
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
disable_preview=True, # disable the preview button
|
||||||
|
disable_search=True, # disable search button
|
||||||
|
title=_('Edit Filter') # the title for the page (template)
|
||||||
|
)
|
||||||
|
#
|
||||||
|
if request.POST.get('save'):
|
||||||
|
if form.is_valid():
|
||||||
|
s.save()
|
||||||
|
messages.success(request, _('Thanks for editing Filter #%(search_id)d.') % {'search_id': s.id})
|
||||||
|
return redirect(patt.url_tasklist(request, user_filter_id=s.id))
|
||||||
|
elif request.POST.get('delete'):
|
||||||
|
messages.success(request, _('Filter #%(search_id)d delteted.') % {'search_id': s.id})
|
||||||
|
s.delete()
|
||||||
|
return redirect('/')
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
messages.error(request, _('Access to Filter (%s) denied.') % repr(search_id))
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
def patt_easysearch(request):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
if not request.POST:
|
||||||
|
form = EasySearchForm()
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
form=form, # the form object to create the form (template)
|
||||||
|
disable_preview=True, # disable the preview button
|
||||||
|
disable_delete=True, # disable the delete button
|
||||||
|
disable_save=True, # disable save button
|
||||||
|
title=_('Edit Filter') # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/raw_single_form.html', context=context)
|
||||||
|
else:
|
||||||
|
form = EasySearchForm(request.POST)
|
||||||
|
form_data = dict(form.data)
|
||||||
|
if 'states' not in form_data:
|
||||||
|
form_data['states'] = []
|
||||||
|
return redirect(patt.url_tasklist(request, search_txt=mk_search_pattern(**form_data)))
|
||||||
|
|
||||||
|
|
||||||
|
def patt_helpview(request, page='main'):
|
||||||
|
context = Context(request) # needs to be executed first because of time mesurement
|
||||||
|
help_content = help_pages[page]
|
||||||
|
context_adaption(
|
||||||
|
context, # the base context
|
||||||
|
request, # the request object to be used in context_adaption
|
||||||
|
current_help_page=page, # the current help_page to identify which taskbar entry has to be highlighted
|
||||||
|
help_content=help_content, # the help content itself (template)
|
||||||
|
title=_('Help') # the title for the page (template)
|
||||||
|
)
|
||||||
|
return render(request, 'patt/help.html', context=context)
|