mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'feature/arcadia-notifications' into 'dev'
Add Arcadia Notifications to Front end See merge request crafty-controller/crafty-4!621
This commit is contained in:
commit
44653f1a67
@ -1,7 +1,7 @@
|
||||
# Changelog
|
||||
## --- [4.2.0] - 2023/TBD
|
||||
### New features
|
||||
TBD
|
||||
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
|
||||
### Bug fixes
|
||||
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
|
||||
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
|
||||
|
@ -45,6 +45,7 @@ class Users(BaseModel):
|
||||
manager = IntegerField(default=None, null=True)
|
||||
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
|
||||
theme = CharField(default="default")
|
||||
cleared_notifs = CharField(default="default")
|
||||
|
||||
class Meta:
|
||||
table_name = "users"
|
||||
@ -171,6 +172,7 @@ class HelperUsers:
|
||||
"roles": [],
|
||||
"servers": [],
|
||||
"support_logs": "",
|
||||
"cleared_notifs": "",
|
||||
}
|
||||
user = model_to_dict(Users.get(Users.user_id == user_id))
|
||||
|
||||
|
@ -579,20 +579,16 @@ class Helpers:
|
||||
|
||||
return version_data
|
||||
|
||||
@staticmethod
|
||||
def get_announcements():
|
||||
data = (
|
||||
'[{"id":"1","date":"Unknown",'
|
||||
'"title":"Error getting Announcements",'
|
||||
'"desc":"Error getting Announcements","link":""}]'
|
||||
)
|
||||
|
||||
def get_announcements(self):
|
||||
data = []
|
||||
try:
|
||||
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
|
||||
response = requests.get("https://craftycontrol.com/notify", timeout=2)
|
||||
data = json.loads(response.content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch notifications with error: {e}")
|
||||
|
||||
if self.update_available:
|
||||
data.append(self.update_available)
|
||||
return data
|
||||
|
||||
def get_version_string(self):
|
||||
|
@ -726,12 +726,21 @@ class TasksManager:
|
||||
def check_for_updates(self):
|
||||
logger.info("Checking for Crafty updates...")
|
||||
self.helper.update_available = self.helper.check_remote_version()
|
||||
remote = self.helper.update_available
|
||||
if self.helper.update_available:
|
||||
logger.info(f"Found new version {self.helper.update_available}")
|
||||
else:
|
||||
logger.info(
|
||||
"No updates found! You are on the most up to date Crafty version."
|
||||
)
|
||||
if self.helper.update_available:
|
||||
self.helper.update_available = {
|
||||
"id": str(remote),
|
||||
"title": f"{remote} Update Available",
|
||||
"date": "",
|
||||
"desc": "Release notes are available by clicking this notification.",
|
||||
"link": "https://gitlab.com/crafty-controller/crafty-4/-/releases",
|
||||
}
|
||||
logger.info("Refreshing Gravatar PFPs...")
|
||||
for user in HelperUsers.get_all_users():
|
||||
if user.email:
|
||||
|
@ -52,6 +52,9 @@ from app.classes.web.routes.api.users.user.permissions import (
|
||||
from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler
|
||||
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
|
||||
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
|
||||
from app.classes.web.routes.api.crafty.announcements.index import (
|
||||
ApiAnnounceIndexHandler,
|
||||
)
|
||||
from app.classes.web.routes.api.crafty.config.index import (
|
||||
ApiCraftyConfigIndexHandler,
|
||||
ApiCraftyCustomizeIndexHandler,
|
||||
@ -77,6 +80,11 @@ def api_handlers(handler_args):
|
||||
ApiAuthInvalidateTokensHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/crafty/announcements/?",
|
||||
ApiAnnounceIndexHandler,
|
||||
handler_args,
|
||||
),
|
||||
(
|
||||
r"/api/v2/crafty/config/?",
|
||||
ApiCraftyConfigIndexHandler,
|
||||
|
110
app/classes/web/routes/api/crafty/announcements/index.py
Normal file
110
app/classes/web/routes/api/crafty/announcements/index.py
Normal file
@ -0,0 +1,110 @@
|
||||
import logging
|
||||
import json
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
notif_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
}
|
||||
|
||||
|
||||
class ApiAnnounceIndexHandler(BaseApiHandler):
|
||||
def get(self):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
(
|
||||
_,
|
||||
_exec_user_crafty_permissions,
|
||||
_,
|
||||
_,
|
||||
_user,
|
||||
) = auth_data
|
||||
|
||||
data = self.helper.get_announcements()
|
||||
cleared = str(
|
||||
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
|
||||
"cleared_notifs"
|
||||
]
|
||||
).split(",")
|
||||
res = [d.get("id", None) for d in data]
|
||||
# remove notifs that are no longer in Crafty.
|
||||
for item in cleared[:]:
|
||||
if item not in res:
|
||||
cleared.remove(item)
|
||||
updata = {"cleared_notifs": ",".join(cleared)}
|
||||
self.controller.users.update_user(auth_data[4]["user_id"], updata)
|
||||
if len(cleared) > 0:
|
||||
for item in data[:]:
|
||||
if item["id"] in cleared:
|
||||
data.remove(item)
|
||||
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": data,
|
||||
},
|
||||
)
|
||||
|
||||
def post(self):
|
||||
auth_data = self.authenticate_user()
|
||||
if not auth_data:
|
||||
return
|
||||
(
|
||||
_,
|
||||
_exec_user_crafty_permissions,
|
||||
_,
|
||||
_,
|
||||
_user,
|
||||
) = auth_data
|
||||
try:
|
||||
data = json.loads(self.request.body)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
return self.finish_json(
|
||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||
)
|
||||
|
||||
try:
|
||||
validate(data, notif_schema)
|
||||
except ValidationError as e:
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "INVALID_JSON_SCHEMA",
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
announcements = self.helper.get_announcements()
|
||||
res = [d.get("id", None) for d in announcements]
|
||||
cleared_notifs = str(
|
||||
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
|
||||
"cleared_notifs"
|
||||
]
|
||||
).split(",")
|
||||
# remove notifs that are no longer in Crafty.
|
||||
for item in cleared_notifs[:]:
|
||||
if item not in res:
|
||||
cleared_notifs.remove(item)
|
||||
if str(data["id"]) in str(res):
|
||||
cleared_notifs.append(data["id"])
|
||||
else:
|
||||
self.finish_json(200, {"status": "error", "error": "INVALID_DATA"})
|
||||
return
|
||||
updata = {"cleared_notifs": ",".join(cleared_notifs)}
|
||||
self.controller.users.update_user(auth_data[4]["user_id"], updata)
|
||||
self.finish_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {},
|
||||
},
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
@ -192,8 +193,8 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||
# remove old server's tasks
|
||||
try:
|
||||
self.tasks_manager.remove_all_server_tasks(server_id)
|
||||
except:
|
||||
logger.info("No active tasks found for server")
|
||||
except JobLookupError as e:
|
||||
logger.info("No active tasks found for server: {e}")
|
||||
self.controller.remove_server(server_id, True)
|
||||
except Exception:
|
||||
return self.finish_json(
|
||||
|
@ -20,11 +20,9 @@ function getDirView(event = false) {
|
||||
console.log("Well that failed");
|
||||
}
|
||||
} else if ($("#root_files_button").hasClass("clicked")) {
|
||||
path = $("#zip_server_path").val();
|
||||
getTreeView(path, true);
|
||||
getTreeView($("#zip_server_path").val(), true);
|
||||
} else {
|
||||
path = $("#file-uploaded").val();
|
||||
getTreeView(path, true, true);
|
||||
getTreeView($("#file-uploaded").val(), true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +130,7 @@ function process_tree_response(response) {
|
||||
}
|
||||
|
||||
function getToggleMain(event) {
|
||||
path = event.target.parentElement.getAttribute('data-path');
|
||||
const path = event.target.parentElement.getAttribute('data-path');
|
||||
document.getElementById("files-tree").classList.toggle("d-block");
|
||||
document.getElementById(path + "span").classList.toggle("tree-caret-down");
|
||||
document.getElementById(path + "span").classList.toggle("tree-caret");
|
||||
|
@ -1,27 +1,32 @@
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link count-indicator">
|
||||
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fas fa-broadcast-tower
|
||||
{% if data.get('update_available') %}
|
||||
text-danger
|
||||
{% end %}
|
||||
"></i>
|
||||
<!-- <span class="count bg-success">3</span>-->
|
||||
</a>
|
||||
"></i><span id="notif-count" class="badge badge-notify"></span> </a>
|
||||
<div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
|
||||
<ul style="padding-top: 10px;" id="announcements">
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link count-indicator" href="/panel/panel_config">
|
||||
<a class="nav-link" href="/panel/panel_config">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown user-dropdown">
|
||||
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
|
||||
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a>
|
||||
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
|
||||
alt="Profile image"> </a>
|
||||
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
|
||||
<div class="dropdown-header text-center">
|
||||
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image">
|
||||
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
|
||||
alt="Profile image">
|
||||
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
|
||||
<p class="font-weight-light text-muted mb-0">Roles: </p>
|
||||
{% for r in data['user_role'] %}
|
||||
@ -33,27 +38,130 @@
|
||||
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
|
||||
</div>
|
||||
{% if data['user_data']['preparing'] %}
|
||||
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}<br><br></span>
|
||||
<span class="dropdown-item" id="support_progress"><i
|
||||
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
data['lang']) }}<br><br></span>
|
||||
<span class="dropdown-item" id="support_progress">
|
||||
<div class="support_progress" style="height: 15px; width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
|
||||
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
</span>
|
||||
{% else %}
|
||||
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a>
|
||||
<a class="dropdown-item" id="support_logs"><i
|
||||
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
|
||||
data['lang']) }}</i></a>
|
||||
{% end %}
|
||||
{% if data['superuser'] %}
|
||||
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a>
|
||||
<a class="dropdown-item" href="/panel/activity_logs"><i
|
||||
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
|
||||
'activityLog', data['lang']) }}</a>
|
||||
{% end %}
|
||||
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
|
||||
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
|
||||
translate('notify', 'logout', data['lang']) }}</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<style>
|
||||
.badge-notify {
|
||||
background: var(--purple);
|
||||
position: absolute;
|
||||
-moz-transform: translate(-70%, -70%);
|
||||
/* For Firefox */
|
||||
-ms-transform: translate(-70%, -70%);
|
||||
/* for IE */
|
||||
-webkit-transform: translate(-70%, -70%);
|
||||
/* For Safari, Chrome, iOS */
|
||||
-o-transform: translate(-70%, -70%);
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.notif-div::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.notif-div {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function pfpError(image) {
|
||||
image.onerror = "";
|
||||
image.src = "/static/assets/images/faces-clipart/pic-3.png";
|
||||
return true;
|
||||
}
|
||||
function updateAnnouncements(data) {
|
||||
console.log(data)
|
||||
let text = "";
|
||||
for (let value of data) {
|
||||
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
|
||||
}
|
||||
if (data.length > 0) {
|
||||
localStorage.setItem("notif-count", data.length);
|
||||
$("#notif-count").html(data.length);
|
||||
$("#announcements").html(text);
|
||||
} else {
|
||||
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
|
||||
</p>`);
|
||||
}
|
||||
$(".clear-button").on("click", function (event) {
|
||||
console.log("CLEAR BUTTON")
|
||||
let uuid = event.target.getAttribute("data-id");
|
||||
$(`#${uuid}`).remove();
|
||||
send_clear(uuid);
|
||||
let notif_count = localStorage.getItem("notif-count") - 1;
|
||||
if (notif_count > 0) {
|
||||
localStorage.setItem("notif-count", notif_count);
|
||||
$("#notif-count").html(notif_count);
|
||||
} else {
|
||||
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
|
||||
</p>`)
|
||||
$("#notif-count").html("");
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
async function getAnnouncements() {
|
||||
var token = getCookie("_xsrf");
|
||||
let res = await fetch(`/api/v2/crafty/announcements/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-XSRFToken': token
|
||||
},
|
||||
});
|
||||
let responseData = await res.json();
|
||||
console.log(responseData);
|
||||
if (responseData.status === "ok") {
|
||||
updateAnnouncements(responseData.data)
|
||||
} else {
|
||||
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
|
||||
}
|
||||
}
|
||||
async function send_clear(uuid) {
|
||||
var token = getCookie("_xsrf");
|
||||
let body = JSON.stringify({ "id": uuid });
|
||||
console.log(body)
|
||||
let res = await fetch(`/api/v2/crafty/announcements/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-XSRFToken': token,
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
let responseData = await res.json();
|
||||
console.log(responseData);
|
||||
if (responseData.status === "ok") {
|
||||
return
|
||||
} else {
|
||||
bootbox.alert(responseData.error)
|
||||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
getAnnouncements();
|
||||
})
|
||||
</script>
|
@ -168,10 +168,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{
|
||||
translate('serverBackups', 'cancel', data['lang']) }}</button>
|
||||
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
|
||||
translate('serverWizard', 'save', data['lang']) }}</button>
|
||||
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
|
||||
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -201,8 +201,8 @@
|
||||
let responseData = await res.json();
|
||||
let html = ``
|
||||
if (responseData.status === "ok") {
|
||||
for (let i = 0; i < responseData.data.length; i++) {
|
||||
html += `<span class='box'>${responseData.data[i]}<br /></span>`
|
||||
for (let value of responseData.data) {
|
||||
html += `<span class='box'>${value}<br /></span>`
|
||||
}
|
||||
console.log('Got Log From Server')
|
||||
$('#virt_console').html(html);
|
||||
|
@ -240,8 +240,8 @@
|
||||
let responseData = await res.json();
|
||||
let html = ``
|
||||
if (responseData.status === "ok") {
|
||||
for (let i = 0; i < responseData.data.length; i++) {
|
||||
html += `<span class='box'>${responseData.data[i]}<br /></span>`
|
||||
for (let value of responseData.data) {
|
||||
html += `<span class='box'>${value}<br /></span>`
|
||||
}
|
||||
console.log('Got Log From Server')
|
||||
$('#virt_console').html(html);
|
||||
@ -385,7 +385,6 @@
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
let scrolled = false;
|
||||
$('#virt_console').on('scroll', chkScroll);
|
||||
$('#to-bottom').on('click', scrollToBottom)
|
||||
});
|
||||
|
@ -890,7 +890,6 @@
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
let doUpload = false;
|
||||
}
|
||||
}, false);
|
||||
xmlHttpRequest.addEventListener('error', (e) => {
|
||||
@ -1198,7 +1197,7 @@
|
||||
let idx = document.getElementById('server_type').selectedIndex;
|
||||
// get the value of the selected option
|
||||
let cSelect = document.getElementById("server");
|
||||
let which;
|
||||
let which = {};
|
||||
try {
|
||||
which = document.getElementById('server_type').options[idx].value;
|
||||
} catch {
|
||||
@ -1234,10 +1233,10 @@
|
||||
}
|
||||
|
||||
function serverJarChange(selectObj) {
|
||||
let type_select = document.getElementById('server_jar')
|
||||
let tidx = type_select.selectedIndex;
|
||||
let val = type_select.options[tidx].value;
|
||||
let jcSelect = "";
|
||||
const type_select = document.getElementById('server_jar')
|
||||
const tidx = type_select.selectedIndex;
|
||||
const val = type_select.options[tidx].value;
|
||||
let jcSelect = {};
|
||||
if (val == 'None') {
|
||||
jcSelect = document.getElementById("server_type");
|
||||
while (jcSelect.options.length > 0) {
|
||||
@ -1251,7 +1250,7 @@
|
||||
// get the value of the selected option
|
||||
let jwhich = selectObj.options[jidx].value;
|
||||
// use the selected option value to retrieve the list of items from the serverTypesLists array
|
||||
jcList = Object.keys(serverTypesLists[jwhich]);
|
||||
let jcList = Object.keys(serverTypesLists[jwhich]);
|
||||
// get the country select element via its known id
|
||||
jcSelect = document.getElementById("server_type");
|
||||
// remove the current options from the country select
|
||||
|
16
app/migrations/20230901_user_notif.py
Normal file
16
app/migrations/20230901_user_notif.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by database migrator
|
||||
import peewee
|
||||
|
||||
|
||||
def migrate(migrator, database, **kwargs):
|
||||
migrator.add_columns("users", cleared_notifs=peewee.CharField(default=""))
|
||||
"""
|
||||
Write your migrations here.
|
||||
"""
|
||||
|
||||
|
||||
def rollback(migrator, database, **kwargs):
|
||||
migrator.drop_columns("users", ["cleared_notifs"])
|
||||
"""
|
||||
Write your rollback migrations here.
|
||||
"""
|
Loading…
Reference in New Issue
Block a user