Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement timestamp logic #5123

Merged
merged 2 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions specifyweb/attachment_gw/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
from django.db import models
from django.conf import settings
from django.db.models.deletion import CASCADE, SET_NULL
from django.utils import timezone
from model_utils import FieldTracker
from functools import partialmethod
from specifyweb.specify.models import datamodel, custom_save
from ..workbench.models import Dataset
Expand All @@ -15,7 +11,6 @@ class Spattachmentdataset(Dataset):
class Meta:
db_table = 'attachmentdataset'

timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified'])
save = partialmethod(custom_save)

# from django.apps import apps
Expand Down
26 changes: 9 additions & 17 deletions specifyweb/specify/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ def set_field_if_exists(obj, field: str, value) -> None:
if f.concrete:
setattr(obj, field, value)

def _maybe_delete(data: Dict[str, Any], to_delete: str):
if to_delete in data:
del data[to_delete]

def cleanData(model, data: Dict[str, Any], agent) -> Dict[str, Any]:
"""Returns a copy of data with only fields that are part of model, removing
metadata fields and warning on unexpected extra fields."""
Expand Down Expand Up @@ -400,30 +404,18 @@ def cleanData(model, data: Dict[str, Any], agent) -> Dict[str, Any]:

if model is models.Agent:
# setting user agents is part of the user management system.
try:
del cleaned['specifyuser']
except KeyError:
pass
_maybe_delete(cleaned, 'specifyuser')

# guid should only be updatable for taxon and geography
if model not in (models.Taxon, models.Geography):
try:
del cleaned['guid']
except KeyError:
pass
_maybe_delete(cleaned, 'guid')

# timestampcreated should never be updated.
# ... well it is now ¯\_(ツ)_/¯
# New requirments are for timestampcreated to be overridable.
try:
# del cleaned['timestampcreated']
pass
except KeyError:
pass
# _maybe_delete(cleaned, 'timestampcreated')

# Password should be set though the /api/set_password/<id>/ endpoint
if model is models.Specifyuser and 'password' in cleaned:
del cleaned['password']
if model is models.Specifyuser:
_maybe_delete(cleaned, 'password')

return cleaned

Expand Down
33 changes: 3 additions & 30 deletions specifyweb/specify/build_models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from django.db import models
from django.db.models.signals import pre_delete

from model_utils import FieldTracker
from requests import get

from specifyweb.businessrules.exceptions import AbortSave
from . import model_extras
from .model_timestamp import pre_save_auto_timestamp_field_with_override
from .model_timestamp import save_auto_timestamp_field_with_override

appname = __name__.split('.')[-2]

Expand Down Expand Up @@ -64,7 +61,7 @@ class Meta:

def save(self, *args, **kwargs):
try:
return super(model, self).save(*args, **kwargs)
return save_auto_timestamp_field_with_override(super(model, self).save, args, kwargs, self)
except AbortSave:
return

Expand All @@ -76,40 +73,16 @@ def pre_constraints_delete(self):
# This is not currently used, but is here for future use.
pre_delete.send(sender=self.__class__, instance=self)

def save_timestamped(self, *args, **kwargs):
timestamp_override = kwargs.pop('timestamp_override', False)
pre_save_auto_timestamp_field_with_override(self, timestamp_override)
try:
super(model, self).save(*args, **kwargs)
except AbortSave:
return

field_names = [field.name.lower() for field in table.fields]
timestamp_fields = ['timestampcreated', 'timestampmodified']
has_timestamp_fields = any(field in field_names for field in timestamp_fields)

if has_timestamp_fields:
tracked_fields = [field for field in timestamp_fields if field in field_names]
attrs['timestamptracker'] = FieldTracker(fields=tracked_fields)
for field in tracked_fields:
attrs[field] = models.DateTimeField(db_column=field) # default=timezone.now is handled in pre_save_auto_timestamp_field_with_override

attrs['Meta'] = Meta
if table.django_name in tables_with_pre_constraints_delete:
# This is not currently used, but is here for future use.
attrs['pre_constraints_delete'] = pre_constraints_delete

if has_timestamp_fields:
attrs['save'] = save_timestamped
else:
attrs['save'] = save
attrs['save'] = save

supercls = models.Model
if hasattr(model_extras, table.django_name):
supercls = getattr(model_extras, table.django_name)
elif has_timestamp_fields:
# FUTURE: supercls = SpTimestampedModel
pass

model = type(table.django_name, (supercls,), attrs)
return model
Expand Down
9 changes: 4 additions & 5 deletions specifyweb/specify/model_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.conf import settings
from django.utils import timezone

from .model_timestamp import SpTimestampedModel, pre_save_auto_timestamp_field_with_override
from .model_timestamp import save_auto_timestamp_field_with_override
from .tree_extras import Tree, TreeRank

if settings.AUTH_LDAP_SERVER_URI is not None:
Expand All @@ -20,7 +20,7 @@ def create_user(self, name, password=None):
def create_superuser(self, name, password=None):
raise NotImplementedError()

class Specifyuser(models.Model): # FUTURE: class Specifyuser(SpTimestampedModel):
class Specifyuser(models.Model):
USERNAME_FIELD = 'name'
REQUIRED_FIELDS = []
is_active = True
Expand Down Expand Up @@ -117,15 +117,14 @@ def save(self, *args, **kwargs):
if self.id and self.usertype != 'Manager':
self.clear_admin()

pre_save_auto_timestamp_field_with_override(self)
return super(Specifyuser, self).save(*args, **kwargs)
return save_auto_timestamp_field_with_override(super(Specifyuser, self).save, args, kwargs, self)

class Meta:
abstract = True



class Preparation(models.Model): # FUTURE: class Preparation(SpTimestampedModel):
class Preparation(models.Model):
def isonloan(self):
# TODO: needs unit tests
from django.db import connection
Expand Down
84 changes: 34 additions & 50 deletions specifyweb/specify/model_timestamp.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,35 @@
from django.db import models
from django.utils import timezone
from django.conf import settings

from model_utils import FieldTracker

def pre_save_auto_timestamp_field_with_override(obj, timestamp_override=None):
# Normal behavior is to update the timestamps automatically when saving.
# If timestampcreated or timestampmodified have been edited, don't update them to the current time.
cur_time = timezone.now()
timestamp_override = (
timestamp_override
if timestamp_override is not None
else getattr(settings, "TIMESTAMP_SAVE_OVERRIDE", False)
)
timestamp_fields = ['timestampcreated', 'timestampmodified']
for field in timestamp_fields:
if hasattr(obj, field) and hasattr(obj, 'timestamptracker'):
if not timestamp_override and field not in obj.timestamptracker.changed() and \
(not obj.id or not getattr(obj, field)):
setattr(obj, field, cur_time)
elif timestamp_override and not getattr(obj, field):
setattr(obj, field, cur_time)

avoid_null_timestamp_fields(obj)

def avoid_null_timestamp_fields(obj):
cur_time = timezone.now()
if hasattr(obj, 'timestampcreated') and getattr(obj, 'timestampcreated') is None:
obj.timestampcreated = cur_time
if hasattr(obj, 'timestampmodified') and getattr(obj, 'timestampmodified') is None:
obj.timestampmodified = cur_time

# NOTE: This class is needed for when we get rid of dynamic model creation from Specify 6 datamodel.xml file.
# NOTE: Currently in sperate file to avoid circular import.
class SpTimestampedModel(models.Model):
"""
SpTimestampedModel(id, timestampcreated, timestampmodified)
"""

timestampcreated = models.DateTimeField(db_column='TimestampCreated', default=timezone.now)
timestampmodified = models.DateTimeField(db_column='TimestampModified', default=timezone.now)

timestamptracker = FieldTracker(fields=['timestampcreated', 'timestampmodified'])

class Meta:
abstract = True

def save(self, *args, **kwargs):
pre_save_auto_timestamp_field_with_override(self)
super().save(*args, **kwargs)
from django.db.models import Model

timestamp_fields = [('timestampmodified', True), ('timestampcreated', False)]

fields_to_skip = [field[0] for field in timestamp_fields if not field[1]]

def save_auto_timestamp_field_with_override(save_func, args, kwargs, obj):
# If object already is present, reset timestamps to null.
model: Model = obj.__class__
is_forced_insert = kwargs.get('force_insert', False)
fields_to_update = kwargs.get('update_fields', None)
grantfitzsimmons marked this conversation as resolved.
Show resolved Hide resolved
if fields_to_update is None:
fields_to_update = [
field.name for field in model._meta.get_fields(include_hidden=True) if field.concrete
and not field.primary_key
]

if obj.id is not None:
fields_to_update = [
field for field in fields_to_update
if field not in fields_to_skip
]

current = timezone.now()
_set_if_empty(obj, timestamp_fields, current, obj.pk is not None)
new_kwargs = {**kwargs, 'update_fields': fields_to_update} if obj.pk is not None and not is_forced_insert else kwargs
return save_func(*args, **new_kwargs)

def _set_if_empty(obj, fields, default_value, override=False):
for field, can_override in fields:
if not hasattr(obj, field):
continue
if (override and can_override) or getattr(obj, field) is None:
setattr(obj, field, default_value)
Loading
Loading