Skip to content

Commit

Permalink
Merge pull request #5123 from specify/issue-5108
Browse files Browse the repository at this point in the history
Reimplement timestamp logic
  • Loading branch information
realVinayak authored Jul 22, 2024
2 parents 256d3c9 + ccd714e commit 08dc104
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 369 deletions.
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)
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

0 comments on commit 08dc104

Please sign in to comment.