Repository: itimor/one-workflow Branch: master Commit: 805035def06b Files: 242 Total size: 993.9 KB Directory structure: gitextract_nol__jqe/ ├── .gitignore ├── 500.html ├── README.md ├── backend/ │ ├── __init__.py │ ├── celery.service │ ├── common/ │ │ ├── JSONRenderer.py │ │ ├── __init__.py │ │ ├── dispath.py │ │ ├── django.py │ │ ├── exceptions.py │ │ ├── models.py │ │ ├── pagination.py │ │ ├── status.py │ │ └── views.py │ ├── core/ │ │ ├── __init__.py │ │ ├── celery.py │ │ ├── settings/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── dev.py │ │ │ ├── mac.py │ │ │ └── prod.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── dev_requirements.txt │ ├── init.sh │ ├── manage.py │ ├── notices/ │ │ ├── __init__.py │ │ ├── management/ │ │ │ ├── __init__.py │ │ │ └── commands/ │ │ │ ├── __init__.py │ │ │ └── init_notice.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── urls.py │ │ └── views.py │ ├── oms.ini │ ├── oms.service │ ├── requirements.txt │ ├── systems/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── management/ │ │ │ ├── __init__.py │ │ │ └── commands/ │ │ │ ├── __init__.py │ │ │ └── init_sys.py │ │ ├── menus.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── serializers.py │ │ ├── urls.py │ │ └── views.py │ ├── tickets/ │ │ ├── __init__.py │ │ ├── filters.py │ │ ├── management/ │ │ │ ├── __init__.py │ │ │ └── commands/ │ │ │ ├── __init__.py │ │ │ └── init_ticket.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── urls.py │ │ └── views.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── filesize.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── storage.py │ │ ├── urls.py │ │ └── views.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── get_realip.py │ │ ├── index.py │ │ ├── mysql.py │ │ ├── sendmail.py │ │ ├── sendskype.py │ │ ├── test.py │ │ ├── time.py │ │ └── verifys.py │ └── workflows/ │ ├── __init__.py │ ├── admin.py │ ├── management/ │ │ ├── __init__.py │ │ └── commands/ │ │ ├── __init__.py │ │ ├── init_leave.py │ │ └── init_wf.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── frontend/ │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .travis.yml │ ├── LICENSE │ ├── babel.config.js │ ├── build/ │ │ └── index.js │ ├── jest.config.js │ ├── jsconfig.json │ ├── package.json │ ├── plop-templates/ │ │ ├── component/ │ │ │ ├── index.hbs │ │ │ └── prompt.js │ │ ├── store/ │ │ │ ├── index.hbs │ │ │ └── prompt.js │ │ ├── utils.js │ │ └── view/ │ │ ├── index.hbs │ │ └── prompt.js │ ├── plopfile.js │ ├── postcss.config.js │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.vue │ │ ├── api/ │ │ │ ├── all.js │ │ │ ├── auths.js │ │ │ └── common.js │ │ ├── assets/ │ │ │ └── custom-theme/ │ │ │ └── index.css │ │ ├── components/ │ │ │ ├── Breadcrumb/ │ │ │ │ └── index.vue │ │ │ ├── Charts/ │ │ │ │ ├── keyboard.vue │ │ │ │ ├── lineMarker.vue │ │ │ │ ├── mixChart.vue │ │ │ │ └── mixins/ │ │ │ │ └── resize.js │ │ │ ├── DndList/ │ │ │ │ └── index.vue │ │ │ ├── Hamburger/ │ │ │ │ └── index.vue │ │ │ ├── HeaderSearch/ │ │ │ │ └── index.vue │ │ │ ├── Kanban/ │ │ │ │ └── index.vue │ │ │ ├── LangSelect/ │ │ │ │ └── index.vue │ │ │ ├── MDinput/ │ │ │ │ └── index.vue │ │ │ ├── Pagination/ │ │ │ │ └── index.vue │ │ │ ├── PanThumb/ │ │ │ │ └── index.vue │ │ │ ├── RightPanel/ │ │ │ │ └── index.vue │ │ │ ├── Screenfull/ │ │ │ │ └── index.vue │ │ │ ├── Share/ │ │ │ │ └── DropdownMenu.vue │ │ │ ├── Sticky/ │ │ │ │ └── index.vue │ │ │ ├── SvgIcon/ │ │ │ │ └── index.vue │ │ │ ├── TextHoverEffect/ │ │ │ │ └── Mallki.vue │ │ │ ├── ThemePicker/ │ │ │ │ └── index.vue │ │ │ └── TreeSelect/ │ │ │ └── index.vue │ │ ├── directive/ │ │ │ ├── clipboard/ │ │ │ │ ├── clipboard.js │ │ │ │ └── index.js │ │ │ ├── sticky.js │ │ │ └── waves/ │ │ │ ├── index.js │ │ │ ├── waves.css │ │ │ └── waves.js │ │ ├── filters/ │ │ │ └── index.js │ │ ├── icons/ │ │ │ ├── index.js │ │ │ └── svgo.yml │ │ ├── lang/ │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh.js │ │ ├── layout/ │ │ │ ├── components/ │ │ │ │ ├── AppMain.vue │ │ │ │ ├── Navbar.vue │ │ │ │ ├── Settings/ │ │ │ │ │ └── index.vue │ │ │ │ ├── Sidebar/ │ │ │ │ │ ├── Item.vue │ │ │ │ │ ├── Link.vue │ │ │ │ │ ├── Logo.vue │ │ │ │ │ ├── SidebarItem.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── TagsView/ │ │ │ │ │ ├── ScrollPane.vue │ │ │ │ │ └── index.vue │ │ │ │ └── index.js │ │ │ ├── index.vue │ │ │ └── mixin/ │ │ │ └── ResizeHandler.js │ │ ├── main.js │ │ ├── permission.js │ │ ├── router/ │ │ │ ├── index.js │ │ │ └── modules/ │ │ │ └── components.js │ │ ├── settings.js │ │ ├── store/ │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── modules/ │ │ │ ├── app.js │ │ │ ├── permission.js │ │ │ ├── settings.js │ │ │ ├── tagsView.js │ │ │ └── user.js │ │ ├── styles/ │ │ │ ├── btn.scss │ │ │ ├── csshake.scss │ │ │ ├── element-ui.scss │ │ │ ├── element-variables.scss │ │ │ ├── index.scss │ │ │ ├── mixin.scss │ │ │ ├── sidebar.scss │ │ │ ├── transition.scss │ │ │ └── variables.scss │ │ ├── utils/ │ │ │ ├── auth.js │ │ │ ├── clipboard.js │ │ │ ├── get-page-title.js │ │ │ ├── i18n.js │ │ │ ├── index.js │ │ │ ├── open-window.js │ │ │ ├── permission.js │ │ │ ├── request.js │ │ │ ├── scroll-to.js │ │ │ └── validate.js │ │ ├── vendor/ │ │ │ ├── Export2Excel.js │ │ │ └── Export2Zip.js │ │ └── views/ │ │ ├── components-demo/ │ │ │ ├── clipboard.vue │ │ │ ├── dnd-list.vue │ │ │ ├── drag-kanban.vue │ │ │ ├── mixin.vue │ │ │ └── sticky.vue │ │ ├── dashboard/ │ │ │ ├── components/ │ │ │ │ ├── BarChart.vue │ │ │ │ ├── BoxCard.vue │ │ │ │ ├── LineChart.vue │ │ │ │ ├── PanelGroup.vue │ │ │ │ ├── PieChart.vue │ │ │ │ ├── RaddarChart.vue │ │ │ │ ├── TodoList/ │ │ │ │ │ ├── Todo.vue │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.vue │ │ │ │ ├── TransactionTable.vue │ │ │ │ └── mixins/ │ │ │ │ └── resize.js │ │ │ └── index.vue │ │ ├── error-page/ │ │ │ ├── 401.vue │ │ │ └── 404.vue │ │ ├── icons/ │ │ │ ├── element-icons.js │ │ │ ├── index.vue │ │ │ └── svg-icons.js │ │ ├── login/ │ │ │ └── index.vue │ │ ├── notice/ │ │ │ ├── mail.vue │ │ │ └── telegram.vue │ │ ├── sys/ │ │ │ ├── group.vue │ │ │ ├── menu.vue │ │ │ ├── role.vue │ │ │ └── user.vue │ │ ├── ticket/ │ │ │ ├── all_ticket.vue │ │ │ ├── my_ticket.vue │ │ │ ├── new_ticket.vue │ │ │ ├── s_ticket.vue │ │ │ ├── todo_ticket.vue │ │ │ └── u_ticket.vue │ │ ├── tool/ │ │ │ ├── audit.vue │ │ │ └── test.vue │ │ └── workflow/ │ │ ├── pages/ │ │ │ ├── customfield.vue │ │ │ ├── state.vue │ │ │ └── transition.vue │ │ ├── wfconf.vue │ │ ├── wfset.vue │ │ └── wftype.vue │ └── vue.config.js └── oms_nginx.conf ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.pyc .idea/ .DS_Store .python-version upload/ *.log node_modules/ package-lock.json .vscode/ venv/ *.db ================================================ FILE: 500.html ================================================

5

0

网站正在更新维护中,请稍后2分钟再试!

================================================ FILE: README.md ================================================ # django + vue 工作流管理系统 包含 `用户`、`角色`、`菜单`、`权限` 管理, 这是基础的工作流系统,初始化会生成请假工作流, 也可以自行配置其他工作流比如,发布工单等。 [comment]: <> (- 后端model参考: [loonflow](https://github.com/blackholll/loonflow), 非常不错的一个项目) [comment]: <> (- 前端设计参考: [花裤衩 vue-element-admin](https://github.com/PanJiaChen/vue-element-admin), 大神作品没得说) ## 开发环境 ### 后端 安装依赖 ```bash cd backend pip install -r dev_requirements.txt ``` 初始化系统 - 生成管理员账号 `admin 123456` ```bash python manage.py migrate python manage.py init_sys ``` 生成工作流 - 用户 `ops`,`ops_tl`,`dev`,`dev_tl`,`hr`,`hr_tl` - 密码 `123456` ```bash python manage.py init_wf python manage.py init_ticket python manage.py init_leave ``` 运行 ```bash python manage.py runserver ``` ### 前端 ```bash cd frontend npm install npm run dev ``` ## 开始使用 使用 `admin` 登录 ### 给所有角色分配工作流权限 ![role](https://github.com/itimor/one-workflow/raw/master/gifs/role.png) ### 分配菜单 和 数据 权限 ![role_edit](https://github.com/itimor/one-workflow/raw/master/gifs/role_edit.png) ### 配置假期工作流 ![role](https://github.com/itimor/one-workflow/raw/master/gifs/leave.png) ### 新建工单 ![role](https://github.com/itimor/one-workflow/raw/master/gifs/new.png) ### 编辑工单 ![role](https://github.com/itimor/one-workflow/raw/master/gifs/edit.png) ### 所有工单 ![role](https://github.com/itimor/one-workflow/raw/master/gifs/all.png) ================================================ FILE: backend/__init__.py ================================================ # -*- coding: utf-8 -*- # author: itimor ================================================ FILE: backend/celery.service ================================================ [Unit] Description=start celery worker [Service] ExecStart=/bin/bash -c 'cd /opt/projects/one-oms/backend; /root/.pyenv/versions/envoms/bin/celery -A core worker -B --loglevel=info -f /data/logs/celery.log' #非正常dead,自动重启 Restart=on-failure #3秒后启动 RestartSec=3s KillSignal=SIGQUIT Type=simple NotifyAccess=all [Install] WantedBy=multi-user.target ================================================ FILE: backend/common/JSONRenderer.py ================================================ # -*- coding: utf-8 -*- # author: itimor from rest_framework.renderers import JSONRenderer class CustomJSONRenderer(JSONRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): response_data = {} object_list = 'results' try: meta_dict = getattr(renderer_context.get('view').get_serializer().Meta, 'meta_dict') except: meta_dict = dict() try: data.get('paginated_results') response_data['meta'] = data['meta'] response_data[object_list] = data['results'] except: response_data[object_list] = data response_data['meta'] = dict() response_data['meta'].update(meta_dict) response = super(CustomJSONRenderer, self).render(response_data, accepted_media_type, renderer_context) return response ================================================ FILE: backend/common/__init__.py ================================================ ================================================ FILE: backend/common/dispath.py ================================================ # -*- coding: utf-8 -*- # author: itimor import six from rest_framework.serializers import Serializer from rest_framework.response import Response class JsonResponse(Response): """ An HttpResponse that allows its data to be rendered into arbitrary media types. """ def __init__(self, data=None, code=None, desc=None, status=None, template_name=None, headers=None, exception=False, content_type=None): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. Setting 'renderer' and 'media_type' will typically be deferred, For example being set automatically by the `APIView`. """ super().__init__(None, status=status) if isinstance(data, Serializer): msg = ( 'You passed a Serializer instance as data, but ' 'probably meant to pass serialized `.data` or ' '`.error`. representation.' ) raise AssertionError(msg) self.data = data self.template_name = template_name self.exception = exception self.content_type = content_type if headers: for name, value in six.iteritems(headers): self[name] = value ================================================ FILE: backend/common/django.py ================================================ # -*- coding: utf-8 -*- # author: timor from django.utils.deprecation import MiddlewareMixin class DisableCSRF(MiddlewareMixin): def process_request(self, request): setattr(request, '_dont_enforce_csrf_checks', True) ================================================ FILE: backend/common/exceptions.py ================================================ # -*- coding: utf-8 -*- # author: itimor from rest_framework.views import exception_handler from common import status def JSONExceptionHandler(exc, context): response = exception_handler(exc, context) if response is not None: resp = { 'code': status.HTTP_400_BAD_REQUEST, 'result': dict(response.data) } response.data = resp return response class ExceptionX_Result: exceptionType = None exceptionTitle = None exceptionDetail = None class ExceptionX: @staticmethod def ToString(e): result = ExceptionX_Result tempStr = str(type(e)) tempStrArray = tempStr.split("'") result.exceptionTitle = tempStrArray[1] result.exceptionType = tempStrArray[0][1:] result.exceptionDetail = str(e) if result.exceptionDetail[0] == "<": if result.exceptionDetail[result.exceptionDetail.__len__() - 1] == ">": result.exceptionDetail = result.exceptionDetail[1:result.exceptionDetail.__len__() - 1] return result @staticmethod def PasreRaise(e): result = ExceptionX_Result tempStr = str(type(e)) tempStrArray = tempStr.split("'") result.exceptionTitle = tempStrArray[1] result.exceptionType = tempStrArray[0][1:] result.exceptionDetail = str(e) if result.exceptionDetail[0] == "<": if result.exceptionDetail[result.exceptionDetail.__len__() - 1] == ">": result.exceptionDetail = result.exceptionDetail[1:result.exceptionDetail.__len__() - 1] return result.exceptionDetail ================================================ FILE: backend/common/models.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.db import models class BaseModel(models.Model): create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') memo = models.TextField(blank=True, verbose_name='备注') class Meta: ordering = ['-create_time'] abstract = True ================================================ FILE: backend/common/pagination.py ================================================ # -*- coding: utf-8 -*- # author: itimor from collections import OrderedDict from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination from common import status from common.dispath import JsonResponse def _positive_int(integer_string, strict=False, cutoff=None): """ 分页大小为零不分页 """ ret = int(integer_string) if ret < 0: raise ValueError() if (ret == 0) and strict: return None if cutoff: return min(ret, cutoff) return ret class StandardResultsSetPagination(PageNumberPagination): """ 配置分页规则 """ page_size = 20 page_size_query_param = 'limit' page_query_param = 'page' max_page_size = 1000 def get_paginated_response(self, data): return JsonResponse(OrderedDict([ ('count', self.page.paginator.count), ('next', self.get_next_link()), ('previous', self.get_previous_link()), ('results', data) ], code=status.HTTP_200_OK)) def get_page_size(self, request): if self.page_size_query_param: try: return _positive_int( request.query_params[self.page_size_query_param], strict=True, cutoff=self.max_page_size ) except (KeyError, ValueError): return None return self.page_size class CustomLimitOffsetPagination(LimitOffsetPagination): def get_offset(self, request): try: return (int(request.query_params['offset']) - 1) * int(request.query_params['limit']) except (KeyError, ValueError): return 1 ================================================ FILE: backend/common/status.py ================================================ # -*- coding: utf-8 -*- # author: itimor from __future__ import unicode_literals def is_informational(code): return 100 <= code <= 199 def is_success(code): return 200 <= code <= 299 def is_redirect(code): return 300 <= code <= 399 def is_client_error(code): return 400 <= code <= 499 def is_server_error(code): return 500 <= code <= 599 HTTP_100_CONTINUE = 10000 HTTP_101_SWITCHING_PROTOCOLS = 10100 HTTP_200_OK = 20000 HTTP_201_CREATED = 20100 HTTP_202_ACCEPTED = 20200 HTTP_203_NON_AUTHORITATIVE_INFORMATION = 20300 HTTP_204_NO_CONTENT = 20400 HTTP_205_RESET_CONTENT = 20500 HTTP_206_PARTIAL_CONTENT = 20600 HTTP_207_MULTI_STATUS = 20700 HTTP_300_MULTIPLE_CHOICES = 30000 HTTP_301_MOVED_PERMANENTLY = 30100 HTTP_302_FOUND = 30200 HTTP_303_SEE_OTHER = 30300 HTTP_304_NOT_MODIFIED = 30400 HTTP_305_USE_PROXY = 30500 HTTP_306_RESERVED = 30600 HTTP_307_TEMPORARY_REDIRECT = 30700 HTTP_400_BAD_REQUEST = 40000 HTTP_401_UNAUTHORIZED = 40100 HTTP_402_PAYMENT_REQUIRED = 40200 HTTP_403_FORBIDDEN = 40300 HTTP_404_NOT_FOUND = 40400 HTTP_405_METHOD_NOT_ALLOWED = 40500 HTTP_406_NOT_ACCEPTABLE = 40600 HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 40700 HTTP_408_REQUEST_TIMEOUT = 40800 HTTP_409_CONFLICT = 40900 HTTP_410_GONE = 41000 HTTP_411_LENGTH_REQUIRED = 41100 HTTP_412_PRECONDITION_FAILED = 41200 HTTP_413_REQUEST_ENTITY_TOO_LARGE = 41300 HTTP_414_REQUEST_URI_TOO_LONG = 41400 HTTP_415_UNSUPPORTED_MEDIA_TYPE = 41500 HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 41600 HTTP_417_EXPECTATION_FAILED = 41700 HTTP_422_UNPROCESSABLE_ENTITY = 42200 HTTP_423_LOCKED = 42300 HTTP_424_FAILED_DEPENDENCY = 42400 HTTP_428_PRECONDITION_REQUIRED = 42800 HTTP_429_TOO_MANY_REQUESTS = 42900 HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 43100 HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 45100 HTTP_500_INTERNAL_SERVER_ERROR = 50000 HTTP_501_NOT_IMPLEMENTED = 50100 HTTP_502_BAD_GATEWAY = 50200 HTTP_503_SERVICE_UNAVAILABLE = 50300 HTTP_504_GATEWAY_TIMEOUT = 50400 HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 50500 HTTP_507_INSUFFICIENT_STORAGE = 50700 HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 51100 ================================================ FILE: backend/common/views.py ================================================ # -*- coding: utf-8 -*- # author: itimor from __future__ import print_function, unicode_literals import json from rest_framework.response import Response from collections import OrderedDict from rest_framework import viewsets from django.utils import timezone from rest_framework.decorators import action from common import status from common.dispath import JsonResponse from common.exceptions import * from tools.models import RequestEvent class ModelViewSet(viewsets.ModelViewSet): def __init__(self, *args, **kwargs): super(ModelViewSet, self).__init__(*args, **kwargs) self.resultData = False def watch_audit_log(self, request): ip = request.META.get("HTTP_X_FORWARDED_FOR", "") if not ip: ip = request.META.get('REMOTE_ADDR', "") method = request._request.method RequestEvent.objects.create( url=request.path, method=method, query_string=json.dumps({ 'query_params': request.query_params, 'json': request.data }), user=self.request.user, remote_ip=ip, create_time=timezone.now() ) def create(self, request, *args, **kwargs): self.watch_audit_log(request) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) try: self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK), headers=headers) except Exception as e: print(e) return JsonResponse(OrderedDict([ ('results', {"msg": ExceptionX.PasreRaise(e)}) ], code=status.HTTP_500_INTERNAL_SERVER_ERROR)) def perform_create(self, serializer): serializer.save() def list(self, request, *args, **kwargs): # 不记录list get请求 # self.watch_audit_log(request) queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK)) def retrieve(self, request, *args, **kwargs): self.watch_audit_log(request) instance = self.get_object() serializer = self.get_serializer(instance) return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK)) def update(self, request, *args, **kwargs): self.watch_audit_log(request) partial = kwargs.pop('partial', False) instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) if getattr(instance, '_prefetched_objects_cache', None): instance._prefetched_objects_cache = {} return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK)) def perform_update(self, serializer): serializer.is_valid(raise_exception=True) serializer.save() def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True return self.update(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): self.watch_audit_log(request) instance = self.get_object() self.perform_destroy(instance) return JsonResponse(OrderedDict(code=status.HTTP_200_OK)) def perform_destroy(self, instance): instance.delete() class FKModelViewSet(ModelViewSet): def transer(self, instance=None, id=None): self.resultData = True if self.action == "create": self.kwargs = {'pk': id} instance = self.get_object() serializer = self.get_serializer(instance) return serializer def perform_create(self, serializer): super(FKModelViewSet, self).perform_create(serializer) self.readSerializer = self.transer(id=serializer.data['id']) def perform_update(self, serializer): super(FKModelViewSet, self).perform_update(serializer) self.readSerializer = self.transer(self.readInstance) def perform_destroy(self, instance): super(FKModelViewSet, self).perform_destroy(instance) def create(self, request, *args, **kwargs): self.watch_audit_log(request) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) serializer = self.readSerializer return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK), headers=headers) def update(self, request, *args, **kwargs): self.watch_audit_log(request) partial = kwargs.pop('partial', False) self.readInstance = instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) serializer = self.readSerializer if getattr(instance, '_prefetched_objects_cache', None): instance._prefetched_objects_cache = {} return JsonResponse(OrderedDict([ ('results', serializer.data) ], code=status.HTTP_200_OK)) def destroy(self, request, *args, **kwargs): self.watch_audit_log(request) instance = self.get_object() self.perform_destroy(instance) return JsonResponse(OrderedDict(code=status.HTTP_200_OK)) # 批量操作modelview bulk_create|bulk_delete|bulk_update class BulkModelMixin(ModelViewSet): # 批量添加 @action(methods=['post'], url_path='bulk_create', detail=False) def bulk_create(self, request, *args, **kwargs): """ /api/tool/simple/bulk_create/ :return: """ self.watch_audit_log(request) objs = request.data if not objs: return Response(status=status.HTTP_400_BAD_REQUEST) bulk_models = [] for obj in objs: req = {'id': '0102', 'msg': 'success'} print(obj) try: serializer = self.get_serializer(data=obj) serializer.is_valid(raise_exception=True) self.perform_create(serializer) req['id'] = serializer.data['id'] except Exception as e: req['msg'] = ExceptionX.ToString(e) bulk_models.append(req) return JsonResponse(OrderedDict([ ('results', bulk_models) ], code=status.HTTP_200_OK)) @action(methods=['delete'], url_path='bulk_delete', detail=False) def bulk_delete(self, request, *args, **kwargs): """ /api/tool/simple/bulk_delete/ :return: """ self.watch_audit_log(request) ids = request.data if not ids: return Response(status=status.HTTP_404_NOT_FOUND) bulk_models = [] for id in ids: req = {'id': id, 'msg': 'success'} try: queryset = self.filter_queryset(self.get_queryset()) instance = queryset.get(pk=id) self.perform_destroy(instance) except Exception as e: req['msg'] = ExceptionX.ToString(e) bulk_models.append(req) return JsonResponse(OrderedDict([ ('results', bulk_models) ], code=status.HTTP_200_OK)) @action(methods=['put', 'patch'], url_path='bulk_update', detail=False) def bulk_update(self, request, *args, **kwargs): """ /api/tool/simple/bulk_update/ :return: """ self.watch_audit_log(request) ids = request.data['ids'] obj = request.data['obj'] if not ids or not obj: return Response(status=status.HTTP_400_BAD_REQUEST) bulk_models = [] for id in ids: req = {'id': id, 'msg': 'success'} try: queryset = self.filter_queryset(self.get_queryset()) instance = queryset.get(pk=id) serializer = self.get_serializer(instance, data=obj, partial=True) serializer.is_valid(raise_exception=True) self.perform_update(serializer) except Exception as e: req['msg'] = ExceptionX.ToString(e) bulk_models.append(req) return JsonResponse(OrderedDict([ ('results', bulk_models) ], code=status.HTTP_200_OK)) ================================================ FILE: backend/core/__init__.py ================================================ from __future__ import absolute_import, unicode_literals # This will make sure the app is always imported when # Django starts so that shared_task will use this app. # import pymysql # # pymysql.install_as_MySQLdb() ================================================ FILE: backend/core/celery.py ================================================ # -*- coding: utf-8 -*- # author: itimor from __future__ import absolute_import, unicode_literals import os from celery import Celery # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') celery_app = Celery('core', result_backend='django-db') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. celery_app.config_from_object('django.conf:settings', namespace='CELERY') # Load task modules from all registered Django app configs. celery_app.autodiscover_tasks() celery_app.loader.override_backends['django-db'] = 'django_celery_results.backends.database:DatabaseBackend' ================================================ FILE: backend/core/settings/__init__.py ================================================ # -*- coding: utf-8 -*- # author: itimor import platform from .base import * os_type = platform.system() if os_type == 'Windows': print('进入 dev ') from .dev import * elif os_type == 'Linux': print('进入 prod ') from .prod import * else: print('进入 mac') from .mac import * ================================================ FILE: backend/core/settings/base.py ================================================ """ Django settings for core project. Generated by 'django-admin startproject' using Django 2.1.1. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os import datetime from logging.config import dictConfig # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', # restful api 'django_filters', # 过滤 'corsheaders', # 跨域 'common', 'tools', 'systems', 'workflows', 'tickets', 'notices', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'core.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 'DIRS': [], 'DIRS': ['../frontend/dist'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'core.wsgi.application' # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.1/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' # Add for vuejs STATICFILES_DIRS = [ os.path.join(BASE_DIR, "./templates"), ] MEDIA_ROOT = os.path.join(BASE_DIR, '../upload') MEDIA_URL = '/upload/' REST_USE_JWT = True REST_FRAMEWORK = { 'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S", 'DEFAULT_PAGINATION_CLASS': 'common.pagination.StandardResultsSetPagination', 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.AllowAny', 'rest_framework.permissions.IsAuthenticated', 'systems.permissions.IsOwnerRoles', ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ], } JWT_AUTH = { 'JWT_AUTH_HEADER_PREFIX': 'core', 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30), } # user model AUTH_USER_MODEL = "systems.User" SUPER_ADMIN_USER = 'admin' CORS_ORIGIN_ALLOW_ALL = True LOGGING = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { 'simple': { 'format': '%(levelname)s %(asctime)s %(message)s', 'datefmt': '%y %b %d, %H:%M:%S', }, }, 'handlers': { 'console': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'simple' }, }, 'loggers': { 'django': { 'handlers': ['console'], 'propagate': True, 'level': 'INFO', }, } } dictConfig(LOGGING) ================================================ FILE: backend/core/settings/dev.py ================================================ # -*- coding: utf-8 -*- # author: itimor import os APP_ENV = 'dev' # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ['*'] # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # sqlite DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, '../core.db'), } } # mysql # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.mysql', # 'NAME': 'one', # 'USER': 'root', # 'PASSWORD': 'momo520', # 'HOST': '1.1.1.11', # 'OPTIONS': { # "init_command": "SET foreign_key_checks=0;", # } # } # } # 加载 mysql # import pymysql # pymysql.install_as_MySQLdb() ================================================ FILE: backend/core/settings/mac.py ================================================ # -*- coding: utf-8 -*- # author: itimor import os APP_ENV = 'dev' # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ['*'] # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # sqlite DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, '../core.db'), } } # mysql # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.mysql', # 'NAME': 'one', # 'USER': 'root', # 'PASSWORD': 'momo520', # 'HOST': '1.1.1.11', # 'OPTIONS': { # "init_command": "SET foreign_key_checks=0;", # } # } # } # 加载 mysql # import pymysql # pymysql.install_as_MySQLdb() ================================================ FILE: backend/core/settings/prod.py ================================================ # -*- coding: utf-8 -*- # author: itimor import os APP_ENV = 'prod' # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '64318ob@vbou7h50)b0a_pfda4d$bw2nhl4h*m$qo0_e_fxw=658!z*x' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False ALLOWED_HOSTS = ['*'] # mysql DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'one', 'USER': 'root', 'PASSWORD': 'TY%pwd123', 'HOST': 'localhost', 'OPTIONS': { "init_command": "SET foreign_key_checks=0;", } } } ================================================ FILE: backend/core/urls.py ================================================ # -*- coding: utf-8 -*- # author: timor from django.conf.urls import url, include from django.conf.urls.static import static from django.views.generic.base import TemplateView from core import settings urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \ [ # 工具管理 url(r'api/tool/', include(('tools.urls', 'tools'), namespace="tools")), # 系统管理 url(r'api/sys/', include(('systems.urls', 'systems'), namespace="systems")), # 工作流管理 url(r'api/workflow/', include(('workflows.urls', 'workflows'), namespace="workflows")), # 工单管理 url(r'api/ticket/', include(('tickets.urls', 'tickets'), namespace="tickets")), # 通知管理 url(r'api/notice/', include(('notices.urls', 'notices'), namespace="notices")), ] if settings.APP_ENV == 'prod': from rest_framework.documentation import include_docs_urls urlpatterns += [ # api文档 url(r'^docs/', include_docs_urls(title='X Document')), # 静态模板 url(r'', TemplateView.as_view(template_name="index.html")), ] else: from django.contrib import admin urlpatterns += [ # 管理后台 url(r'^admin/', admin.site.urls), ] ================================================ FILE: backend/core/wsgi.py ================================================ """ WSGI config for core project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') application = get_wsgi_application() ================================================ FILE: backend/dev_requirements.txt ================================================ asgiref==3.2.3 certifi==2019.11.28 chardet==3.0.4 coreapi==2.3.3 coreschema==0.0.4 Django==3.0.3 django-cors-headers==3.2.1 django-filter==2.2.0 django-rest-auth==0.9.5 djangorestframework==3.11.0 djangorestframework-jwt==1.11.0 idna==2.8 IPy==1.0 itypes==1.1.0 Jinja2==2.10.3 MarkupSafe==1.1.1 PyMySQL==0.9.3 PyJWT==1.7.1 pytz==2019.3 requests==2.22.0 six==1.14.0 sqlparse==0.3.0 uritemplate==3.0.1 urllib3==1.25.8 ================================================ FILE: backend/init.sh ================================================ #!/bin/bash apps=(systems tools notices workflows tickets) rm -rf core.db for app in ${apps[@]};do rm -rf $app/migrations done for app in ${apps[@]};do echo $app python manage.py makemigrations $app done python manage.py migrate python manage.py init_sys python manage.py init_wf python manage.py init_ticket python manage.py init_leave python manage.py runserver ================================================ FILE: backend/manage.py ================================================ # -*- coding: utf-8 -*- # author: itimor import os import sys if __name__ == '__main__': os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') # 排错 “The maximum column size is 767 bytes” from django.db.backends.mysql.schema import DatabaseSchemaEditor DatabaseSchemaEditor.sql_create_table += " ROW_FORMAT=DYNAMIC" try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) ================================================ FILE: backend/notices/__init__.py ================================================ ================================================ FILE: backend/notices/management/__init__.py ================================================ ================================================ FILE: backend/notices/management/commands/__init__.py ================================================ ================================================ FILE: backend/notices/management/commands/init_notice.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.core.management.base import BaseCommand, CommandError from systems.models import * from systems.menus import init_menu class Command(BaseCommand): help = '初始化工作流' def handle(self, *args, **options): topmenu = Menu.objects.get(name='top', code='top') self.stdout.write(self.style.SUCCESS('############ 初始化通知菜单 ###########')) noticemenu = Menu.objects.create(name='通知管理', code='notice', curl='/notice', icon='notice', sequence=5, type=1, parent_id=topmenu.id) menumodel = Menu.objects.create(name='mail通知', code='mail', curl='/mail', icon='mail', sequence=10, type=2, parent_id=noticemenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='telegram通知', code='telegram', curl='/telegram', icon='telegram', sequence=20, type=2, parent_id=noticemenu.id) init_menu(menumodel) self.stdout.write(self.style.SUCCESS('初始化完成')) ================================================ FILE: backend/notices/migrations/0001_initial.py ================================================ # Generated by Django 3.0.3 on 2021-06-27 00:05 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='MailBot', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('type', models.CharField(choices=[('mail', 'mail'), ('telegram', 'telegram')], default=0, max_length=10, verbose_name='通知类型')), ('name', models.CharField(max_length=112, unique=True, verbose_name='名称')), ('host', models.CharField(max_length=112, verbose_name='主机')), ('user', models.CharField(max_length=112, verbose_name='账号')), ('password', models.CharField(max_length=112, verbose_name='密码')), ('to', models.CharField(max_length=112, verbose_name='接收者')), ], options={ 'verbose_name': '邮件机器人', 'verbose_name_plural': '邮件机器人', }, ), migrations.CreateModel( name='TelegramBot', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('type', models.CharField(choices=[('mail', 'mail'), ('telegram', 'telegram')], default=0, max_length=10, verbose_name='通知类型')), ('name', models.CharField(max_length=112, unique=True, verbose_name='名称')), ('uid', models.CharField(max_length=112, verbose_name='账号id')), ('token', models.CharField(max_length=112, verbose_name='token')), ('chat_id', models.CharField(max_length=112, verbose_name='chat_id')), ], options={ 'verbose_name': 'tg机器人', 'verbose_name_plural': 'tg机器人', }, ), ] ================================================ FILE: backend/notices/migrations/__init__.py ================================================ ================================================ FILE: backend/notices/models.py ================================================ # -*- coding: utf-8 -*- # author: timor from django.db import models from common.models import BaseModel notice_type = { 'mail': 'mail', 'telegram': 'telegram', } class MailBot(BaseModel): type = models.CharField(max_length=10, choices=tuple(notice_type.items()), default=0, verbose_name='通知类型') name = models.CharField(max_length=112, unique=True, verbose_name='名称') host = models.CharField(max_length=112, verbose_name='主机') user = models.CharField(max_length=112, verbose_name='账号') password = models.CharField(max_length=112, verbose_name='密码') to = models.CharField(max_length=112, verbose_name='接收者') def __str__(self): return self.name class Meta: verbose_name = "邮件机器人" verbose_name_plural = verbose_name class TelegramBot(BaseModel): type = models.CharField(max_length=10, choices=tuple(notice_type.items()), default=0, verbose_name='通知类型') name = models.CharField(max_length=112, unique=True, verbose_name='名称') uid = models.CharField(max_length=112, verbose_name='账号id') token = models.CharField(max_length=112, verbose_name='token') chat_id = models.CharField(max_length=112, verbose_name='chat_id') def __str__(self): return self.name class Meta: verbose_name = "tg机器人" verbose_name_plural = verbose_name ================================================ FILE: backend/notices/serializers.py ================================================ # -*- coding: utf-8 -*- # author: timor from notices.models import * from rest_framework import serializers class MailBotSerializer(serializers.ModelSerializer): class Meta: model = MailBot fields = '__all__' class TelegramBotSerializer(serializers.ModelSerializer): class Meta: model = TelegramBot fields = '__all__' ================================================ FILE: backend/notices/urls.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.conf.urls import url from rest_framework import routers from notices.views import NoticeViewSet, MailBotViewSet, TelegramBotViewSet router = routers.DefaultRouter() router.register(r'notice', NoticeViewSet) router.register(r'mail', MailBotViewSet) router.register(r'telegram', TelegramBotViewSet) urlpatterns = [ ] urlpatterns += router.urls ================================================ FILE: backend/notices/views.py ================================================ # -*- coding: utf-8 -*- # author: timor from notices.serializers import * from common.views import ModelViewSet, JsonResponse from common import status from collections import OrderedDict from rest_framework.decorators import action class NoticeViewSet(ModelViewSet): queryset = MailBot.objects.all() serializer_class = MailBotSerializer # send @action(methods=['post'], url_path='send', detail=False) def send(self, request, *args, **kwargs): self.watch_audit_log(request) data = {'code': 20000, 'msg': 'null'} type = request.GET['type'] if type == 'mail': import smtplib from email.mime.text import MIMEText bot_name = request.GET['bot_name'] bot_obj = MailBot.objects.get(name=bot_name) mail_user = '{}@{}'.format(bot_obj.user, bot_obj.host) tos = bot_obj.to content = request.data.get('content', 'Hello Pornhub') message = MIMEText(content, 'plain', 'utf-8') message['Subject'] = "{}".format(request.form['subject']) message['From'] = mail_user message['To'] = tos[0:] try: smtpObj = smtplib.SMTP_SSL(bot_obj.host) smtpObj.login(mail_user, bot_obj.pasword) smtpObj.sendmail(mail_user, tos, message.as_string()) smtpObj.quit() except smtplib.SMTPException as e: print(e) data['msg'] = 'error' elif type == 'telegram': import telegram bot_name = request.GET['bot_name'] content = request.data.get('content', 'Hello Pornhub') bot_obj = TelegramBot.objects.get(name=bot_name) token = '{}:{}'.format(bot_obj.uid, bot_obj.token) bot = telegram.Bot(token=token) data['msg'] = bot.send_message(chat_id=bot_obj.chat_id, text=content) else: pass return JsonResponse(OrderedDict([ ('results', data) ], code=status.HTTP_200_OK)) class MailBotViewSet(ModelViewSet): queryset = MailBot.objects.all() serializer_class = MailBotSerializer search_fields = ['name'] filter_fields = ['type', 'id', 'name'] ordering_fields = ['name'] class TelegramBotViewSet(ModelViewSet): queryset = TelegramBot.objects.all() serializer_class = TelegramBotSerializer search_fields = ['name'] filter_fields = ['type', 'id', 'name'] ordering_fields = ['name'] ================================================ FILE: backend/oms.ini ================================================ [uwsgi] project = core base = /data/app/one/backend chdir = %(base) module = %(project).wsgi:application master = true processes = 5 enable-threads = true socket = %(base)/%(project).sock chmod-socket = 666 vacuum = true logto = /data/logs/django/one.log ================================================ FILE: backend/oms.service ================================================ [Unit] Description=uWSGI instance to serve one-oms [Service] Type=simple User=root Group=root WorkingDirectory=/data/app/one/backend ExecStart=/root/.pyenv/versions/boce/bin/uwsgi --ini oms.ini --touch-reload=/etc/nginx/uwsgi_params Restart=on-failure KillSignal=SIGQUIT Type=notify NotifyAccess=all [Install] WantedBy=multi-user.target ================================================ FILE: backend/requirements.txt ================================================ asgiref==3.2.3 certifi==2019.11.28 chardet==3.0.4 coreapi==2.3.3 coreschema==0.0.4 Django==3.0.3 django-cors-headers==3.2.1 django-filter==2.2.0 django-rest-auth==0.9.5 djangorestframework==3.11.0 djangorestframework-jwt==1.11.0 idna==2.8 IPy==1.0 itypes==1.1.0 Jinja2==2.10.3 MarkupSafe==1.1.1 python-telegram-bot==12.6.1 mysqlclient==1.4.6 PyJWT==1.7.1 pytz==2019.3 requests==2.22.0 six==1.14.0 sqlparse==0.3.0 uritemplate==3.0.1 urllib3==1.25.8 ================================================ FILE: backend/systems/__init__.py ================================================ # -*- coding: utf-8 -*- # author: timor ================================================ FILE: backend/systems/admin.py ================================================ from django.contrib import admin from systems.models import * admin.site.register(Menu) admin.site.register(Role) admin.site.register(Group) admin.site.register(User) ================================================ FILE: backend/systems/management/__init__.py ================================================ ================================================ FILE: backend/systems/management/commands/__init__.py ================================================ ================================================ FILE: backend/systems/management/commands/init_sys.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.core.management.base import BaseCommand, CommandError from systems.models import * from systems.menus import init_menu from django.contrib.auth.hashers import make_password class Command(BaseCommand): help = '初始化 菜单 角色 用户 用户组' def handle(self, *args, **options): try: self.stdout.write(self.style.SUCCESS('############ 初始化角色 ###########')) role = Role.objects.create(name='top', code='top', sequence=0, parent=None) except: raise CommandError('初始化角色失败') try: self.stdout.write(self.style.SUCCESS('############ 初始化用户组 ###########')) group = Group.objects.create(name='top', code='top', sequence=0, parent=None) group.roles.add(role) except: raise CommandError('初始化用户组失败') try: self.stdout.write(self.style.SUCCESS('############ 初始化用户 ###########')) user = User.objects.create(username='admin', password=make_password("123456"), group=group, is_admin=True) user.roles.add(role) except: raise CommandError('初始化用户失败') menus = Menu.objects.all() if len(menus) == 0: topmenu = Menu.objects.create(name='top', code='top', curl='/top', icon='top', sequence=0, type=1, parent=None) self.stdout.write(self.style.SUCCESS('############ 初始化系统菜单 ###########')) sysmenu = Menu.objects.create(name='系统管理', code='sys', curl='/sys', icon='sys', sequence=1, type=1, parent=topmenu) menumodel = Menu.objects.create(name='角色管理', code='role', curl='/role', icon='role', sequence=30, type=2, parent=sysmenu) init_menu(menumodel) menumodel = Menu.objects.create(name='分组管理', code='group', curl='/group', icon='group', sequence=10, type=2, parent=sysmenu) init_menu(menumodel) menumodel = Menu.objects.create(name='用户管理', code='user', curl='/user', icon='user', sequence=20, type=2, parent=sysmenu) init_menu(menumodel) menumodel = Menu.objects.create(name='菜单管理', code='menu', curl='/menu', icon='menu', sequence=40, type=2, parent=sysmenu) init_menu(menumodel) menumodel = Menu.objects.create(name='图标管理', code='icon', curl='/icon', icon='icon', sequence=50, type=2, parent=sysmenu) init_menu(menumodel) self.stdout.write(self.style.SUCCESS('############ 初始化工具菜单 ###########')) toolmenu = Menu.objects.create(name='工具管理', code='tool', curl='/tool', icon='tool', sequence=2, type=1, parent=topmenu) menumodel = Menu.objects.create(name='审计日志', code='audit', curl='/audit', icon='audit', sequence=10, type=2, parent=toolmenu) init_menu(menumodel) menumodel = Menu.objects.create(name='测试页面', code='test', curl='/test', icon='list', sequence=20, type=2, parent=toolmenu) init_menu(menumodel) self.stdout.write(self.style.SUCCESS('############ 初始化通知菜单 ###########')) noticemenu = Menu.objects.create(name='通知管理', code='notice', curl='/notice', icon='notice', sequence=4, type=1, parent=topmenu) menumodel = Menu.objects.create(name='邮箱通知', code='mail', curl='/mail', icon='mail', sequence=10, type=2, parent=noticemenu) init_menu(menumodel) menumodel = Menu.objects.create(name='telegram通知', code='telegram', curl='/telegram', icon='telegram', sequence=20, type=2, parent=noticemenu) init_menu(menumodel) self.stdout.write(self.style.SUCCESS('初始化完成')) ================================================ FILE: backend/systems/menus.py ================================================ # -*- coding: utf-8 -*- # author: itimor from systems.models import * from itertools import chain # 获取管理员权限下所有菜单 def get_menus_by_user(user): user_obj = User.objects.get(username=user) if user_obj.is_admin: menus = Menu.objects.all() all_roles = Role.objects.all() else: user_roles = user_obj.roles.all() group_roles = user_obj.group.roles.all() all_roles = sorted(chain(user_roles, group_roles), key=lambda t: t.id, reverse=True) print("用户拥有角色: %s" % all_roles) menu_list = [item.menus.all() for item in all_roles if item.menus.all()][0] menuMap = dict() for item in menu_list: menuMap[item.id] = item for item in menu_list: if item.parent_id in menuMap: continue user_menus = find_menu_daddy(item.parent_id, menuMap) menus = [user_menus[i] for i in sorted(user_menus.keys())] roles = [i.code for i in all_roles] return menus, roles def find_menu_daddy(menuid, menuMap): obj = Menu.objects.filter(id=menuid).first() if obj: mid = obj.id if mid not in menuMap: menuMap[mid] = obj find_menu_daddy(obj.parent_id, menuMap) return menuMap def set_menu(menus, parent_id): amenus = [i for i in menus if i.parent_id == parent_id] if len(amenus) == 0: return [] all_menus = [] for item in amenus: menu = { 'path': item.curl, 'component': item.code, 'name': item.code, 'hidden': item.hidden, 'meta': {'title': item.code, 'icon': item.icon, 'no_cache': item.no_cache, 'active_menu': item.active_menu, 'hidden': item.hidden, }, 'children': [] } if item.type == 3: menu['hidden'] = True # 查询是否有子级 menu_children = set_menu(menus, item.id) if len(menu_children) > 0: menu['children'] = menu_children if item.type == 2: # 添加子级首页,有这一级NoCache才有效 menu_index = { 'path': 'index', 'component': item.code, 'name': item.code, 'hidden': item.hidden, 'meta': {'title': item.code, 'icon': item.icon, 'no_cache': item.no_cache, 'active_menu': item.active_menu, 'hidden': item.hidden, }, 'children': [] } menu['children'].append(menu_index) menu['meta'] = [] all_menus.append(menu) return all_menus # 新增菜单后自动添加菜单下的常规操作 def init_menu(menu): if menu.type == 2: menu_list = [ {"name": "新增", "code": menu.code + "_add", "curl": menu.curl + "/add", 'type': 3, "operate": "add", "sequence": 10}, {"name": "删除", "code": menu.code + "_del", "curl": menu.curl + "/del", 'type': 3, "operate": "del", "sequence": 20}, {"name": "编辑", "code": menu.code + "_update", "curl": menu.curl + "/update", 'type': 3, "operate": "update", "sequence": 30}, {"name": "查看", "code": menu.code + "_view", "curl": menu.curl + "/view", 'type': 3, "operate": "view", "sequence": 40}, ] menu_models = [] for item in menu_list: menu_models.append(Menu(name=item['name'], code=item['code'], curl=item['curl'], type=item['type'], operate=item['operate'], sequence=item['sequence'], parent_id=menu.id, hidden=True)) Menu.objects.bulk_create(menu_models) ================================================ FILE: backend/systems/migrations/0001_initial.py ================================================ # Generated by Django 3.0.3 on 2021-06-27 00:05 import django.contrib.auth.models from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ('auth', '0011_update_proxy_permissions'), ] operations = [ migrations.CreateModel( name='Menu', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('name', models.CharField(max_length=32, verbose_name='菜单名称')), ('code', models.CharField(max_length=32, verbose_name='菜单代码')), ('curl', models.CharField(max_length=101, verbose_name='菜单URL')), ('icon', models.CharField(blank=True, max_length=32, verbose_name='菜单图标')), ('hidden', models.BooleanField(default=False, verbose_name='菜单是否隐藏')), ('no_cache', models.BooleanField(default=True, verbose_name='菜单是否缓存')), ('active_menu', models.CharField(blank=True, max_length=32, verbose_name='激活菜单')), ('sequence', models.SmallIntegerField(default=0, verbose_name='排序值')), ('type', models.CharField(choices=[(1, '模块'), (2, '菜单'), (3, '操作')], default=2, max_length=1, verbose_name='菜单类型')), ('status', models.BooleanField(default=True, verbose_name='状态')), ('operate', models.CharField(choices=[('none', '无'), ('add', '新增'), ('del', '删除'), ('update', '编辑'), ('view', '查看')], default='none', max_length=11, verbose_name='操作类型')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='systems.Menu', verbose_name='父级菜单')), ], options={ 'verbose_name': '角色', 'verbose_name_plural': '角色', 'ordering': ['id'], }, ), migrations.CreateModel( name='Role', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('name', models.CharField(max_length=32, unique=True, verbose_name='名称')), ('code', models.CharField(max_length=32, unique=True, verbose_name='代码')), ('sequence', models.SmallIntegerField(default=0, verbose_name='排序值')), ('menus', models.ManyToManyField(blank=True, to='systems.Menu', verbose_name='菜单')), ('model_perms', models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='model权限')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='systems.Role', verbose_name='父级角色')), ], options={ 'verbose_name': '角色', 'verbose_name_plural': '角色', }, ), migrations.CreateModel( name='Group', fields=[ ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('code', models.CharField(max_length=32, unique=True, verbose_name='代码')), ('sequence', models.SmallIntegerField(default=0, verbose_name='排序值')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='systems.Group', verbose_name='父级角色')), ('roles', models.ManyToManyField(blank=True, to='systems.Role', verbose_name='roles')), ], options={ 'verbose_name': '分组', 'verbose_name_plural': '分组', }, bases=('auth.group', models.Model), managers=[ ('objects', django.contrib.auth.models.GroupManager()), ], ), migrations.CreateModel( name='User', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('username', models.CharField(db_index=True, max_length=32, unique=True)), ('realname', models.CharField(blank=True, default='图书馆管理员', max_length=32, verbose_name='真实名字')), ('email', models.EmailField(blank=True, default='itimor@126.com', max_length=254, verbose_name='邮箱')), ('avatar', models.CharField(default='http://m.imeitou.com/uploads/allimg/2017110610/b3c433vwhsk.jpg', max_length=255)), ('status', models.BooleanField(default=True)), ('is_admin', models.BooleanField(default=False)), ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_set', related_query_name='user', to='systems.Group', verbose_name='group')), ('model_perms', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ('roles', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='systems.Role', verbose_name='roles')), ], options={ 'verbose_name': '用户', 'verbose_name_plural': '用户', }, ), ] ================================================ FILE: backend/systems/migrations/__init__.py ================================================ ================================================ FILE: backend/systems/models.py ================================================ # -*- coding: utf-8 -*- # author: timor from django.db import models from django.contrib.auth.models import Permission, Group, GroupManager from django.contrib.auth.models import BaseUserManager, AbstractBaseUser from systems.models import * from common.models import BaseModel menu_type = { 1: '模块', 2: '菜单', 3: '操作', } operate_type = { 'none': '无', 'add': '新增', 'del': '删除', 'update': '编辑', 'view': '查看', } class Menu(BaseModel): parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='父级菜单') name = models.CharField(max_length=32, verbose_name='菜单名称') code = models.CharField(max_length=32, verbose_name='菜单代码') curl = models.CharField(max_length=101, verbose_name='菜单URL') icon = models.CharField(max_length=32, blank=True, verbose_name='菜单图标') hidden = models.BooleanField(default=False, verbose_name='菜单是否隐藏') no_cache = models.BooleanField(default=True, verbose_name='菜单是否缓存') active_menu = models.CharField(max_length=32, blank=True, verbose_name='激活菜单') sequence = models.SmallIntegerField(default=0, verbose_name='排序值') type = models.CharField(max_length=1, choices=tuple(menu_type.items()), default=2, verbose_name='菜单类型') status = models.BooleanField(default=True, verbose_name='状态') operate = models.CharField(max_length=11, choices=tuple(operate_type.items()), default='none', verbose_name='操作类型') def __str__(self): return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '') class Meta: verbose_name = '菜单' verbose_name_plural = verbose_name ordering = ['id'] class Role(BaseModel): parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='父级角色') name = models.CharField(max_length=32, unique=True, verbose_name='名称') code = models.CharField(max_length=32, unique=True, verbose_name='代码') sequence = models.SmallIntegerField(default=0, verbose_name='排序值') menus = models.ManyToManyField(Menu, blank=True, verbose_name='菜单') model_perms = models.ManyToManyField(Permission, blank=True, verbose_name='model权限') def __str__(self): return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '') class Meta: verbose_name = '角色' verbose_name_plural = verbose_name ordering = ['id'] class Group(BaseModel, Group): parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='父级角色') code = models.CharField(max_length=32, unique=True, verbose_name='代码') sequence = models.SmallIntegerField(default=0, verbose_name='排序值') roles = models.ManyToManyField(Role, verbose_name='roles', blank=True, ) def __str__(self): return "{parent}{name}".format(name=self.name, parent="%s-->" % self.parent.name if self.parent else '') class Meta: verbose_name = '分组' verbose_name_plural = verbose_name ordering = ['id'] objects = GroupManager() # 创建用户 class PermissionsMixin(models.Model): group = models.ForeignKey( Group, verbose_name='group', null=True, blank=True, on_delete=models.SET_NULL, related_name="user_set", related_query_name="user", ) roles = models.ManyToManyField( Role, verbose_name='roles', blank=True, related_name="user_set", related_query_name="user", ) model_perms = models.ManyToManyField( Permission, verbose_name='user permissions', blank=True, related_name="user_set", related_query_name="user", ) class Meta: abstract = True ordering = ['id'] class UserManager(BaseUserManager): def create_user(self, username, password=None): """ username 是唯一标识,没有会报错 """ if not username: raise ValueError('Users must have an username') user = self.model( username=username, ) user.set_password(password) # 检测密码合理性 user.save(using=self._db) # 保存密码 return user def create_superuser(self, username, password): user = self.create_user(username=username, password=password, ) user.is_admin = True # 比创建用户多的一个字段 user.save(using=self._db) return user class User(BaseModel, PermissionsMixin, AbstractBaseUser): username = models.CharField(max_length=32, unique=True, db_index=True) realname = models.CharField(max_length=32, default="图书馆管理员", blank=True, verbose_name='真实名字') email = models.EmailField(blank=True, default="itimor@126.com", verbose_name='邮箱') avatar = models.CharField(max_length=255, default='https://pica.zhimg.com/80/v2-e4e933375b971b0941907ff9d7985188_640w.jpg') status = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) USERNAME_FIELD = 'username' # 必须有一个唯一标识--USERNAME_FIELD @property def is_staff(self): return self.is_admin def has_perm(self, perm, obj=None): return self.is_admin def has_module_perms(self, app_label): return self.is_admin def __str__(self): return self.username class Meta: verbose_name = '用户' verbose_name_plural = verbose_name ordering = ['id'] objects = UserManager() # 创建用户 ================================================ FILE: backend/systems/permissions.py ================================================ # -*- coding: utf-8 -*- # author: itimor from rest_framework.permissions import BasePermission from systems.models import * from itertools import chain parse_method_action = { 'GET': 'view', 'POST': 'add', 'PUT': 'change', 'PATCH': 'change', 'DELETE': 'delete', } ignore_path = [ '/api/sys/auth/jwt-token-auth/', '/api/sys/auth/getuserinfo/', '/api/sys/auth/getmenubutons/', ] def check_permission(request, perm): user = User.objects.get(username=request.user) if user.is_admin: return True if request.path in ignore_path: return True user_roles = user.roles.all() group_roles = user.group.roles.all() all_roles = sorted(chain(user_roles, group_roles), key=lambda t: t.id, reverse=True) perms = Permission.objects.filter(role__in=all_roles) for i in perms: if i.codename == perm: return True class IsOwnerRoles(BasePermission): def has_permission(self, request, view): app = view.get_view_name().split() object = ''.join(app[:-1]).lower() perm = 'view_{}'.format(object) return check_permission(request, perm) def has_object_permission(self, request, view, obj): app_label = obj._meta.app_label model = obj._meta.object_name.lower() perm = '{}_{}'.format(parse_method_action[request.method], model) return check_permission(request, perm) ================================================ FILE: backend/systems/serializers.py ================================================ # -*- coding: utf-8 -*- # author: timor from systems.models import * from systems.menus import init_menu from rest_framework import serializers class UserReadSerializer(serializers.ModelSerializer): class Meta: model = User fields = '__all__' depth = 1 class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = '__all__' extra_kwargs = {'password': {'write_only': True, 'required': False}} def create(self, validated_data): roles = validated_data.pop('roles') obj = User.objects.create(**validated_data) if len(roles) > 0: obj.roles.set(roles) else: role = Role.objects.get(id=1) obj.roles.add(role) try: obj.set_password(validated_data['password']) except: pass obj.save() return obj def update(self, instance, validated_data): roles = validated_data.pop('roles') instance.username = validated_data.get('username', instance.username) instance.realname = validated_data.get('realname', instance.realname) instance.group = validated_data.get('group', instance.group) instance.email = validated_data.get('email', instance.email) instance.avatar = validated_data.get('avatar', instance.avatar) instance.status = validated_data.get('status', instance.status) instance.memo = validated_data.get('memo', instance.memo) try: instance.set_password(validated_data['password']) except: pass instance.roles.set(roles) instance.save() return instance class RoleSerializer(serializers.ModelSerializer): class Meta: model = Role fields = '__all__' class GroupSerializer(serializers.ModelSerializer): user_set = UserSerializer(many=True, read_only=True) class Meta: model = Group fields = '__all__' class PermissionSerializer(serializers.ModelSerializer): class Meta: model = Permission fields = '__all__' class MenuSerializer(serializers.ModelSerializer): class Meta: model = Menu fields = '__all__' def create(self, validated_data): obj = Menu.objects.create(**validated_data) if obj.type == 2: init_menu(obj) return obj ================================================ FILE: backend/systems/urls.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.conf.urls import url, include from rest_framework import routers from rest_auth.views import PasswordChangeView from systems.views import UserViewSet, GroupViewSet, RoleViewSet, PermissionViewSet, MenuViewSet, AuthViewSet, \ ObtainJSONWebToken router = routers.DefaultRouter() router.register(r'user', UserViewSet) router.register(r'group', GroupViewSet) router.register(r'role', RoleViewSet) router.register(r'perm', PermissionViewSet) router.register(r'menu', MenuViewSet) router.register(r'auth', AuthViewSet) urlpatterns = [ url(r'^auth/changepwd/', PasswordChangeView.as_view(), name='changepwd'), # token认证 url(r'^auth/jwt-token-auth/', ObtainJSONWebToken.as_view(), name='rest_framework_token'), url(r'^auth/api-token-auth/', include('rest_framework.urls', namespace='rest_framework')), ] urlpatterns += router.urls ================================================ FILE: backend/systems/views.py ================================================ # -*- coding: utf-8 -*- # author: timor from systems.serializers import * from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin from rest_framework.decorators import action from systems.menus import get_menus_by_user, set_menu from common import status from collections import OrderedDict from rest_framework_jwt.serializers import JSONWebTokenSerializer from rest_framework_jwt.views import JSONWebTokenAPIView, jwt_response_payload_handler from rest_framework_jwt.settings import api_settings from datetime import datetime class UserViewSet(BulkModelMixin): queryset = User.objects.all() serializer_class = UserSerializer search_fields = ['username'] filter_fields = ['username', 'group', 'roles'] ordering_fields = ['username', 'status'] class GroupViewSet(BulkModelMixin): queryset = Group.objects.all() serializer_class = GroupSerializer search_fields = ['name'] filter_fields = ['name'] ordering_fields = ['parent_id', 'sequence'] class RoleViewSet(BulkModelMixin): queryset = Role.objects.all() serializer_class = RoleSerializer search_fields = ['name'] filter_fields = ['name'] ordering_fields = ['parent_id', 'sequence'] class PermissionViewSet(ModelViewSet): queryset = Permission.objects.all() serializer_class = PermissionSerializer search_fields = ['name'] filter_fields = ['name'] ordering_fields = ['name'] class MenuViewSet(BulkModelMixin): queryset = Menu.objects.all() serializer_class = MenuSerializer search_fields = ['name'] filter_fields = ['name'] ordering_fields = ['parent_id', 'sequence'] class AuthViewSet(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer @action(methods=['get'], url_path='getuserinfo', detail=False) def getuserinfo(self, request): user = request.user user_obj = User.objects.get(username=user) data, roles = get_menus_by_user(user) if len(data) > 0: topmenuid = data[0].parent_id if not topmenuid: topmenuid = data[0].id menus = set_menu(data, topmenuid) ip = request.META.get("HTTP_X_FORWARDED_FOR", "") if not ip: ip = request.META.get('REMOTE_ADDR', "") data = {'menus': menus, 'username': user_obj.username, 'avatar': user_obj.avatar, 'memo': user_obj.memo, 'ip': ip, 'user_id': user_obj.id, 'roles': roles} return JsonResponse(OrderedDict([ ('results', data) ], code=status.HTTP_200_OK)) @action(methods=['get'], url_path='getmenubutons', detail=False) def getmenubutons(self, request): user = request.user user_obj = User.objects.get(username=user) buttons = [] if user_obj.is_admin: buttons = ['add', 'del', 'update', 'view'] else: menucode = request.GET['menucode'] match_menu = Menu.objects.get(code=menucode) data, roles = get_menus_by_user(user) for item in data: if item.parent_id == match_menu.id: buttons.append(item.operate) data = buttons return JsonResponse(OrderedDict([ ('results', data) ], code=status.HTTP_200_OK)) class ObtainJSONWebToken(JSONWebTokenAPIView): serializer_class = JSONWebTokenSerializer def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') response_data = jwt_response_payload_handler(token, user, request) response = JsonResponse(OrderedDict([ ('results', response_data) ], code=status.HTTP_200_OK)) if api_settings.JWT_AUTH_COOKIE: expiration = (datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA) response.set_cookie(api_settings.JWT_AUTH_COOKIE, token, expires=expiration, httponly=True) return response return JsonResponse(OrderedDict([ ('results', serializer.errors) ], code=status.HTTP_500_INTERNAL_SERVER_ERROR)) ================================================ FILE: backend/tickets/__init__.py ================================================ # -*- coding: utf-8 -*- # author: itimor ================================================ FILE: backend/tickets/filters.py ================================================ # -*- coding: utf-8 -*- # author: itimor from tickets.models import * from django_filters import rest_framework as filters class TicketFilter(filters.FilterSet): class Meta: model = Ticket fields = { 'id': ['exact'], 'name': ['exact'], 'participant': ['exact'], 'create_user__username': ['exact'], "transition__attribute_type": ['exact', "lt"], } ================================================ FILE: backend/tickets/management/__init__.py ================================================ ================================================ FILE: backend/tickets/management/commands/__init__.py ================================================ ================================================ FILE: backend/tickets/management/commands/init_ticket.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.core.management.base import BaseCommand, CommandError from systems.models import * from systems.menus import init_menu class Command(BaseCommand): help = '初始化工作流' def handle(self, *args, **options): topmenu = Menu.objects.get(name='top', code='top') self.stdout.write(self.style.SUCCESS('############ 初始化工单菜单 ###########')) ticketmenu = Menu.objects.create(name='工单系统', code='ticket', curl='/ticket', icon='ticket', sequence=4, type=1, parent_id=topmenu.id) menumodel = Menu.objects.create(name='新建工单', code='new_ticket', curl='/new_ticket', icon='new_ticket', sequence=10, type=2, parent_id=ticketmenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='编辑工单', code='u_ticket', curl='/u_ticket/:id', icon='u_ticket', sequence=10, type=2, hidden=True, active_menu='/new_ticket', parent_id=ticketmenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='审批工单', code='s_ticket', curl='/s_ticket/:id', icon='s_ticket', sequence=10, type=2, hidden=True, active_menu='/todo_ticket', parent_id=ticketmenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='我的工单', code='my_ticket', curl='/my_ticket', icon='my_ticket', sequence=30, type=2, no_cache=True, parent_id=ticketmenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='我的待办', code='todo_ticket', curl='/todo_ticket', icon='todo_ticket', sequence=40, type=2, no_cache=True, parent_id=ticketmenu.id) init_menu(menumodel) menumodel = Menu.objects.create(name='所有工单', code='all_ticket', curl='/all_ticket', icon='all_ticket', sequence=90, type=2, no_cache=True, parent_id=ticketmenu.id) init_menu(menumodel) self.stdout.write(self.style.SUCCESS('初始化完成')) ================================================ FILE: backend/tickets/migrations/0001_initial.py ================================================ # Generated by Django 3.0.3 on 2021-06-27 00:05 from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ('workflows', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Ticket', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('name', models.CharField(blank=True, default='', max_length=112, verbose_name='标题')), ('sn', models.CharField(blank=True, help_text='工单的流水号', max_length=25, verbose_name='流水号')), ('participant', models.CharField(blank=True, default='', max_length=50, verbose_name='当前处理人')), ('customfield', models.TextField(default=[], verbose_name='所有表单数据')), ('relation', models.TextField(blank=True, default='', help_text='创建人、处理人,用于查询', verbose_name='工单关联人')), ('create_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.State', verbose_name='当前状态')), ('transition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='workflows.Transition', verbose_name='进行状态')), ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.Workflow', verbose_name='工作流')), ], options={ 'verbose_name': '工单记录', 'verbose_name_plural': '工单记录', }, ), migrations.CreateModel( name='TicketUser', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('username', models.CharField(max_length=100, verbose_name='关系人')), ('in_process', models.BooleanField(default=False, verbose_name='待处理中')), ('worked', models.BooleanField(default=False, verbose_name='处理过')), ('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tickets.Ticket')), ], options={ 'verbose_name': '工单关系人', 'verbose_name_plural': '工单关系人', }, ), migrations.CreateModel( name='TicketFlowLog', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('suggestion', models.CharField(blank=True, max_length=140, verbose_name='审批意见')), ('participant', models.CharField(blank=True, default='', max_length=50, verbose_name='处理人')), ('intervene_type', models.CharField(choices=[(0, '转交操作'), (1, '接单操作'), (2, '评论操作'), (3, '删除操作'), (4, '强制关闭操作'), (5, '强制修改状态操作'), (6, '撤回')], default=0, max_length=1, verbose_name='干预类型')), ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.State', verbose_name='当前状态')), ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.Ticket', verbose_name='工单')), ('transition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.Transition', verbose_name='流转')), ], options={ 'verbose_name': '工单流转日志', 'verbose_name_plural': '工单流转日志', }, ), migrations.CreateModel( name='TicketCustomField', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('field_value', models.TextField(blank=True, default='', verbose_name='字段值')), ('customfield', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.CustomField', verbose_name='字段')), ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.Ticket', verbose_name='工单')), ], options={ 'verbose_name': '工单自定义字段值', 'verbose_name_plural': '工单自定义字段值', }, ), ] ================================================ FILE: backend/tickets/migrations/__init__.py ================================================ ================================================ FILE: backend/tickets/models.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.db import models from common.models import BaseModel from workflows.models import * from systems.models import User class Ticket(BaseModel): """ 工单记录 """ name = models.CharField(u'标题', max_length=112, blank=True, default='') sn = models.CharField(u'流水号', max_length=25, blank=True, help_text="工单的流水号") create_user = models.ForeignKey(User, verbose_name='创建者', blank=True, null=True, on_delete=models.SET_NULL) participant = models.CharField('当前处理人', max_length=50, default='', blank=True) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='工作流') state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='当前状态') transition = models.ForeignKey(Transition, on_delete=models.SET_NULL, blank=True, null=True, verbose_name='进行状态') customfield = models.TextField('所有表单数据', default=[]) relation = models.TextField('工单关联人', default='', blank=True, help_text='创建人、处理人,用于查询') def __str__(self): return self.name class Meta: verbose_name = '工单记录' verbose_name_plural = verbose_name intervene_type = { 0: '转交操作', 1: '接单操作', 2: '评论操作', 3: '删除操作', 4: '强制关闭操作', 5: '强制修改状态操作', 6: '撤回', } class TicketFlowLog(BaseModel): """ 工单流转日志 """ ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='工单') suggestion = models.CharField('审批意见', max_length=140, blank=True) transition = models.ForeignKey(Transition, on_delete=models.CASCADE, verbose_name='流转') participant = models.CharField('处理人', max_length=50, default='', blank=True) state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='当前状态') intervene_type = models.CharField(max_length=1, choices=tuple(intervene_type.items()), default=0, verbose_name='干预类型') class Meta: verbose_name = '工单流转日志' verbose_name_plural = verbose_name field_type = { 10: '字符串', 15: '整形', 20: '浮点型', 25: '布尔', 30: '日期', 35: '时间', 40: '日期时间', 45: '单选框', 50: '多选框', 55: '下拉列表', 60: '多选下拉列表', 65: '文本域', 70: '用户名', 75: '多选的用户名', } class TicketCustomField(BaseModel): """ 工单自定义字段, 工单自定义字段实际的值。 """ ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='工单') customfield = models.ForeignKey(CustomField, on_delete=models.CASCADE, verbose_name='字段') field_value = models.TextField('字段值', default='', blank=True) class Meta: verbose_name = '工单自定义字段值' verbose_name_plural = verbose_name class TicketUser(BaseModel): """ 工单关系人, 用于加速待办工单及关联工单列表查询 """ ticket = models.ForeignKey(Ticket, null=True, blank=True, on_delete=models.SET_NULL) username = models.CharField('关系人', max_length=100) in_process = models.BooleanField('待处理中', default=False) worked = models.BooleanField('处理过', default=False) def __str__(self): return self.username class Meta: verbose_name = '工单关系人' verbose_name_plural = verbose_name ================================================ FILE: backend/tickets/serializers.py ================================================ # -*- coding: utf-8 -*- # author: itimor from tickets.models import * from workflows.models import * from rest_framework import serializers from utils.index import gen_time_pid import json class TicketReadSerializer(serializers.ModelSerializer): class Meta: model = Ticket fields = '__all__' depth = 1 class TicketSerializer(serializers.ModelSerializer): class Meta: model = Ticket fields = '__all__' def create(self, validated_data): cur_user = self.context['request'].user workflow = validated_data["workflow"] transition = validated_data["transition"] customfield_list = validated_data["customfield"] # save ticket validated_data["sn"] = gen_time_pid(workflow.ticket_sn_prefix) ticket = Ticket.objects.create(**validated_data) # save ticketlog ticketlog = dict() ticketlog["ticket"] = ticket ticketlog["suggestion"] = "没啥意见" ticketlog["state"] = transition.source_state ticketlog["transition"] = transition ticketlog["participant"] = cur_user TicketFlowLog.objects.create(**ticketlog) # save customfield print(ticket.create_time) field_models = [] for item in json.loads(customfield_list): if item['field_key'] == "create_user": field_models.append( TicketCustomField(ticket=ticket, customfield_id=int(item['customfield']), field_value=ticket.create_user), ) elif item['field_key'] == "create_time": field_models.append( TicketCustomField(ticket=ticket, customfield_id=int(item['customfield']), field_value=ticket.create_time), ) elif item['field_key'] == "group": field_models.append( TicketCustomField(ticket=ticket, customfield_id=int(item['customfield']), field_value=ticket.create_user.group), ) elif item['field_key'] == "id": field_models.append( TicketCustomField(ticket=ticket, customfield_id=int(item['customfield']), field_value=ticket.create_user.id), ) else: field_models.append( TicketCustomField(ticket=ticket, customfield_id=int(item['customfield']), field_value=item['field_value']) ) TicketCustomField.objects.bulk_create(field_models) # save ticketuser TicketUser.objects.create(ticket=ticket, username=cur_user, worked=True) TicketUser.objects.create(ticket=ticket, username=ticket.participant, in_process=True) return ticket def update(self, instance, validated_data): cur_user = self.context['request'].user instance.name = validated_data.get('name', instance.name) instance.workflow = validated_data.get('workflow', instance.workflow) instance.sn = validated_data.get('sn', instance.sn) instance.state = validated_data.get('state', instance.state) instance.create_user = validated_data.get('create_user', instance.create_user) instance.participant = validated_data.get('participant', instance.participant) instance.transition = validated_data.get('transition', instance.transition) instance.customfield = validated_data.get('customfield', instance.customfield) instance.memo = validated_data.get('memo', instance.memo) # update relation relation = instance.relation.split(',') relation.append(validated_data['relation']) instance.relation = ','.join(set(relation)) instance.save() # save ticketlog ticketlog = dict() ticketlog["ticket"] = instance ticketlog["suggestion"] = instance.memo ticketlog["state"] = instance.transition.source_state ticketlog["transition"] = instance.transition ticketlog["participant"] = cur_user TicketFlowLog.objects.create(**ticketlog) # save customfield customfield_list = json.loads(instance.customfield) for item in customfield_list: TicketCustomField.objects.filter(id=item["id"]).update(field_value=item["field_value"]) # save ticketuser TicketUser.objects.create(ticket=instance, username=cur_user, worked=True) TicketUser.objects.create(ticket=instance, username=instance.participant, in_process=True) return instance class TicketFlowLogReadSerializer(serializers.ModelSerializer): class Meta: model = TicketFlowLog fields = '__all__' depth = 2 class TicketFlowLogSerializer(serializers.ModelSerializer): class Meta: model = TicketFlowLog fields = '__all__' class TicketCustomFieldReadSerializer(serializers.ModelSerializer): class Meta: model = TicketCustomField fields = '__all__' depth = 1 class TicketCustomFieldSerializer(serializers.ModelSerializer): class Meta: model = TicketCustomField fields = '__all__' class TicketUserSerializer(serializers.ModelSerializer): class Meta: model = TicketUser fields = '__all__' ================================================ FILE: backend/tickets/urls.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.conf.urls import url, include from rest_framework import routers from tickets.views import TicketViewSet, TicketFlowLogViewSet, TicketCustomFieldViewSet, TicketUserViewSet router = routers.DefaultRouter() router.register(r'ticket', TicketViewSet) router.register('ticketflowlog', TicketFlowLogViewSet) router.register(r'ticketcustomfield', TicketCustomFieldViewSet) router.register(r'ticketuser', TicketUserViewSet) urlpatterns = [ ] urlpatterns += router.urls ================================================ FILE: backend/tickets/views.py ================================================ # -*- coding: utf-8 -*- # author: itimor from tickets.serializers import * from tickets.filters import * from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin class TicketViewSet(BulkModelMixin): queryset = Ticket.objects.all() serializer_class = TicketSerializer filterset_class = TicketFilter search_fields = ['name'] ordering_fields = ['state'] def get_serializer_class(self): if self.action in ['list', 'retrieve'] or self.resultData: return TicketReadSerializer return TicketSerializer def get_queryset(self): try: user = User.objects.get(username=self.request.user) if user.is_admin: return Ticket.objects.all() else: return Ticket.objects.filter(relation__icontains=self.request.user).distinct() except Exception as e: print(e) return Ticket.objects.all() class TicketFlowLogViewSet(BulkModelMixin): queryset = TicketFlowLog.objects.all() serializer_class = TicketFlowLogSerializer search_fields = ['ticket'] filter_fields = ['ticket', 'state'] def get_serializer_class(self): if self.action in ['list', 'retrieve'] or self.resultData: return TicketFlowLogReadSerializer return TicketFlowLogSerializer class TicketCustomFieldViewSet(BulkModelMixin): queryset = TicketCustomField.objects.all() serializer_class = TicketCustomFieldSerializer filter_fields = ['ticket', 'customfield'] def get_serializer_class(self): if self.action in ['list', 'retrieve'] or self.resultData: return TicketCustomFieldReadSerializer return TicketCustomFieldSerializer class TicketUserViewSet(BulkModelMixin): queryset = TicketUser.objects.all() serializer_class = TicketUserSerializer search_fields = ['username'] filter_fields = ['username', 'in_process', 'worked'] ================================================ FILE: backend/tools/__init__.py ================================================ # -*- coding: utf-8 -*- # author: timor ================================================ FILE: backend/tools/filesize.py ================================================ # -*- coding: utf-8 -*- # author: timor import math def convert_size(size_bytes): if size_bytes == 0: return "0B" size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") i = int(math.floor(math.log(size_bytes, 1024))) p = math.pow(1024, i) s = round(size_bytes / p, 2) return "%s %s" % (s, size_name[i]) ================================================ FILE: backend/tools/migrations/0001_initial.py ================================================ # Generated by Django 3.0.3 on 2021-06-27 00:05 from django.db import migrations, models import tools.storage class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='FileUpload', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('file', models.FileField(blank=True, upload_to='./tmp', verbose_name='上传文件')), ], options={ 'verbose_name': '文件上传', 'verbose_name_plural': '文件上传', }, ), migrations.CreateModel( name='RequestEvent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('url', models.CharField(db_index=True, max_length=255, verbose_name='请求URI')), ('method', models.CharField(db_index=True, max_length=20, verbose_name='请求方法')), ('query_string', models.TextField(verbose_name='请求内容')), ('user', models.CharField(max_length=255, null=True, verbose_name='用户')), ('remote_ip', models.CharField(db_index=True, max_length=50, null=True, verbose_name='请求IP')), ], options={ 'verbose_name': '请求事件', 'verbose_name_plural': '请求事件', 'ordering': ['-create_time'], }, ), migrations.CreateModel( name='SimpleModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, unique=True, verbose_name='名称')), ], ), migrations.CreateModel( name='Upload', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('memo', models.TextField(blank=True, verbose_name='备注')), ('username', models.CharField(max_length=20, verbose_name='上传用户')), ('file', models.FileField(blank=True, upload_to=tools.storage.PathAndRename('./'), verbose_name='上传文件')), ('archive', models.CharField(blank=True, default='其他', max_length=201, null=True, verbose_name='文件归档')), ('filename', models.CharField(blank=True, max_length=201, null=True, verbose_name='文件名')), ('filepath', models.CharField(blank=True, max_length=201, null=True, verbose_name='文件路径')), ('type', models.CharField(blank=True, max_length=100, null=True, verbose_name='文件类型')), ('size', models.CharField(blank=True, max_length=20, null=True, verbose_name='文件大小')), ], options={ 'verbose_name': '文件上传', 'verbose_name_plural': '文件上传', }, ), ] ================================================ FILE: backend/tools/migrations/__init__.py ================================================ ================================================ FILE: backend/tools/models.py ================================================ # -*- coding: utf-8 -*- # author: timor from django.db import models from common.models import BaseModel from tools.filesize import convert_size from tools.storage import PathAndRename import os class Upload(BaseModel): username = models.CharField(max_length=20, verbose_name=u'上传用户') file = models.FileField(upload_to=PathAndRename("./"), blank=True, verbose_name=u'上传文件') archive = models.CharField(max_length=201, default=u'其他', null=True, blank=True, verbose_name=u'文件归档') filename = models.CharField(max_length=201, null=True, blank=True, verbose_name=u'文件名') filepath = models.CharField(max_length=201, null=True, blank=True, verbose_name=u'文件路径') type = models.CharField(max_length=100, null=True, blank=True, verbose_name=u'文件类型') size = models.CharField(max_length=20, null=True, blank=True, verbose_name=u'文件大小') def save(self, *args, **kwargs): from re import sub self.size = '{}'.format(convert_size(self.file.size)) filename = os.path.splitext(self.file.name) self.filename = '{}-{}{}'.format(sub('\W+', '', filename[0]), self.create_time, filename[1]).replace(' ', '_') self.filepath = '{}/{}'.format(self.archive, self.filename) super(Upload, self).save(*args, **kwargs) def __str__(self): return self.filepath class Meta: verbose_name = u'文件上传' verbose_name_plural = u'文件上传' class FileUpload(models.Model): file = models.FileField(upload_to=("./tmp"), blank=True, verbose_name=u'上传文件') class Meta: verbose_name = u'文件上传' verbose_name_plural = u'文件上传' class RequestEvent(BaseModel): url = models.CharField(max_length=255, null=False, db_index=True, verbose_name='请求URI') method = models.CharField(max_length=20, null=False, db_index=True, verbose_name='请求方法') query_string = models.TextField(verbose_name='请求内容') user = models.CharField(max_length=255, null=True, verbose_name='用户') remote_ip = models.CharField(max_length=50, null=True, db_index=True, verbose_name='请求IP') class Meta: ordering = ['-create_time'] verbose_name = '请求事件' verbose_name_plural = verbose_name class SimpleModel(models.Model): name = models.CharField(max_length=255, unique=True, verbose_name='名称') ================================================ FILE: backend/tools/serializers.py ================================================ # -*- coding: utf-8 -*- # author: timor from rest_framework import serializers from tools.models import * class UploadSerializer(serializers.ModelSerializer): class Meta: model = Upload fields = '__all__' class FileUploadSerializer(serializers.ModelSerializer): class Meta: model = FileUpload fields = '__all__' class RequestEventSerializer(serializers.ModelSerializer): class Meta: model = RequestEvent fields = '__all__' class SimpleSerializer(serializers.ModelSerializer): class Meta(object): model = SimpleModel fields = '__all__' ================================================ FILE: backend/tools/storage.py ================================================ # -*- coding: utf-8 -*- # author: timor import os from django.utils.deconstruct import deconstructible from re import sub @deconstructible class PathAndRename(object): def __init__(self, sub_path): self.path = sub_path def __call__(self, instance, file): filename = os.path.splitext(file) last_filename = "%s-%s%s" % (sub('\W+', '', filename[0]), instance.create_time, filename[1]) return os.path.join(self.path, instance.archive, last_filename) ================================================ FILE: backend/tools/urls.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.conf.urls import url from rest_framework import routers from tools.views import UploadViewSet, FileUploadViewSet, RequestEventViewSet, SimpleViewSet router = routers.DefaultRouter() router.register(r'upload', UploadViewSet) router.register(r'fileupload', FileUploadViewSet) router.register(r'audit', RequestEventViewSet) router.register('simple', SimpleViewSet) urlpatterns = [ ] urlpatterns += router.urls ================================================ FILE: backend/tools/views.py ================================================ # -*- coding: utf-8 -*- # author: timor from rest_framework import viewsets, permissions from tools.models import * from tools.serializers import * from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin class UploadViewSet(ModelViewSet): queryset = Upload.objects.all().order_by("-create_time") serializer_class = UploadSerializer filter_fields = ('username', 'type',) class FileUploadViewSet(ModelViewSet): permission_classes = [permissions.AllowAny] queryset = FileUpload.objects.all() serializer_class = FileUploadSerializer class RequestEventViewSet(ModelViewSet): queryset = RequestEvent.objects.all() serializer_class = RequestEventSerializer search_fields = ['url', 'query_string', 'user', 'remote_ip'] filter_fields = ['method'] class SimpleViewSet(BulkModelMixin): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer permission_classes = [permissions.AllowAny] filter_fields = ['id', 'name'] ================================================ FILE: backend/utils/__init__.py ================================================ # -*- coding: utf-8 -*- # author: timor ================================================ FILE: backend/utils/get_realip.py ================================================ # -*- coding: utf-8 -*- # author: itimor import requests import socket import json #获取外网ip信息 output = requests.get('https://ifconfig.me/all.json').json() # 获取本机计算机ip def get_local_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('8.8.8.8', 80)) ip = s.getsockname()[0] finally: s.close() return ip output['local_ip'] = get_local_ip() ## output """ { "ip_addr": "203.177.78.226", "remote_host": "unavailable", "user_agent": "python-requests/2.22.0", "port": 38542, "method": "GET", "encoding": "gzip, deflate", "mime": "*/*", "via": "1.1 google", "forwarded": "203.177.78.226, 216.239.32.21" "local_ip": "172.16.51.115" }""" with open('d:/ooxx.log', 'a+') as fn: print(output) fn.write(json.dumps(output)) ================================================ FILE: backend/utils/index.py ================================================ # -*- coding: utf-8 -*- # author: timor from datetime import datetime, timedelta import time def gen_time_pid(prefix): pid = '{}_{}'.format(prefix, datetime.now().strftime('%Y%m%d%H%M%S') + str(time.time()).replace('.', '')[-3:]) return pid def diff_times_in_seconds(t1, t2): h1, m1, s1 = t1.hour, t1.minute, t1.second h2, m2, s2 = t2.hour, t2.minute, t2.second t1_secs = s1 + 60 * (m1 + 60 * h1) t2_secs = s2 + 60 * (m2 + 60 * h2) tc = str(timedelta(seconds=(t2_secs - t1_secs))) return tc if __name__ == '__main__': prefix = 'xxoo' print(gen_time_pid(prefix)) ================================================ FILE: backend/utils/mysql.py ================================================ # -*- coding: utf-8 -*- # author: itimor import MySQLdb class MYSQL: def __init__(self, db, sql): self.sql = sql self.conn = MySQLdb.connect( host=db["host"], port=db["port"], user=db["user"], passwd=db["passwd"], db=db["db"], charset='utf8') self.cursor = self.conn.cursor() def insert(self): self.cursor.execute(self.sql) self.conn.commit() self.cursor.close() self.conn.close() return True def select(self): self.cursor.execute(self.sql) alldata = self.cursor.fetchall() self.cursor.close() self.conn.close() return alldata def update(self): self.cursor.execute(self.sql) self.conn.commit() self.cursor.close() self.conn.close() return True if __name__ == '__main__': xxljob_info = { "host": "localhost", "port": 3306, "user": "root", "passwd": "TY%pwd123", "db": "xxl_job", } jobapi = MYSQL(xxljob_info) sql = "select * from xxl_job_group" data = jobapi.insert(sql) rep_data = [] for item in data: json_data = { "id": item[0], "app_name": item[1], "title": item[2], "order": item[3], "address_type": item[4], "address_list": item[5], } rep_data.append(json_data) print(rep_data) ================================================ FILE: backend/utils/sendmail.py ================================================ # -*- coding: utf-8 -*- # author: timor import sys import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from omsBackend.settings import MAIL_ACOUNT # 设置服务器名称、用户名、密码以及邮件后缀 mail_host = MAIL_ACOUNT["mail_host"] mail_user = MAIL_ACOUNT["mail_user"] mail_pass = MAIL_ACOUNT["mail_pass"] mail_postfix = MAIL_ACOUNT["mail_postfix"] # 发送邮件函数 def send_mail(to_list, cc_list, sub, content): me = mail_user + "<" + mail_user + "@" + mail_postfix + ">" # f = open(context) # msg = MIMEText(f.read(),_charset="utf-8") # f.close() # msg = MIMEText(context) msg = MIMEMultipart('alternative') msg['Subject'] = sub msg['From'] = me msg['To'] = to_list msg['Cc'] = cc_list list = msg['Cc'].split(',') list.append(msg['To']) context = MIMEText(content, _subtype='html', _charset='utf-8') # 解决乱码 msg.attach(context) try: send_smtp = smtplib.SMTP() send_smtp.connect(mail_host, 587) send_smtp.starttls() send_smtp.login(mail_user, mail_pass) send_smtp.sendmail(me, list, msg.as_string()) send_smtp.close() return {"code": 'success', "msg": "通知邮件发送成功"} except Exception as e: print(e) return {"code": 'error', "msg": "通知邮件发送失败"} if __name__ == '__main__': to_list = sys.argv[1] # 收件人列表 '111@126.com' cc_list = sys.argv[2] # 抄送人列表 '111@126.com;222@126.com;' sub = sys.argv[3] context = sys.argv[4] if send_mail(to_list, cc_list, sub, context): print({"code": 'success', "msg": "通知邮件发送成功"}) else: print({"code": 'error', "msg": "通知邮件发送失败"}) ================================================ FILE: backend/utils/sendskype.py ================================================ # -*- coding: utf-8 -*- # author: itimor # 登录skype from skpy import Skype # skype账号 SK_ACOUNT = { 'sk_user': 'itimor@126.com', 'sk_pass': 'xxx' } SK = Skype(SK_ACOUNT["sk_user"], SK_ACOUNT["sk_pass"]) def skype_bot(user, content): chat = SK.chats[user] chat.sendMsg(content) if __name__ == '__main__': skypeid = 'live:dafaricky123' user = '8:' + skypeid # skypeid 前面需要加 8 skype_bot(user, "hello,gay,你个逗比,不加好友,发你妹的消息啊") ================================================ FILE: backend/utils/test.py ================================================ #!/bin/bash id=$1 echo $id cp -r site-10 site-$id cd site-$id sed -i "s/10/$id/" * ================================================ FILE: backend/utils/time.py ================================================ # -*- coding: utf-8 -*- # author: timor import time import datetime def utc2local(utc_st): """ UTC时间转本地时间 +8:00 """ now_stamp = time.time() local_time = datetime.datetime.fromtimestamp(now_stamp) utc_time = datetime.datetime.utcfromtimestamp(now_stamp) offset = local_time - utc_time local_st = utc_st + offset return local_st def local2utc(local_st): """ 本地时间转UTC时间 -8:00 """ time_struct = time.mktime(local_st.timetuple()) utc_st = datetime.datetime.utcfromtimestamp(time_struct) return utc_st # '2015-08-28 16:43:37.283' --> 1440751417.283 # 或者 '2015-08-28 16:43:37' --> 1440751417.0 def string2timestamp(strValue): try: d = datetime.datetime.strptime(strValue, "%Y-%m-%d %H:%M:%S.%f") t = d.timetuple() timeStamp = int(time.mktime(t)) timeStamp = float(str(timeStamp) + str("%06d" % d.microsecond)) / 1000000 return int(timeStamp) except ValueError as e: d = datetime.datetime.strptime(strValue, "%Y-%m-%d %H:%M:%S") t = d.timetuple() timeStamp = int(time.mktime(t)) timeStamp = float(str(timeStamp) + str("%06d" % d.microsecond)) / 1000000 return int(timeStamp) # 1440751417.283 --> '2015-08-28 16:43:37.283' def timestamp2string(timeStamp): try: d = datetime.datetime.fromtimestamp(timeStamp) str1 = d.strftime("%Y-%m-%d %H:%M:%S.%f") # 2015-08-28 16:43:37.283000' return str1 except Exception as e: pass ================================================ FILE: backend/utils/verifys.py ================================================ # -*- coding: utf-8 -*- # author: itimor import re import IPy def is_valid_domain(value): domain_pattern = re.compile( r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|' r'([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|' r'([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.' r'([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$' ) return True if domain_pattern.match(value) else False def is_domain(domain): domain_regex = re.compile( r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(? 3 and {days}<10", "target_state_id":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突', verbose_name='条件表达式')), ('attribute_type', models.CharField(choices=[(0, '草稿'), (1, '待审'), (2, '驳回'), (3, '撤销'), (4, '结束'), (5, '已关闭')], default=0, max_length=1, verbose_name='属性类型')), ('alert_enable', models.BooleanField(default=False, verbose_name='点击弹窗提示')), ('alert_text', models.CharField(blank=True, default='', max_length=100, verbose_name='弹窗内容')), ('dest_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dest_state', to='workflows.State', verbose_name='目的状态')), ('source_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_state', to='workflows.State', verbose_name='源状态')), ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.Workflow', verbose_name='工作流')), ], options={ 'verbose_name': '工作流流转', 'verbose_name_plural': '工作流流转', }, ), migrations.AddField( model_name='state', name='workflow', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.Workflow', verbose_name='工作流'), ), migrations.AddField( model_name='customfield', name='workflow', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='workflows.Workflow', verbose_name='工作流'), ), ] ================================================ FILE: backend/workflows/migrations/__init__.py ================================================ ================================================ FILE: backend/workflows/models.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.db import models from common.models import BaseModel from systems.models import * class WorkflowType(BaseModel): name = models.CharField('名称', max_length=50) status = models.BooleanField(default=True) code = models.CharField(max_length=32, unique=True, verbose_name='代码') order_id = models.IntegerField('状态顺序', default=1) def __str__(self): return self.name class Meta: ordering = ['order_id'] verbose_name = '工作流类型' verbose_name_plural = verbose_name class Workflow(BaseModel): """ 工作流 """ name = models.CharField('名称', max_length=50) key = models.CharField('流程标识key', blank=True, max_length=168) ticket_sn_prefix = models.CharField('工单流水号前缀', default='xxoo', max_length=20) status = models.BooleanField(default=True) type = models.ForeignKey(WorkflowType, on_delete=models.CASCADE, verbose_name='工作流类型') roles = models.ManyToManyField(Role, verbose_name='关联角色', blank=True, related_name="workflow_set", related_query_name="workflow") view_permission_check = models.BooleanField('查看权限校验', default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单') limit_expression = models.TextField('限制表达式', default='{}', blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)') display_form_str = models.TextField('展现表单字段', default='[]', blank=True, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') title_template = models.CharField('标题模板', max_length=50, default='你有一个待办工单:{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}') def __str__(self): return self.name class Meta: verbose_name = '工作流' verbose_name_plural = verbose_name field_type = { 1: '字符串', 2: '整型', 3: '浮点型', 4: '布尔', 5: '日期', 6: '日期时间', 7: '范围日期', 8: '文本域', 9: '单选框', 10: '下拉列表', 11: '用户名', 12: '多选框', 13: '多选下拉', 14: '多选用户名', } class CustomField(BaseModel): """自定义字段, 设定某个工作流有哪些自定义字段""" workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='工作流') field_attribute = models.BooleanField('字段是否内置', default=False) field_type = models.CharField(max_length=1, choices=tuple(field_type.items()), default=0, verbose_name='字段类型') field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突') field_name = models.CharField('字段名称', max_length=50) # 内置 field 的 order_id 不要超过10 order_id = models.IntegerField('排序', default=0) default_value = models.CharField('默认值', null=True, blank=True, max_length=100, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值') field_template = models.TextField('文本域模板', default='', blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder') boolean_field_display = models.CharField('布尔类型显示名', max_length=100, default='{}', blank=True, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"是","0":"否"}或{"1":"需要","0":"不需要"},注意数字也需要引号') field_choice = models.CharField('radio、checkbox、select的选项', max_length=255, default='{}', blank=True, help_text='radio,checkbox,select,multiselect类型可供选择的选项,格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号') label = models.CharField('标签', max_length=100, blank=True, default='{}', help_text='自定义标签,json格式,调用方可根据标签自行处理特殊场景逻辑,loonflow只保存文本内容') def __str__(self): return self.field_name class Meta: ordering = ['order_id'] verbose_name = '工作流自定义字段' verbose_name_plural = verbose_name # 0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition) state_type = { 0: '普通状态', 1: '初始状态', 2: '结束状态', } participant_type = { 'none': '无处理人', 'user': '个人', 'group': '部门', 'role': '角色', } class State(BaseModel): """ 状态记录, 变量支持通过脚本获取 """ name = models.CharField('名称', max_length=50) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='工作流') is_hidden = models.BooleanField('是否隐藏', default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)') order_id = models.IntegerField('状态顺序', default=1) state_type = models.CharField(max_length=1, choices=tuple(state_type.items()), default=0, verbose_name='状态类型') enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态') participant_type = models.CharField(max_length=5, choices=tuple(participant_type.items()), default='none', verbose_name='参与者类型') user_participant = models.ManyToManyField(User, blank=True, verbose_name='参与用户') group_participant = models.ManyToManyField(Group, blank=True, verbose_name='参与组') role_participant = models.ManyToManyField(Role, blank=True, verbose_name='参与角色') fields = models.ManyToManyField(CustomField, blank=True, verbose_name='可编辑字段') def __str__(self): return self.name class Meta: ordering = ['order_id'] verbose_name = '工作流状态' verbose_name_plural = verbose_name transition_name = { 0: '保存', 1: '转交下一步', 2: '驳回', 3: '撤销', 4: '关闭', } transition_type = { 0: '常规流转', 1: '定时器流转', } attribute_type = { 0: '草稿', 1: '待审', 2: '驳回', 3: '撤销', 4: '结束', 5: '已关闭', } class Transition(BaseModel): """ 工作流流转,定时器,条件(允许跳过), 条件流转与定时器不可同时存在 """ name = models.CharField('名称类型', max_length=1, choices=tuple(transition_name.items()), default=1) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='工作流') transition_type = models.CharField('流转类型', max_length=1, choices=tuple(transition_type.items()), default=0) timer = models.IntegerField('定时器(单位秒)', default=0, help_text='流转类型设置为定时器流转时生效,单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态') source_state = models.ForeignKey(State, null=True, blank=True, on_delete=models.SET_NULL, related_name="source_state", verbose_name='源状态') dest_state = models.ForeignKey(State, null=True, blank=True, on_delete=models.SET_NULL, related_name="dest_state", verbose_name='目的状态') condition_expression = models.TextField('条件表达式', default='[]', help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state_id":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突') attribute_type = models.CharField(max_length=1, choices=tuple(attribute_type.items()), default=0, verbose_name='属性类型') alert_enable = models.BooleanField('点击弹窗提示', default=False) alert_text = models.CharField('弹窗内容', max_length=100, default='', blank=True) def __str__(self): return self.name class Meta: verbose_name = '工作流流转' verbose_name_plural = verbose_name """ https://github.com/GoldSubmarine/workflow-bpmn-modeler https://github.com/miyuesc/bpmn-process-designer https://juejin.cn/post/6844904069736169480 """ class WorkflowBpmn(BaseModel): workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='工作流') xml = models.TextField('xml数据', blank=True) svg = models.TextField('svg数据', blank=True) class Meta: verbose_name = '工作流bpmn' verbose_name_plural = verbose_name ================================================ FILE: backend/workflows/serializers.py ================================================ # -*- coding: utf-8 -*- # author: itimor from workflows.models import * from rest_framework import serializers class WorkflowReadSerializer(serializers.ModelSerializer): class Meta: model = Workflow fields = '__all__' depth = 1 class WorkflowSerializer(serializers.ModelSerializer): class Meta: model = Workflow fields = '__all__' def create(self, validated_data): roles = validated_data['roles'] del validated_data['roles'] obj = Workflow.objects.create(**validated_data) obj.roles.set(roles) obj.save() # 建立初始和结束状态 State.objects.create(name="开始", order_id=1, state_type=1, is_hidden=True, participant_type=0, workflow=obj) State.objects.create(name="关闭", order_id=99, state_type=2, is_hidden=True, participant_type=0, workflow=obj) # 建立内置字段 CustomField.objects.create(field_name="申请人", order_id=1, field_attribute=True, field_type=1, field_key="create_user", workflow=obj) CustomField.objects.create(field_name="申请时间", order_id=2, field_attribute=True, field_type=6, field_key="create_time", workflow=obj) CustomField.objects.create(field_name="部门", order_id=3, field_attribute=True, field_type=1, field_key="group", workflow=obj) CustomField.objects.create(field_name="工号", order_id=4, field_attribute=True, field_type=1, field_key="id", workflow=obj) return obj class WorkflowTypeSerializer(serializers.ModelSerializer): workflow_list = serializers.SerializerMethodField() # workflow_set = WorkflowSerializer(many=True, read_only=True,) def get_workflow_list(self, instance): a = instance.workflow_set.get_queryset().filter(status=True) return a.values() class Meta: model = WorkflowType fields = '__all__' class StateReadSerializer(serializers.ModelSerializer): class Meta: model = State fields = '__all__' depth = 1 class StateSerializer(serializers.ModelSerializer): class Meta: model = State fields = '__all__' class TransitionReadSerializer(serializers.ModelSerializer): class Meta: model = Transition fields = '__all__' depth = 2 class TransitionSerializer(serializers.ModelSerializer): class Meta: model = Transition fields = '__all__' class CustomFieldSerializer(serializers.ModelSerializer): class Meta: model = CustomField fields = '__all__' class WorkflowBpmnSerializer(serializers.ModelSerializer): class Meta: model = WorkflowBpmn fields = '__all__' ================================================ FILE: backend/workflows/urls.py ================================================ # -*- coding: utf-8 -*- # author: itimor from django.conf.urls import url, include from rest_framework import routers from workflows.views import WorkflowTypeViewSet, WorkflowViewSet, StateViewSet, TransitionViewSet, CustomFieldViewSet, WorkflowBpmnViewSet router = routers.DefaultRouter() router.register(r'workflowtype', WorkflowTypeViewSet) router.register(r'workflow', WorkflowViewSet) router.register('state', StateViewSet) router.register(r'transition', TransitionViewSet) router.register(r'customfield', CustomFieldViewSet) router.register(r'workflowbpmn', WorkflowBpmnViewSet) urlpatterns = [ ] urlpatterns += router.urls ================================================ FILE: backend/workflows/views.py ================================================ # -*- coding: utf-8 -*- # author: itimor from itertools import chain from workflows.serializers import * from common.views import ModelViewSet, FKModelViewSet, JsonResponse, BulkModelMixin class WorkflowTypeViewSet(BulkModelMixin): queryset = WorkflowType.objects.all() serializer_class = WorkflowTypeSerializer search_fields = ['name'] filter_fields = ['name', 'code', 'status'] class WorkflowViewSet(BulkModelMixin): queryset = Workflow.objects.all() serializer_class = WorkflowSerializer search_fields = ['name'] filter_fields = ['name', 'id', 'type'] def get_serializer_class(self): if self.action in ['list', 'retrieve'] or self.resultData: return WorkflowReadSerializer return WorkflowSerializer # def get_queryset(self): # try: # user = User.objects.get(username=self.request.user) # if user.is_admin: # return Workflow.objects.all() # else: # user_roles = user.roles.all() # group_roles = user.group.roles.all() # all_roles = sorted(chain(user_roles, group_roles), key=lambda t: t.id, reverse=True) # return Workflow.objects.filter(roles__in=all_roles).distinct() # except Exception as e: # print(e) # return Workflow.objects.all() class StateViewSet(BulkModelMixin): queryset = State.objects.all() serializer_class = StateSerializer search_fields = ['name'] filter_fields = ['workflow', 'is_hidden'] ordering_fields = ['state_type', 'order_id'] class TransitionViewSet(BulkModelMixin): queryset = Transition.objects.all() serializer_class = TransitionSerializer search_fields = ['name'] filter_fields = ['workflow', 'transition_type', 'source_state', 'dest_state'] def get_serializer_class(self): if self.action in ['list', 'retrieve'] or self.resultData: return TransitionReadSerializer return TransitionSerializer class CustomFieldViewSet(BulkModelMixin): queryset = CustomField.objects.all() serializer_class = CustomFieldSerializer search_fields = ['field_name'] filter_fields = ['workflow', 'field_type', 'field_attribute'] ordering_fields = ['field_type', 'order_id'] class WorkflowBpmnViewSet(BulkModelMixin): queryset = WorkflowBpmn.objects.all() serializer_class = WorkflowBpmnSerializer ================================================ FILE: frontend/.editorconfig ================================================ # https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: frontend/.eslintignore ================================================ build/*.js src/assets public dist ================================================ FILE: frontend/.eslintrc.js ================================================ module.exports = { root: true, parserOptions: { parser: 'babel-eslint', sourceType: 'module' }, env: { browser: true, node: true, es6: true, }, extends: ['plugin:vue/recommended', 'eslint:recommended'], // add your custom rules here //it is base on https://github.com/vuejs/eslint-config-vue rules: { "vue/max-attributes-per-line": [2, { "singleline": 10, "multiline": { "max": 1, "allowFirstLine": false } }], "vue/singleline-html-element-content-newline": "off", "vue/multiline-html-element-content-newline":"off", "vue/name-property-casing": ["error", "PascalCase"], "vue/no-v-html": "off", 'accessor-pairs': 2, 'arrow-spacing': [2, { 'before': true, 'after': true }], 'block-spacing': [2, 'always'], 'brace-style': [2, '1tbs', { 'allowSingleLine': true }], 'camelcase': [0, { 'properties': 'always' }], 'comma-dangle': [2, 'never'], 'comma-spacing': [2, { 'before': false, 'after': true }], 'comma-style': [2, 'last'], 'constructor-super': 2, 'curly': [2, 'multi-line'], 'dot-location': [2, 'property'], 'eol-last': 2, 'eqeqeq': ["error", "always", {"null": "ignore"}], 'generator-star-spacing': [2, { 'before': true, 'after': true }], 'handle-callback-err': [2, '^(err|error)$'], 'indent': [2, 2, { 'SwitchCase': 1 }], 'jsx-quotes': [2, 'prefer-single'], 'key-spacing': [2, { 'beforeColon': false, 'afterColon': true }], 'keyword-spacing': [2, { 'before': true, 'after': true }], 'new-cap': [2, { 'newIsCap': true, 'capIsNew': false }], 'new-parens': 2, 'no-array-constructor': 2, 'no-caller': 2, 'no-console': 'off', 'no-class-assign': 2, 'no-cond-assign': 2, 'no-const-assign': 2, 'no-control-regex': 0, 'no-delete-var': 2, 'no-dupe-args': 2, 'no-dupe-class-members': 2, 'no-dupe-keys': 2, 'no-duplicate-case': 2, 'no-empty-character-class': 2, 'no-empty-pattern': 2, 'no-eval': 2, 'no-ex-assign': 2, 'no-extend-native': 2, 'no-extra-bind': 2, 'no-extra-boolean-cast': 2, 'no-extra-parens': [2, 'functions'], 'no-fallthrough': 2, 'no-floating-decimal': 2, 'no-func-assign': 2, 'no-implied-eval': 2, 'no-inner-declarations': [2, 'functions'], 'no-invalid-regexp': 2, 'no-irregular-whitespace': 2, 'no-iterator': 2, 'no-label-var': 2, 'no-labels': [2, { 'allowLoop': false, 'allowSwitch': false }], 'no-lone-blocks': 2, 'no-mixed-spaces-and-tabs': 2, 'no-multi-spaces': 2, 'no-multi-str': 2, 'no-multiple-empty-lines': [2, { 'max': 1 }], 'no-native-reassign': 2, 'no-negated-in-lhs': 2, 'no-new-object': 2, 'no-new-require': 2, 'no-new-symbol': 2, 'no-new-wrappers': 2, 'no-obj-calls': 2, 'no-octal': 2, 'no-octal-escape': 2, 'no-path-concat': 2, 'no-proto': 2, 'no-redeclare': 2, 'no-regex-spaces': 2, 'no-return-assign': [2, 'except-parens'], 'no-self-assign': 2, 'no-self-compare': 2, 'no-sequences': 2, 'no-shadow-restricted-names': 2, 'no-spaced-func': 2, 'no-sparse-arrays': 2, 'no-this-before-super': 2, 'no-throw-literal': 2, 'no-trailing-spaces': 2, 'no-undef': 2, 'no-undef-init': 2, 'no-unexpected-multiline': 2, 'no-unmodified-loop-condition': 2, 'no-unneeded-ternary': [2, { 'defaultAssignment': false }], 'no-unreachable': 2, 'no-unsafe-finally': 2, 'no-unused-vars': [2, { 'vars': 'all', 'args': 'none' }], 'no-useless-call': 2, 'no-useless-computed-key': 2, 'no-useless-constructor': 2, 'no-useless-escape': 0, 'no-whitespace-before-property': 2, 'no-with': 2, 'one-var': [2, { 'initialized': 'never' }], 'operator-linebreak': [2, 'after', { 'overrides': { '?': 'before', ':': 'before' } }], 'padded-blocks': [2, 'never'], 'quotes': [2, 'single', { 'avoidEscape': true, 'allowTemplateLiterals': true }], 'semi': [2, 'never'], 'semi-spacing': [2, { 'before': false, 'after': true }], 'space-before-blocks': [2, 'always'], 'space-before-function-paren': [2, 'never'], 'space-in-parens': [2, 'never'], 'space-infix-ops': 2, 'space-unary-ops': [2, { 'words': true, 'nonwords': false }], 'spaced-comment': [2, 'always', { 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] }], 'template-curly-spacing': [2, 'never'], 'use-isnan': 2, 'valid-typeof': 2, 'wrap-iife': [2, 'any'], 'yield-star-spacing': [2, 'both'], 'yoda': [2, 'never'], 'prefer-const': 2, 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'object-curly-spacing': [2, 'always', { objectsInObjects: false }], 'array-bracket-spacing': [2, 'never'] } } ================================================ FILE: frontend/.travis.yml ================================================ language: node_js node_js: 10 script: npm run test notifications: email: false ================================================ FILE: frontend/LICENSE ================================================ MIT License Copyright (c) 2017-present PanJiaChen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: frontend/babel.config.js ================================================ module.exports = { presets: [ '@vue/app' ] } ================================================ FILE: frontend/build/index.js ================================================ const { run } = require('runjs') const chalk = require('chalk') const config = require('../vue.config.js') const rawArgv = process.argv.slice(2) const args = rawArgv.join(' ') if (process.env.npm_config_preview || rawArgv.includes('--preview')) { const report = rawArgv.includes('--report') run(`vue-cli-service build ${args}`) const port = 9526 const publicPath = config.publicPath var connect = require('connect') var serveStatic = require('serve-static') const app = connect() app.use( publicPath, serveStatic('./dist', { index: ['index.html', '/'] }) ) app.listen(port, function () { console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) if (report) { console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) } }) } else { run(`vue-cli-service build ${args}`) } ================================================ FILE: frontend/jest.config.js ================================================ module.exports = { moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], transform: { '^.+\\.vue$': 'vue-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '^.+\\.jsx?$': 'babel-jest' }, moduleNameMapper: { '^@/(.*)$': '/src/$1' }, snapshotSerializers: ['jest-serializer-vue'], testMatch: [ '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' ], collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], coverageDirectory: '/tests/unit/coverage', // 'collectCoverage': true, 'coverageReporters': [ 'lcov', 'text-summary' ], testURL: 'http://localhost/' } ================================================ FILE: frontend/jsconfig.json ================================================ { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "exclude": ["node_modules", "dist"] } ================================================ FILE: frontend/package.json ================================================ { "name": "vue-element-admin", "version": "4.0.0", "description": "A magical vue admin. An out-of-box UI solution for enterprise applications. Newest development stack of vue. Lots of awesome features", "author": "Pan ", "license": "MIT", "scripts": { "dev": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "eslint --ext .js,.vue src", "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "src/**/*.{js,vue}": [ "eslint --fix", "git add" ] }, "keywords": [ "vue", "admin", "dashboard", "element-ui", "boilerplate", "admin-template", "management-system" ], "repository": { "type": "git", "url": "git+https://github.com/PanJiaChen/vue-element-admin.git" }, "bugs": { "url": "https://github.com/PanJiaChen/vue-element-admin/issues" }, "dependencies": { "axios": "^0.19.2", "csshake": "^1.5.3", "element-ui": "^2.12.0", "fibers": "^5.0.0", "path-to-regexp": "^6.1.0", "vue": "^2.6.11", "vue-bpmn-modeler": "^1.2.0", "vue-i18n": "^8.15.0", "vue-markdown": "^2.2.4", "vue-route": "^1.5.1", "vuex": "^3.1.1" }, "devDependencies": { "@babel/core": "7.0.0", "@babel/register": "7.0.0", "@vue/cli-plugin-babel": "3.5.3", "@vue/cli-plugin-eslint": "^3.5.1", "@vue/cli-plugin-unit-jest": "3.5.3", "@vue/cli-service": "3.5.3", "babel-core": "^6.26.3", "babel-eslint": "^10.1.0", "chalk": "^2.4.2", "clipboard": "^2.0.4", "echarts": "^4.7.0", "eslint": "^6.8.0", "eslint-plugin-vue": "^6.2.2", "fuse.js": "^3.6.1", "html-webpack-plugin": "^3.2.0", "js-cookie": "^2.2.1", "node-sass": "^4.14.0", "normalize.css": "^8.0.1", "nprogress": "^0.2.0", "sass-loader": "^8.0.2", "screenfull": "^5.0.2", "script-ext-html-webpack-plugin": "^2.1.4", "script-loader": "^0.7.2", "svg-sprite-loader": "^4.2.5", "svgo": "^1.3.2", "vue-count-to": "^1.0.13", "vue-router": "^3.1.6", "vue-template-compiler": "^2.6.11", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }, "engines": { "node": ">=8.9", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } ================================================ FILE: frontend/plop-templates/component/index.hbs ================================================ {{#if template}} {{/if}} {{#if script}} {{/if}} {{#if style}} {{/if}} ================================================ FILE: frontend/plop-templates/component/prompt.js ================================================ const { notEmpty } = require('../utils.js') module.exports = { description: 'generate vue component', prompts: [{ type: 'input', name: 'name', message: 'component name please', validate: notEmpty('name') }, { type: 'checkbox', name: 'blocks', message: 'Blocks:', choices: [{ name: '