mirror of https://github.com/jumpserver/jumpserver
[Fixture] 添加task list 搜索,重试
parent
a4504dc0c7
commit
caec9709ef
|
@ -12,8 +12,8 @@ def update_assets_hardware_info(assets):
|
||||||
task_tuple = (
|
task_tuple = (
|
||||||
('setup', ''),
|
('setup', ''),
|
||||||
)
|
)
|
||||||
task = run_AdHoc.delay(task_tuple, assets, record=False)
|
task_name = ','.join([asset.hostname for asset in assets])
|
||||||
summary, result = task.get(timeout=60*10)
|
summary, result = run_AdHoc(task_tuple, assets, record=True, task_name=task_name)
|
||||||
for hostname, info in result['contacted'].items():
|
for hostname, info in result['contacted'].items():
|
||||||
if info:
|
if info:
|
||||||
info = info[0]['ansible_facts']
|
info = info[0]['ansible_facts']
|
||||||
|
|
|
@ -46,12 +46,12 @@ class ProxyLogListView(AdminUserRequiredMixin, ListView):
|
||||||
filter_kwargs = {}
|
filter_kwargs = {}
|
||||||
if self.date_from_s:
|
if self.date_from_s:
|
||||||
date_from = datetime.strptime(self.date_from_s, self.date_format)
|
date_from = datetime.strptime(self.date_from_s, self.date_format)
|
||||||
date_from.replace(tzinfo=timezone.get_current_timezone())
|
date_from = date_from.replace(tzinfo=timezone.get_current_timezone())
|
||||||
filter_kwargs['date_start__gt'] = date_from
|
filter_kwargs['date_start__gt'] = date_from
|
||||||
if self.date_to_s:
|
if self.date_to_s:
|
||||||
date_to = timezone.datetime.strptime(
|
date_to = timezone.datetime.strptime(
|
||||||
self.date_to_s + ' 23:59:59', '%m/%d/%Y %H:%M:%S')
|
self.date_to_s + ' 23:59:59', '%m/%d/%Y %H:%M:%S')
|
||||||
date_to.replace(tzinfo=timezone.get_current_timezone())
|
date_to = date_to.replace(tzinfo=timezone.get_current_timezone())
|
||||||
filter_kwargs['date_start__lt'] = date_to
|
filter_kwargs['date_start__lt'] = date_to
|
||||||
if self.user:
|
if self.user:
|
||||||
filter_kwargs['user'] = self.user
|
filter_kwargs['user'] = self.user
|
||||||
|
@ -64,6 +64,8 @@ class ProxyLogListView(AdminUserRequiredMixin, ListView):
|
||||||
Q(user__icontains=self.keyword) |
|
Q(user__icontains=self.keyword) |
|
||||||
Q(asset__icontains=self.keyword) |
|
Q(asset__icontains=self.keyword) |
|
||||||
Q(system_user__icontains=self.keyword)).distinct()
|
Q(system_user__icontains=self.keyword)).distinct()
|
||||||
|
if filter_kwargs:
|
||||||
|
self.queryset = self.queryset.filter(**filter_kwargs)
|
||||||
return self.queryset
|
return self.queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
|
@ -19,7 +19,7 @@ from .callback import AdHocResultCallback, PlaybookResultCallBack, \
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["ADHocRunner", "Options"]
|
__all__ = ["AdHocRunner", "PlayBookRunner"]
|
||||||
|
|
||||||
C.HOST_KEY_CHECKING = False
|
C.HOST_KEY_CHECKING = False
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,13 @@ import json
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
__all__ = ["TaskRecord"]
|
__all__ = ["Task"]
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskRecord(models.Model):
|
class Task(models.Model):
|
||||||
uuid = models.CharField(max_length=128, verbose_name=_('UUID'), primary_key=True)
|
uuid = models.CharField(max_length=128, verbose_name=_('UUID'), primary_key=True)
|
||||||
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
||||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time'))
|
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time'))
|
||||||
|
|
|
@ -2,108 +2,21 @@
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from assets.models import Asset
|
from common.utils import get_logger
|
||||||
from common.utils import get_logger, encrypt_password
|
from .utils import run_AdHoc
|
||||||
from .ansible.runner import AdHocRunner
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True)
|
@shared_task
|
||||||
def run_AdHoc(self, task_tuple, assets,
|
def rerun_task(task_id):
|
||||||
task_name='Ansible AdHoc runner',
|
from .models import Task
|
||||||
pattern='all', record=True):
|
record = Task.objects.get(uuid=task_id)
|
||||||
|
|
||||||
if not assets:
|
|
||||||
logger.warning('Empty assets, runner cancel')
|
|
||||||
if isinstance(assets[0], Asset):
|
|
||||||
assets = [asset._to_secret_json() for asset in assets]
|
|
||||||
|
|
||||||
runner = AdHocRunner(assets)
|
|
||||||
if record:
|
|
||||||
from .models import TaskRecord
|
|
||||||
if not TaskRecord.objects.filter(uuid=self.request.id):
|
|
||||||
record = TaskRecord(uuid=self.request.id,
|
|
||||||
name=task_name,
|
|
||||||
assets=','.join(str(asset['id']) for asset in assets),
|
|
||||||
module_args=task_tuple,
|
|
||||||
pattern=pattern)
|
|
||||||
record.save()
|
|
||||||
else:
|
|
||||||
record = TaskRecord.objects.get(uuid=self.request.id)
|
|
||||||
record.date_start = timezone.now()
|
|
||||||
ts_start = time.time()
|
|
||||||
logger.warn('Start runner {}'.format(task_name))
|
|
||||||
result = runner.run(task_tuple, pattern=pattern, task_name=task_name)
|
|
||||||
timedelta = round(time.time() - ts_start, 2)
|
|
||||||
summary = runner.clean_result()
|
|
||||||
if record:
|
|
||||||
record.date_finished = timezone.now()
|
|
||||||
record.is_finished = True
|
|
||||||
record.result = json.dumps(result)
|
|
||||||
record.summary = json.dumps(summary)
|
|
||||||
record.timedelta = timedelta
|
|
||||||
if len(summary['failed']) == 0:
|
|
||||||
record.is_success = True
|
|
||||||
else:
|
|
||||||
record.is_success = False
|
|
||||||
record.save()
|
|
||||||
return summary, result
|
|
||||||
|
|
||||||
|
|
||||||
def rerun_AdHoc(uuid):
|
|
||||||
from .models import TaskRecord
|
|
||||||
record = TaskRecord.objects.get(uuid=uuid)
|
|
||||||
assets = record.assets_json
|
assets = record.assets_json
|
||||||
task_tuple = record.module_args
|
task_tuple = record.module_args
|
||||||
pattern = record.pattern
|
pattern = record.pattern
|
||||||
task_name = record.name
|
task_name = record.name
|
||||||
task = run_AdHoc.apply_async((task_tuple, assets),
|
return run_AdHoc(task_tuple, assets, pattern=pattern,
|
||||||
{'pattern': pattern, 'task_name': task_name},
|
task_name=task_name, task_id=task_id)
|
||||||
task_id=uuid)
|
|
||||||
return task
|
|
||||||
|
|
||||||
|
|
||||||
def push_users(assets, users):
|
|
||||||
"""
|
|
||||||
user: {
|
|
||||||
name: 'web',
|
|
||||||
username: 'web',
|
|
||||||
shell: '/bin/bash',
|
|
||||||
password: '123123123',
|
|
||||||
public_key: 'string',
|
|
||||||
sudo: '/bin/whoami,/sbin/ifconfig'
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
if isinstance(users, dict):
|
|
||||||
users = [users]
|
|
||||||
if isinstance(assets, dict):
|
|
||||||
assets = [assets]
|
|
||||||
task_tuple = []
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
# 添加用户, 设置公钥, 设置sudo
|
|
||||||
task_tuple.extend([
|
|
||||||
('user', 'name={} shell={} state=present password={}'.format(
|
|
||||||
user['username'], user.get('shell', '/bin/bash'),
|
|
||||||
encrypt_password(user.get('password', None)))),
|
|
||||||
('authorized_key', "user={} state=present key='{}'".format(
|
|
||||||
user['username'], user['public_key'])),
|
|
||||||
('lineinfile',
|
|
||||||
"name=/etc/sudoers state=present regexp='^{0} ALL=(ALL)' "
|
|
||||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
|
||||||
"validate='visudo -cf %s'".format(
|
|
||||||
user['username'], user.get('sudo', '/bin/whoami')
|
|
||||||
))
|
|
||||||
])
|
|
||||||
task_name = 'Push user {}'.format(','.join([user['name'] for user in users]))
|
|
||||||
task = run_AdHoc.delay(task_tuple, assets, pattern='all', task_name=task_name)
|
|
||||||
return task
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Date start' %}:</td>
|
<td>{% trans 'Date start' %}:</td>
|
||||||
<td><b>{{ object.date_start}}</b></td>
|
<td><b>{{ object.date_start }}</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Date finished' %}:</td>
|
<td>{% trans 'Date finished' %}:</td>
|
||||||
|
@ -64,11 +64,11 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Is finished' %}:</td>
|
<td>{% trans 'Is finished' %}:</td>
|
||||||
<td><b>{{ object.is_finished }}</b></td>
|
<td><b>{{ object.is_finished|yesno:"Yes,No,Unkown" }}</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Is success ' %}:</td>
|
<td>{% trans 'Is success ' %}:</td>
|
||||||
<td><b>{{ object.is_success }}</b></td>
|
<td><b>{{ object.is_success|yesno:"Yes,No,Unkown" }}</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans 'Assets ' %}:</td>
|
<td>{% trans 'Assets ' %}:</td>
|
|
@ -46,10 +46,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block table_body %}
|
{% block table_body %}
|
||||||
{% for object in task_record_list %}
|
{% for object in task_list %}
|
||||||
<tr class="gradeX">
|
<tr class="gradeX">
|
||||||
<td class="text-center"><input type="checkbox" class="cbx-term"> </td>
|
<td class="text-center"><input type="checkbox" class="cbx-term"> </td>
|
||||||
<td class="text-center"><a href="{% url 'ops:task-record-detail' pk=object.uuid %}">{{ object.name }}</a></td>
|
<td class="text-center"><a href="{% url 'ops:task-detail' pk=object.uuid %}">{{ object.name }}</a></td>
|
||||||
<td class="text-center">{{ object.total_assets|length }}</td>
|
<td class="text-center">{{ object.total_assets|length }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if object.is_success %}
|
{% if object.is_success %}
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
<td class="text-center">{{ object.date_start }}</td>
|
<td class="text-center">{{ object.date_start }}</td>
|
||||||
<td class="text-center">{{ object.timedelta }} s</td>
|
<td class="text-center">{{ object.timedelta }} s</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="" class="btn btn-xs btn-info">{% trans "Repush" %}</a>
|
<a href="{% url 'ops:task-run' pk=object.uuid %}" class="btn btn-xs btn-info">{% trans "Run again" %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
|
@ -9,6 +9,7 @@ __all__ = ["urlpatterns"]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# TResource Task url
|
# TResource Task url
|
||||||
url(r'^task-record/$', views.TaskRecordListView.as_view(), name='task-record-list'),
|
url(r'^task/$', views.TaskListView.as_view(), name='task-list'),
|
||||||
url(r'^task-record/(?P<pk>[0-9a-zA-Z-]+)/$', views.TaskRecordDetailView.as_view(), name='task-record-detail'),
|
url(r'^task/(?P<pk>[0-9a-zA-Z-]+)/$', views.TaskDetailView.as_view(), name='task-detail'),
|
||||||
|
url(r'^task/(?P<pk>[0-9a-zA-Z-]+)/run/$', views.TaskRunView.as_view(), name='task-run'),
|
||||||
]
|
]
|
|
@ -17,7 +17,8 @@ logger = get_logger(__file__)
|
||||||
|
|
||||||
def run_AdHoc(task_tuple, assets,
|
def run_AdHoc(task_tuple, assets,
|
||||||
task_name='Ansible AdHoc runner',
|
task_name='Ansible AdHoc runner',
|
||||||
task_id=None, pattern='all', record=True):
|
task_id=None, pattern='all',
|
||||||
|
record=True, verbose=False):
|
||||||
|
|
||||||
if not assets:
|
if not assets:
|
||||||
logger.warning('Empty assets, runner cancel')
|
logger.warning('Empty assets, runner cancel')
|
||||||
|
@ -29,25 +30,27 @@ def run_AdHoc(task_tuple, assets,
|
||||||
|
|
||||||
runner = AdHocRunner(assets)
|
runner = AdHocRunner(assets)
|
||||||
if record:
|
if record:
|
||||||
from .models import TaskRecord
|
from .models import Task
|
||||||
if not TaskRecord.objects.filter(uuid=task_id):
|
if not Task.objects.filter(uuid=task_id):
|
||||||
record = TaskRecord(uuid=task_id,
|
record = Task(uuid=task_id,
|
||||||
name=task_name,
|
name=task_name,
|
||||||
assets=','.join(str(asset['id']) for asset in assets),
|
assets=','.join(str(asset['id']) for asset in assets),
|
||||||
module_args=task_tuple,
|
module_args=task_tuple,
|
||||||
pattern=pattern)
|
pattern=pattern)
|
||||||
record.save()
|
record.save()
|
||||||
else:
|
else:
|
||||||
record = TaskRecord.objects.get(uuid=task_id)
|
record = Task.objects.get(uuid=task_id)
|
||||||
record.date_start = timezone.now()
|
record.date_start = timezone.now()
|
||||||
ts_start = time.time()
|
ts_start = time.time()
|
||||||
logger.warn('Start runner {}'.format(task_name))
|
if verbose:
|
||||||
|
logger.debug('Start runner {}'.format(task_name))
|
||||||
result = runner.run(task_tuple, pattern=pattern, task_name=task_name)
|
result = runner.run(task_tuple, pattern=pattern, task_name=task_name)
|
||||||
timedelta = round(time.time() - ts_start, 2)
|
timedelta = round(time.time() - ts_start, 2)
|
||||||
summary = runner.clean_result()
|
summary = runner.clean_result()
|
||||||
if record:
|
if record:
|
||||||
record.date_finished = timezone.now()
|
record.date_finished = timezone.now()
|
||||||
record.is_finished = True
|
record.is_finished = True
|
||||||
|
if verbose:
|
||||||
record.result = json.dumps(result)
|
record.result = json.dumps(result)
|
||||||
record.summary = json.dumps(summary)
|
record.summary = json.dumps(summary)
|
||||||
record.timedelta = timedelta
|
record.timedelta = timedelta
|
||||||
|
@ -57,21 +60,3 @@ def run_AdHoc(task_tuple, assets,
|
||||||
record.is_success = False
|
record.is_success = False
|
||||||
record.save()
|
record.save()
|
||||||
return summary, result
|
return summary, result
|
||||||
|
|
||||||
|
|
||||||
def rerun_AdHoc(task_id):
|
|
||||||
from .models import TaskRecord
|
|
||||||
record = TaskRecord.objects.get(uuid=task_id)
|
|
||||||
assets = record.assets_json
|
|
||||||
task_tuple = record.module_args
|
|
||||||
pattern = record.pattern
|
|
||||||
task_name = record.name
|
|
||||||
return run_AdHoc(task_tuple, assets, pattern=pattern,
|
|
||||||
task_name=task_name, task_id=task_id)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,69 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.generic import ListView, DetailView
|
from django.views.generic import ListView, DetailView, View
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.shortcuts import redirect, reverse
|
||||||
|
|
||||||
from .models import TaskRecord
|
from .models import Task
|
||||||
|
from ops.tasks import rerun_task
|
||||||
|
|
||||||
|
|
||||||
class TaskRecordListView(ListView):
|
class TaskListView(ListView):
|
||||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||||
model = TaskRecord
|
model = Task
|
||||||
ordering = ('-date_start',)
|
ordering = ('-date_start',)
|
||||||
context_object_name = 'task_record_list'
|
context_object_name = 'task_list'
|
||||||
template_name = 'ops/task_record_list.html'
|
template_name = 'ops/task_list.html'
|
||||||
|
date_format = '%m/%d/%Y'
|
||||||
|
keyword = date_from_s = date_to_s = ''
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
date_now = timezone.localtime(timezone.now())
|
||||||
|
date_to_default = date_now.strftime(self.date_format)
|
||||||
|
date_from_default = (date_now - timezone.timedelta(7)) \
|
||||||
|
.strftime(self.date_format)
|
||||||
|
|
||||||
|
self.queryset = super(TaskListView, self).get_queryset()
|
||||||
|
self.keyword = self.request.GET.get('keyword', '')
|
||||||
|
self.date_from_s = self.request.GET.get('date_from', date_from_default)
|
||||||
|
self.date_to_s = self.request.GET.get('date_to', date_to_default)
|
||||||
|
|
||||||
|
if self.date_from_s:
|
||||||
|
date_from = datetime.strptime(self.date_from_s, self.date_format)
|
||||||
|
date_from = date_from.replace(tzinfo=timezone.get_current_timezone())
|
||||||
|
self.queryset = self.queryset.filter(date_start__gt=date_from)
|
||||||
|
|
||||||
|
if self.date_to_s:
|
||||||
|
date_to = timezone.datetime.strptime(
|
||||||
|
self.date_to_s + ' 23:59:59', '%m/%d/%Y %H:%M:%S')
|
||||||
|
date_to = date_to.replace(tzinfo=timezone.get_current_timezone())
|
||||||
|
self.queryset = self.queryset.filter(date_finished__lt=date_to)
|
||||||
|
|
||||||
|
if self.keyword:
|
||||||
|
self.queryset = self.queryset.filter(
|
||||||
|
name__icontains=self.keyword,
|
||||||
|
)
|
||||||
|
return self.queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = {
|
context = {
|
||||||
'app': 'Ops',
|
'app': 'Ops',
|
||||||
'action': 'Task record list',
|
'action': 'Task record list',
|
||||||
|
'date_from': self.date_from_s,
|
||||||
|
'date_to': self.date_to_s,
|
||||||
|
'keyword': self.keyword,
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super(TaskRecordListView, self).get_context_data(**kwargs)
|
return super(TaskListView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TaskRecordDetailView(DetailView):
|
class TaskDetailView(DetailView):
|
||||||
model = TaskRecord
|
model = Task
|
||||||
template_name = 'ops/task_record_detail.html'
|
template_name = 'ops/task_detail.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = {
|
context = {
|
||||||
|
@ -36,4 +72,13 @@ class TaskRecordDetailView(DetailView):
|
||||||
'results': json.loads(self.object.summary),
|
'results': json.loads(self.object.summary),
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super(TaskRecordDetailView, self).get_context_data(**kwargs)
|
return super(TaskDetailView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRunView(View):
|
||||||
|
pk_url_kwarg = 'pk'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
pk = kwargs.get(self.pk_url_kwarg)
|
||||||
|
rerun_task.delay(pk)
|
||||||
|
return redirect(reverse('ops:task-detail', kwargs={'pk': pk}))
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from common.utils import setattr_bulk, get_logger
|
from common.utils import setattr_bulk, get_logger
|
||||||
from ops.tasks import push_users
|
from .tasks import push_users
|
||||||
from .hands import User, UserGroup, Asset, AssetGroup, SystemUser
|
from .hands import User, UserGroup, Asset, AssetGroup, SystemUser
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -231,7 +231,7 @@ def push_system_user(assets, system_user):
|
||||||
|
|
||||||
assets = [asset._to_secret_json() for asset in assets]
|
assets = [asset._to_secret_json() for asset in assets]
|
||||||
system_user = system_user._to_secret_json()
|
system_user = system_user._to_secret_json()
|
||||||
task = push_users(assets, system_user)
|
task = push_users.delay(assets, system_user)
|
||||||
return task.id
|
return task.id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
|
<i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="nav nav-second-level">
|
<ul class="nav nav-second-level">
|
||||||
<li id="task-record"><a href="{% url 'ops:task-record-list' %}">{% trans 'Task Record' %}</a></li>
|
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task' %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue